What is the effect of "-ScriptBlock" in "New-Module -AsCustomObject" - powershell

I am new with powershell and I had been researching how to create custom objects.
With "New-Module" I always find results like "New-Module -AsCustomObject -ScriptBlock".
Like this for example:
$myObject = New-Module -AsCustomObject -ScriptBlock {
function myFunc(){
return "myFunc";
}
}
but when I try without "-ScriptBlock":
$myObject = New-Module -AsCustomObject {
function myFunc(){
return "myFunc";
}
}
it appears to have the same effect to me. I get a custom object with a function myFunc in both cases.
Am I missing something? Or actually it will have no difference in this particular case?

New-Module -AsCustomObject -ScriptBlock { function myFunc(){ return "myFunc"; } }
passes the script block { ... } as a named argument to the -ScriptBlock parameter. That is, the target parameter is explicitly named.
PowerShell commands can choose to alternatively support positional arguments, where the target parameter is not named, and instead the relative position among all unnamed arguments implies the target parameter.
The New-Module cmdlet's first positional parameter is indeed -ScriptBlock, which means that
-ScriptBlock before the { ... } block can be omitted here, which is why your two commands are equivalent:
# Same as above, except that -ScriptBlock is *implied*, positionally.
New-Module -AsCustomObject { function myFunc(){ return "myFunc"; } }
(Note that the -AsCustomObject switch parameter (flag) is by definition named, so it has no impact on positional parameter binding.)
You can learn which of a given command's parameters are positional by looking at its syntax diagrams, which you can obtain with New-Module -? or Get-Command New-Module -Syntax:
# Note: PowerShell commands can have multiple *parameter sets*, with
# distinct parameter combinations.
# Here, I've omitted the 2nd parameter set that doesn't apply to
# your call, because it doesn't use the -Name parameter.
PS> Get-Command -Syntax New-Module
New-Module [-ScriptBlock] <scriptblock> [-Function <string[]>] [-Cmdlet <string[]>] [-ReturnResult] [-AsCustomObject] [-ArgumentList <Object[]>] [<CommonParameters>]
The [...] around a parameter name tells you that a given parameter accepts a positional argument, and in the case at hand that only applies to -ScriptBlock, so it is the first and only positional parameter in that parameter set.
To learn more about:
how to read syntax diagrams, including a helper function that programmatically lists a command's positional parameters, see this answer
declaring your own positional parameters when authoring functions and scripts, see this answer.

Related

Print all positional arguments in PowerShell

I'm trying to write a PowerShell function that prints out all its arguments.
ArgChecker.ps1
function Print-Args
{
[CmdletBinding()]
param ([string[]]$words)
Write-Verbose "Count: $($words.Count)"
Write-Output "Passed arguments:"
$words
}
I'd also like to call it from a command prompt.
I'm doing it like this
powershell -ExecutionPolicy Bypass -command "& { . .\ArgChecker.ps1; Print-Args 'hi' 'Two' -Verbose }"
but it is throwing an error Print-Args : A positional parameter cannot be found that accepts argument 'Two'.
Is there any way to write the function so that it can accept an unlimited amount of parameters in that format? I think I'm looking for something similar to the params keyword from C#.
In regular functions, you can use the $args automatic variable.
function Write-Args {
$args
}
If you need a cmdlet with CmdletBinding, you can use the ValueFromRemainingArguments option:
function Write-Args {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments)]
[string[]]$Arguments
)
$Arguments
}
Note however, because of the CmdletBinding, -Verbose will not appear in that list, because it's a common parameter. If you want to list those too, you could use the $PSBoundParameters automatic variable.

Find the values in ValidateSet

I was wondering if there was a way to retrieve the values used in the clause Param() for ValidateSet. Something like this would be great:
Function Foo {
Param (
[ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
[String]$Type = 'Startup'
)
$Type.ValidateSet
}
But of course there is no such property on the Type object. Is it possible to retrieve the values set in ValidateSet?
function Foo {
param (
[ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
[String]$Type = 'Startup'
)
$ParameterList = (Get-Command -Name $MyInvocation.MyCommand).Parameters
$ParameterList["Type"].Attributes.ValidValues
}
After your comment:
param (
[ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
[String]$Type = 'Startup'
)
(Get-Variable "Type").Attributes.ValidValues
The Get-Variable call also works in a function.
All solutions below work in both functions and scripts.
Most robust solution that should work in any invocation scenario, PSv2+:
param (
[ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
[String]$Type = 'Startup'
)
# -> #('Startup', 'Shutdown', ...)
($MyInvocation.MyCommand.Parameters['Type'].Attributes |
Where-Object { $_ -is [ValidateSet] }).ValidValues
A simpler, but potentially fragile PSv3+ solution, which assumes:
that Set-StrictMode is either set to -Version 1 or not set.
Set-StrictMode may have been set outside of your control, so if you don't fully control the execution environment, it is safer to use the more verbose, PSv2-compatible command above.
(The Set-StrictMode setting behaves like a variable: it is inherited by descendent scopes, but setting it in a descendent scope sets it locally (only affects that scope and its descendants).)
However:
You can explicitly run Set-StrictMode -Off or Set-StrictMode -Version 1 at the start of your script / function, though you may want to restore the desired value afterwards. Whatever mode you set will affect descendant scopes too. Note that that there is no way to query the strict mode currently in effect.
If you define a function as part of a module, the outside world's Set-StrictMode setting does not apply.
that running into this bug (still present as of PowerShell 7.3.1) when repeatedly dot-sourcing a script is not a concern.
param (
[ValidateSet('Startup', 'Shutdown', 'LogOn', 'LogOff')]
[String]$Type = 'Startup'
)
# Assumes that at most Set-StrictMode -Version 1 is in effect.
# You could explicitly run Set-StrictMode -Off or Set-StrictVersion -Version 1
# in here first.
(Get-Variable Type).Attributes.ValidValues
Optional background information
The PSv3+ shorthand syntax (Get-Variable Type).Attributes.ValidValues is essentially the equivalent of:
(Get-Variable Type).Attributes | ForEach-Object { $_.ValidValues }
That is, PowerShell automatically enumerates the collection .Attributes and collects the values of each element's .ValidValues property.
In the case at hand, only one attribute in the .Attributes collection - the one of subtype [System.Management.Automation.ValidateSetAttribute] - has a .ValidValues property, so that single value is returned.
Given that the other attributes have no such property, setting Set-StrictMode to -version 2 or higher causes the attempt to access a nonexistent property to raise an error, and the command fails.
((Get-Variable Type).Attributes |
Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues
bypasses this problem by explicitly targeting the one attribute of interest (using the -is operator to identify it by type) that is known to have a .ValidValues property.
The more verbose alternative to accessing the attributes of parameter [variable] $Type with (Get-Variable Type).Attributes is to use $MyInvocation.MyCommand.Parameters['Type'].Attributes.
Use of the $MyInvocation.MyCommand.Parameters collection enables enumerating and inspecting all parameters without needing to know their names in advance.
David Brabant's answer is helpful, but (as of this writing):
It may create the mistaken impression that separate approaches are needed for scripts and functions.
The Get-Command -Name $MyInvocation.MyCommand part is:
unnecessary, because $MyInvocation.MyCommand itself provides the information of interest:
$MyInvocation.MyCommand is an instance of type [System.Management.Automation.ExternalScriptInfo] in scripts, and type [System.Management.Automation.FunctionInfo] in functions, both of which derive from type [System.Management.Automation.CommandInfo], which is the type that Get-Commmand returns - so not only do they provide the same information, they also unambiguously refer to the enclosing script/function.
brittle:
$MyInvocation.MyCommand is converted to a string due to being passed to the -Name parameter, which in a script results in the script's mere filename (e.g., script.ps1), and in a function in the function's name (e.g., Foo).
In a script, this will typically cause Get-Command not to find the script at all - unless that script happens to be in the PATH (one of the directories listed in $env:PATH). But that also means that a different script that happens to have the same filename and that happens to be / come first in the PATH may be matched, yielding incorrect results.
In short: Get-Command -Name $MyInvocation.MyCommand in scripts will often break, and when it does return a result, it may be for the wrong script.
In a function, it can identify the wrong command too, although that is much less likely:
Due to PowerShell's command precedence, a given name is first interpreted as an alias, and then as a function, so, in theory, with a Foo alias defined, Get-Command -Name $MyInvocation.MyCommand inside function Foo would mistakenly return information about the alias.
(It's nontrivial to invoke function Foo while alias Foo is defined, but it can be done; e.g.: & (Get-Item Function:Foo))
validateScript, can provide a more flexible solution and would work well if you needed additional parameter validation. This also allows you to get a list of the valid parameters outside of the foo function, with the creation of the get-validTypes function.
Function Foo {
Param (
[validateScript({test-validTypes $_})]
[String]$Type = 'Startup'
)
get-validTypes
}
function get-validTypes {
$powerOptions = #('Startup', 'Shutdown', 'LogOn', 'LogOff')
Write-Output $powerOptions
}
function test-validTypes {
[cmdletbinding()]
param ($typeInput)
$validTypes = get-validTypes
if ($validTypes.contains($typeInput)){
return $true
} else {
Write-Error "Invalid Type Paramater, Must be on of the following: $powerOptions"
}
}

Print all parameters passed to a cmdlet

Is there an easy way to get all paramters of a PowerShell cmdlet as a hashtable? I want the ability to dump all paramters via Write-Verbose (for debugging issues). What I want to do:
Function verb-noun {
Param($p1, $p2, $p2)
$parameters = ... # Get all parameters as hash
$parameters.Keys | % { Write-Verbose "$_=$parameters.Item($_)" }
...
}
You are looking for the automatic $PSBoundParameters variable which is a dictionary containing all bound parameter (parameters that you pass to the function)

Default value of parameter is not used in function

I have a very basic PowerShell script:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
This is how I execute it:
PS C:\> .\test.ps1 -MyWord 'hello'
hello
All fine. But I want to set a default value if -MyWord isn't specified.
I tried this:
Param(
[string]$MyWord='hi'
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output of my script was just empty. It was printing nothing when I did not describe my parameter. (it only showed 'hello' if I specified the parameter).
I also tried:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
[string]$MyWord='hi'
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output was of course always 'hi' and never 'hello'. Even when I executed the script with the parameter -MyWord 'hello'
Can someone explaining what I'm doing wrong?
When I'm not using the function it is working as expected:
Param(
[string]$MyWord='hi'
)
Write-Host $MyWord
Output:
PS C:\> .\test.ps1 -MyWord 'hallo'
hallo
PS C:\> .\test.ps1
hi
Automatic variable $PSBoundParameters, as the name suggests, contains only bound parameters, where bound means that an actual value was supplied by the caller.
Therefore, a parameter default value does not qualify as binding the associated parameter, so $MyWord with its default value of 'hi' does not become part of $PSBoundParameters.
Note: Arguably, a parameter with a default value should also be considered bound (it is bound by its default value, as opposed to by a caller-supplied value). Either way, it would be convenient to have an automatic variable that includes default values too, so as to enable simple and comprehensive passing through of arguments. A suggestion has been submitted to the PowerShell repository as GitHub issue #3285.
Workarounds
The following solutions assume that you want to pass the default value through, and don't want to simply duplicate the default value in function myfunc (as demonstrated in Ansgar Wiecher's helpful answer), because that creates a maintenance burden.
Regarding function syntax: The following two forms are equivalent (in this case), though you may prefer the latter for consistency and readability.[1]
function myfunc([string] $MyWord = 'hi') { ... }
parameter declaration inside (...) after the function name.
function myfunc { param([string] $MyWord = 'hi') ... }
parameter declaration inside a param(...) block inside the function body.
A simple fix would be to add the default value explicitly to $PSBoundParameters:
Param(
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord){
Write-Host "$MyWord"
}
# Add the $MyWord default value to PSBoundParameters.
# If $MyWord was actually bound, this is effectively a no-op.
$PSBoundParameters.MyWord = $MyWord
myfunc #PSBoundParameters
To achieve what you want generically, you must use reflection (introspection):
param(
[alias('foop')]
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord) {
Write-Host "$MyWord"
}
# Add all unbound parameters that have default values.
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
if (-not $PSBoundParameters.ContainsKey($paramName)) {
$defaultVal = Get-Variable -Scope Local $paramName -ValueOnly
# A default value is identified by either being non-$null or
# by being a [switch] parameter that defaults to $true (which is bad practice).
if (-not ($null -eq $defaultVal -or ($defaultVal -is [switch] -and -not $defaultVal))) {
$PSBoundParameters[$paramName] = $defaultVal
}
}
}
myfunc #PSBoundParameters
[1] The param(...) form is required if you need to use the [CmdletBinding()] attribute with non-default values, as well as in scripts (.ps1). See this answer.
A parameter is bound only if you actually pass it a value, meaning that a parameter's default value does not show up in $PSBoundParameters. If you want to pass script parameters into a function, you must replicate the script parameter set in the function parameter set:
Param(
[string]$MyWord = 'hi'
)
function myfunc([string]$MyWord = 'hi') {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
Maintaining something like this is easier if you define both parameter sets the same way, though, so I'd put the function parameter definition in a Param() block as well:
Param(
[string]$MyWord = 'hi'
)
function myfunc {
Param(
[string]$MyWord = 'hi'
)
Write-Host "$MyWord"
}
If you want to use "Param" enclose it in the function like this:
function myfunc {
Param(
[string]$MyWord='hi'
)
Write-Host "$MyWord"
}
Very simple way is,
function myfunc([string]$MyWord = "hi") {
Write-Output $MyWord
}

Powershell calling function with argument as string

I am trying to call Get-ChildItem function from custom function. The problem is the arguments to the function can be dynamic.
function Test {
Get-ChildItem $Args
}
When I try
Test .\ //this works as Path is taken as default argument value
Test .\ -Force //this doesn't work as expected as it still tries to consider entire thing as Path
Test -Path .\ -Force //same error
How to wrap around function and pass the arguments as it's?
$args is an array of arguments, and passing it to the Get-ChildItem wouldn't work, as you've noticed. The PowerShell-way for this would be the Proxy Command.
For a quick-and-dirty hack, you can use Invoke-Expression:
function Test {
Invoke-Expression "Get-ChildItem $Args"
}
Invoke-Expression will be difficult to work with because what's been passed as strings will need quoting all over again when expressed in a string. ProxyCommand is the better way as beatcracker has suggested.
There are a few alternatives for fun and interest. You might splat PSBoundParameters, but you will need to declare the parameters you expect to pass.
This is an incomplete example in that it will easily get upset if there are duplicate parameters (including common parameters if you set CmdletBinding on the function Test).
function Test {
dynamicparam {
$dynamicParams = New-Object Management.Automation.RuntimeDefinedParameterDictionary
foreach ($parameter in (Get-Command Microsoft.PowerShell.Management\Get-ChildItem).Parameters.Values) {
$runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter(
$parameter.Name,
$parameter.ParameterType,
$parameter.Attribtes
)
$dynamicParams.Add($parameter.Name, $runtimeParameter)
}
return $dynamicParams
}
end {
Get-ChildItem #psboundparameters
}
}