I'm struggling to understand the outputs of the below function
function testApp
{
param(
[string] $appName,
[switch] $sw = $false,
[string[]] $test,
[string[]] $test2
)
Write-Host $appName - $sw - $test - $test2
}
testApp -appName "TestApp" -sw $true -test "one", "two" -test2 "three","four"
Output: TestApp - True - one two - three four
testApp -appName "TestApp" -sw $true -test "one", "two"
Output: TestApp - True - one two - True
The first output is as expected. But I cannot understand why the second output has "True" for the test2 array when I did not pass it. Can anyone help me in understanding the reason for the behavior? Thanks.
To summarize and complement the helpful comments on the question by Lee_Dailey, Matthew and mclayton:
[switch] parameters in PowerShell (aka flags in other shells):
switch parameters are meant to imply $true vs. $false by their presence in an invocation: e.g., passing -sw by itself signals $true, whereas omitting -sw signals $false.
It is possible to pass a Boolean value explicitly, for the purpose of passing a programmatically determined value; e.g.: -sw:$var
Note the required : following the switch name, which tells PowerShell that the Boolean value belongs to the switch parameter; without it, PowerShell thinks the value is a positional argument meant for a different parameter (see below).
Caveat: Commands may interpret -sw:$false differently from omitting -sw; a prominent example is is the use of common parameter -Confirm:$false to override the effective $ConfirmPreference value.
If you need to make this distinction in your own code, use $PSBoundParameters.ContainsKey('sw') -and -not $sw to detect the -sw:$false case.
Do not assign a default value to a switch parameter variable: while technically possible, the convention is that switches default to $false (which is a [switch] instance's default value anyway); that is, a [switch] parameter should always have opt-in logic.
A [switch] parameter variable effectively behaves like a Boolean value in most contexts:
That is, you can say if ($sw) { ... }, for instance.
If you need to access the wrapped Boolean value explicitly, access the .IsPresent property (note that the property name is somewhat confusing, because in a -sw:$false invocation the switch is still present, but its value, as reflected in .IsPresent, is $false).
An example of where .IsPresent is needed is the use of a Boolean as an implicit array index, notably to emulate a ternary conditional[1]: ('falseValue', 'trueValue')[$sw.IsPresent]; without the .IsPresent, the effective Boolean value wouldn't be recognized as such and wouldn't automatically be mapped to index 0 (from $false) or 1 (from $true).
Ultimately, your problem was that you thought $true was an argument for -sw, whereas it became a positional argument implicitly bound to the -test2 parameter.
[switch] parameters never need a value, so the next argument becomes a separate, positional argument - unless you explicitly indicate that the argument belongs to the switch by following the switch name with :, as shown above.[2]
Positional vs. named argument passing in PowerShell:
Terminology note: For conceptual clarity the term argument is used to refer to a value passed to a declared parameter. This avoids the ambiguity of using parameter situationally to either refer to the language construct that receives a value vs. a given value.
Named argument passing (binding) refers to explicitly placing the target parameter name before the argument (typically separated by a space, but alternatively also and / or by :); e.g., -AppName foo.
The order in which named arguments are passed never matters.
Positional (unnamed) argument passing refers to passing an argument without preceding it by the name of its target parameter; e.g., foo.
The passing is positional in the sense that the relative position (order) among other unnamed arguments determines what target parameter is implied.
[switch] parameters are the exception in that they:
are typically passed by name only (-sw), implying value $true, and if a value is passed, require : to separate the name from the value.
never support positional binding.
You may combine named passing with positional passing, in which case the named arguments are bound first, after which the positional ones are then considered (in order) for binding to the not-yet-bound parameters.
PowerShell functions are simple functions by default. In order to exercise control over positional binding, use of the [CmdletBinding()] and / or [Parameter()] attributes is necessary (see below), which invariably turn a simple function into an advanced function.
Making a simple function an advanced one has larger behavioral implications (mostly beneficial ones), which are detailed in this answer.
By default, PowerShell functions accept positional arguments for any parameter (other than those of type [switch]), in the order in which the parameters were declared.
Additionally, simple functions accept arbitrary additional arguments for which no parameters were declared, which are collected in the automatic $args array variable.
To prevent your function from accepting any positional arguments by default, place a [CmdletBinding(PositionalBinding=$false, ...)] attribute above the param(...) block.
Since this makes your function an advanced one, this also disables passing arbitrary additional arguments ($args no longer applies and isn't populated).
As an aside: when you implement a cmdlet (a command implemented as a binary, typically via C#), this behavior is implied.
To selectively support positional arguments, decorate individual parameter declarations with a [Parameter(Position=<n>, ...)] attribute (e.g, [Parameter(Position=0)] [string] $Path)
Note: Whether you start your numbering with 0 or 1 doesn't matter, as long as the numbers used reflect the desired ordering among all positional parameters; 0 is advisable as a self-documenting convention, because it is unambiguous.
Attribute [Parameter(Position=<n>)] is an explicit opt-in that selectively overrides [CmdletBinding(PositionalBinding=$false)]: that is, the latter disables positional binding unless explicitly indicated by individual parameter declarations; in fact, the latter is implied by the former, in that once you use one [Parameter(Position=<n>)] attribute, you must use it on all other parameters you want to bind positionally as well.
[1] Note that PowerShell [Core] 7.0+ supports ternary conditionals natively: $sw ? 'trueValue' : 'falseValue'
[2] In effect, [switch] parameters are the only type for which PowerShell supports an optional argument. See this answer for more information.
Related
I have a PS script I call from a Windows shortcut. I drop on it several files or directories, and it works fine.
I would like to add some named parameters (let's call them : -Param1 and -Param2), optional, that can be used, of course, only from PowerShell Prompt.
param (
[switch]$CreateShortcut
)
A switch parameter works.
But, if I add a string parameter :
param (
[switch]$CreateShortcut,
[string]$Param1
)
Of course, it does not work anymore when I call my script thru the Windows shortcut : $Param1 receive the first file.
Is there a solution ?
Thanks
When you drop files/folders on a shortcut file, their full paths are passed as individual, unnamed arguments to the shortcut's executable (script).
PowerShell allows you to collect such unnamed arguments in a single, array-valued parameter, by declaring it as ValueFromRemainingArguments:
[CmdletBinding(PositionalBinding=$false)]
param (
[switch] $CreateShortcut,
# Collect all unnamed arguments in this parameter:
[Parameter(ValueFromRemainingArguments)]
[string[]] $FilesOrFolders
)
[CmdletBinding(PositionalBinding=$false)] ensures that any parameters not explicitly marked with a Position property must be passed as named arguments (i.e., the argument must be preceded by the name of the target parameter, e.g. -Path foo).
This isn't necessary to support [switch] parameters, because they are implicitly named-only, but it allows you to support additional, non-[switch] parameters that can be bound by explicit invocation (only).
Alternatively, if you do not need support for additional predeclared non-switch parameters, you can omit [CmdletBinding(PositionalBinding=$false)] and the $FilesOrFolders parameter declaration and access any arguments that do not bind to predeclared parameters via the automatic $args variable.
Generally, note that use of a [Parameter()] attribute on any of the predeclared parameters would make $args unavailable, as the presence of [CmdletBinding()] does.
The reason is that the use of either attribute makes a script or function an advanced one, i.e., makes it cmdlet-like, and therefore disallows passing arguments that do not bind to declared parameters; to put it differently: $args is then by definition always empty (an empty array).
Advanced scripts or functions automatically gain additional features, notably support for common parameters such as -Verbose.
Can anyone tell me why this command works fine in the Powershell console, returning a single thumbprint, but when run as a script it just returns all the certificate's thumbprints:
$crt = (Get-ChildItem -Path Cert:\LocalMachine\WebHosting\ | Where-Object {$_.Subject.Contains($certcn)}).thumbprint
$certcn is a string containing a domain. eg "www.test.com"
I figured it out. $certcn was derived from $args[0]. It turns out $args[0] is not a string, and even though PS would quite happily use it as a string in other commands, it would not do this with Where-Object.
Not sure what type $args[0] actually is, but doing $certcn = $args[0].tostring() fixed it.
The only explanation for your symptom is that the value of variable $certcn:
either: is the empty string ('') because 'someString'.Contains('') returns $true for any input string.
or: is implicitly converted to the empty string, though that wouldn't happen often in practice; here are some examples (see GitHub issue # for the [pscustomobject] stringification bug mentioned below):
# An empty array stringifies to ''
'someString'.Contains(#()) # -> $true
# A single-element array containing $null stringifies to ''
'someString'.Contains(#($null)) # -> $true
# Due to a longstanding bug, [pscustomobject] instances, when
# stringified via .ToString(), convert to the empty string.
# This makes the command equivalent to `.Contains(#(''))`, which is again
# the same as `.Contains('')`
'someString'.Contains(#([pscustomobject] #{ foo=1 })) # -> $true
$args[0] is not a string
The automatic $args variable is an array that contains all positional arguments that weren't bound to declared parameters, if any.
$args can contain elements of any data type, and what that type is is solely determined by the caller.
However, if you formally declare a parameter, you can type it, which means that if the caller passes an argument of a different data type, an attempt is made to convert the argument to the parameter's type (which may fail, but at least the failure will be "loud", and the reason obvious).
A robust solution for your script:
param(
[Parameter(Mandatory)] # Ensure that the caller passes a value.
[string] $CertCN # Type-constrain to a string.
# , ... declare other parameters as needed
)
# $CertCN is now guaranteed to be a *string* that is *non-empty*.
$crt =
(Get-ChildItem -Path Cert:\LocalMachine\WebHosting |
Where-Object { $_.Subject.Contains($CertCN) }).thumbprint
Note:
The use of the [Parameter()] attribute in the parameter declaration block (param(...)) makes your script an advanced one, which means that $args isn't supported, requiring all arguments to bind to explicitly declared parameters; however, you can define a catch-all parameter with [Parameter(ValueFromRemaningArguments)], if needed. (The other thing that makes a script or function an advanced one is use of the [CmdletBinding()] attribute above the param(...) block as a whole.)
[Parameter(Mandatory)], in addition to ensuring that the caller passes a value for the parameter, implicitly also prevents passing the empty string (or $null) - though you could explicitly allow that with [AllowEmptyString()]
Additionally, advanced scripts and functions automatically prevent passing arrays to [string]-typed parameters, which is desirable. (By contrast, simple functions and scripts simply stringify arrays, as would happen in expandable strings (string interpolation); e.g., & { param([string] $foo) $foo } 1, 2 binds '1 2', which is also what you'd get with "$(1, 2)")
Caveat:
When passing a value to a [string]-typed parameter, PowerShell accepts a scalar (non-collection) of any type, and any non-string type is automatically converted to a string, via .ToString(). This is usually desirable and convenient, but can result in useless stringifications; e.g.:
& { param([string] $str) $str } #{} # -> 'System.Collections.Hashtable'
Instances of hashtables (#{ ... }) stringify to their type name, which is unhelpful, and this behavior is the default behavior for any type that doesn't explicitly implement a meaningful string representation by overriding the .ToString() method.
If that is a concern, you can modify your script to ensure that the argument value being passed already is a string, using a [ValidateScript()] attribute.
param(
[Parameter(Mandatory)]
# Ensure that the argument value is a [string] to begin with.
# Note: The `ErrorMessage` property requires PowerShell (Core) 7+
[ValidateScript({ $_ -is [string] }, ErrorMessage='Please pass a *string* argument.')]
$CertCN # Do not type-constrain, so that the original type can be inspected.
# , ... declare other parameters as needed
)
# ...
As stated in the code comments, use of the ErrorMessage property in the requires PowerShell (Core) 7+, unfortunately. In Windows PowerShell a standard error message is invariably shown, which isn't user-friendly at all.
I'm writing a function that wraps a cmdlet using ValueFromRemainingArguments (as discussed here).
The following simple code demonstrates the problem:
works
function Test-WrapperArgs {
Set-Location #args
}
Test-WrapperArgs -Path C:\
does not work
function Test-WrapperUnbound {
Param(
[Parameter(ValueFromRemainingArguments)] $UnboundArgs
)
Set-Location #UnboundArgs
}
Test-WrapperUnbound -Path C:\
Set-Location: F:\cygwin\home\thorsten\.config\powershell\test.ps1:69
Line |
69 | Set-Location #UnboundArgs
| ~~~~~~~~~~~~~~~~~~~~~~~~~
| A positional parameter cannot be found that accepts argument 'C:\'.
I tried getting to the issue with GetType and EchoArgs from the PowerShell Community Extensions to no avail. At the moment I'm almost considering a bug (maybe related to this ticket??).
The best solution for an advanced function (one that uses a [CmdletBinding()] attribute and/or a [Parameter()] attribute) is to scaffold a proxy (wrapper) function via the PowerShell SDK, as shown in this answer.
This involves essentially duplicating the target command's parameter declarations (albeit in an automatic, but static fashion).
If you do not want to use this approach, your only option is to perform your own parsing of the $UnboundArgs array (technically, it is an instance of [System.Collections.Generic.List[object]]), which is cumbersome, however, and not foolproof:
function Test-WrapperUnbound {
Param(
[Parameter(ValueFromRemainingArguments)] $UnboundArgs
)
# (Incompletely) emulate PowerShell's own argument parsing by
# building a hashtable of parameter-argument pairs to pass through
# to Set-Location via splatting.
$htPassThruArgs = #{}; $key = $null
switch -regex ($UnboundArgs) {
'^-(.+)' { if ($key) { $htPassThruArgs[$key] = $true } $key = $Matches[1] }
default { $htPassThruArgs[$key] = $_; $key = $null }
}
if ($key) { $htPassThruArgs[$key] = $true } # trailing switch param.
# Pass the resulting hashtable via splatting.
Set-Location #htPassThruArgs
}
Note:
This isn't foolproof in that your function won't be able to distinguish between an actual parameter name (e.g., -Path) and a string literal that happens to look like a parameter name (e.g., '-Path')
Also, unlike with the scaffolding-based proxy-function approach mentioned at the top, you won't get tab-completion for any pass-through parameters and the pass-through parameters won't be listed with -? / Get-Help / Get-Command -Syntax.
If you don't mind having neither tab-completion nor syntax help and/or your wrapper function must support pass-through to multiple or not-known-in-advance target commands, using a simple (non-advanced) function with #args (as in your working example; see also below) is the simplest option, assuming your function doesn't itself need to support common parameters (which requires an advanced function).
Using a simple function also implies that common parameters are passed through to the wrapped command only (whereas an advanced function would interpret them as meant for itself, though their effect usually propagates to calls inside the function; with a common parameter such as -OutVariable, however, the distinction matters).
As for what you tried:
While PowerShell does support splatting via arrays (or array-like collections such as [System.Collections.Generic.List[object]]) in principle, this only works as intended if all elements are to be passed as positional arguments and/or if the target command is an external program (about whose parameter structure PowerShell knows nothing, and always passes arguments as a list/array of tokens).
In order to pass arguments with named parameters to other PowerShell commands, you must use hashtable-based splatting, where each entry's key identifies the target parameter and the value the parameter value (argument).
Even though the automatic $args variable is technically also an array ([object[]]), PowerShell has built-in magic that allows splatting with #args to also work with named parameters - this does not work with any custom array or collection.
Note that the automatic $args variable, which collects all arguments for which no parameter was declared - is only available in simple (non-advanced) functions and scripts; advanced functions and scripts - those that use the [CmdletBinding()] attribute and/or [Parameter()] attributes - require that all potential parameters be declared.
I am attempting to conditionally add parameters to a command I am invoking in powershell. However, when I try, my parameter gets passed as a string. I can’t figure out how to pass it as an identifier instead.
This is my attempt so far:
$readParams = $(if ("2".Equals("2")) {"-AsSecureString"})
read-host 'Prompt' $readParams
The output I get is:
Prompt -AsSecureString:
I want to be able to set $readParams differently depending on conditions. If the condition is true get this behavior:
read-host 'Prompt' -AsSecureString
and if the condition is false get this behavior:
read-host 'Prompt'
I only want to write read-host once in my program.
How do I specify the argument dynamically without passing it as a string which causes it to become part of the prompt instead of passed as an identifier-style parameter?
The feature you're looking for is called splatting. It allows you to present a hashtable to a command and have it interpreted as parameters.
$readParams=#{}
if("2".Equals("2")) {
$readParams["AsSecureString"]=$true
}
read-host 'Prompt' #readParams
If you want to prepare multiple parameters in advance, use the splatting technique described in Mike Shepard's helpful answer, which uses a hashtable whose keys are named for the target cmdlet's parameters and also works with switch parameters (as an alternative to conditionally omitting an entry named for the switch parameter, include it unconditionally and assign $True to emulate passing the switch, and $False to emulate omitting it[1]).
To expand on Jacob Colvin's helpful answer:
For a switch parameter such as -AsSecureString (an optional Boolean parameter without argument), the challenge is that a switch's value is normally implied by its presence ($True) vs. its absence ($False)[2].
However, there is a syntax that allows passing the value explicitly, namely by appending :<expr> directly to the switch name, where <expr> is an expression whose value is converted to a Boolean; to illustrate with the automatic $True and $False variables:
-AsSecureString:$True ... same as just -AsSecureString
-AsSecureString:$False ... usually the same as not passing -AsSecureString[1]
Therefore, you can solve your problem as follows, using an expression (enclosed in (...)) directly, with no need for aux. variable $readParams:
Read-Host 'Prompt' -AsSecureString:('2' -eq '2')
* Obviously, the expression value shouldn't be invariant in real-life use.
* '2' -eq '2' is PowerShell's equivalent of "2".Equals("2"), though note that PowerShell's -eq is case-insensitive by default, so, strictly speaking, -ceq would be the closer analog.
[1] On occasion, -SomeSwitch:$False has different semantics from omitting the switch altogether, notably when overriding preference variables ad hoc. For instance, if $VerbosePreference = 'Continue' is in effect to make all cmdlets produce verbose output by default, you can use -Verbose:$False on individual commands to suppress it.
[2] Strictly speaking, a parameter variable inside a cmdlet/advanced function that represents a switch parameter is of type [switch](System.Management.Automation.SwitchParameter); however, instances of this type effectively behave like Booleans.
Somewhat confusingly, and against what the documentation says, the type's .IsPresent property reflects the effective Boolean value of the switch (on or off, loosely speaking), and not whether the switch was explicitly passed by the user.
You could set the switch to true or false depending on the condition:
$readParams = $(if ("2".Equals("2")) {$true} else {$false})
read-host 'Prompt' -AsSecureString:$readParams
I am trying to call a PS script via batch file, like so
Powershell.exe -file "C:\Scripts\Blah\Blah\Blah.ps1" -webUID "usernameValue" -webPWD "passwordValue" -Param "param value" -Param2 "param 2 value"
The issue seems to be the batch file is confusing Param and Param2. It thinks I am setting Param2 twice however Param and Param2 are separate parameters altogether. Has anyone experienced this? Is there perhaps a way to explicitly state the param names? Thanks
Param block
# Parameters
Param
(
[string]$WebUID,
[string]$WebPWD,
[string]$Param,
[string]$Param2
)
In an effort to support concise command-line use, PowerShell's "elastic syntax" allows specifying unambiguous prefix substrings of parameter names so that you only need to type as much of a parameter name as is necessary to identify it without ambiguity;
e.g., typing -p to refer to -Path is enough, if no other parameters start with p.
However, an exact match is always recognized, so that specifying -Param in your case should unambiguously match the -Param parameter, even though its full name happens to be a prefix substring of different parameter -Param2.
If the problem were an issue of ambiguity (it isn't), you'd see a different error message. For instance, were you to use the ambiguous -Para, you'd see:
Parameter cannot be processed because the parameter name 'para' is ambiguous. Possible matches include: -Param -Param2.
Instead, the wording of your error message suggests that the exact same parameter name - -Param2 - was indeed specified more than once - even though your sample code doesn't show that.
I've tested the behavior in PSv2 and PSv5.1 / 6.0 alpha 10 - it's conceivable, however, that other versions act differently due to a bug. Do let us know.
Consider an alternative approach:
If you invoked your script from within PowerShell, you could use a single, array-valued parameter - e.g. [string[]] $Params - and then simply pass as many parameters as needed, comma-separated, without needing to specify a distinct parameter name for each value.
Sadly, when invoking a script from outside of PowerShell, this approach won't work, because passing arrays isn't supported from the outside.
There is a workaround, however:
Declare the array-valued parameter decorated with [parameter(ValueFromRemainingArguments=$true)]
Invoke the script with the parameters as a space-separated list at the end of the command.
Applied to your scenario:
If your script defined its parameters as follows:
Param
(
[string]$WebUID,
[string]$WebPWD,
[parameter(ValueFromRemainingArguments=$true)]
[string[]] $Params
)
You could then invoke your script as follows:
Powershell.exe -file "C:\Scripts\Blah\Blah\Blah.ps1" `
-webUID "usernameValue" `
-webPWD "passwordValue" `
"param value" "param 2 value"
and $Params would receive an array of values: $Params[0] would receive param value, and $Params[1] would receive param 2 value.
Note that when calling from outside of PowerShell:
you must not use parameter name -Params in the invocation - just specify the values at the end.
you must not use , to separate the values - use spaces.
I'm no guru, but this looks like it's related to "Partial Parameters" and "Parameter Completion". See this article for more information.
Simply changing Param to Param1 should fix the issue.