Why this code passing switch parameters to a PowerShell function fails - powershell

This question is about passing switch parameters. Let's see the code. I have this PowerShell 3.0 function:
#test1.ps1
param(
[switch] $param1 = $false
)
Write-Host "param1: $($param1.IsPresent)"
Write-Host
I have this main PowerShell function that invokes test.ps1 in four different ways:
#Test0.ps1
cls
$param1 = $True
# 1
.\test1.ps1 -param1
# 2
.\test1.ps1 -param1:$true
# 3
$potato = "-param1:`$$($param1)"
Write-Host "Parameter value: $potato"
.\test1.ps1 $potato
# 4
$command = ".\test1.ps1 -param1:`$$($param1)"
Write-Host "Command: $command"
iex $command
exit
Why is the 3rd way of doing it failing? I know I can do 4th way but I would love to understand why 3rd is failing.
Here is the output. As result all the parameters should be True but third one is False...
param1: True
param1: True
Parameter value: -param1:$True
param1: False
Command: .\test1.ps1 -param1:$True
param1: True

What happens is that:
The parser looks at the provided argument: "-param1:$true"
Fails to bind it to parameter param1, since the value you provided is a string, not a switch/bool
No specific parameter type is required at position 0, argument is ignored
If you make param1 positional, you can see how PowerShell fails to bind it properly:
function test-parambinding {param([Parameter(Position=0)][switch]$param1);$param1.IsPresent}
test-parambinding "-param1:`$true"
You'll see a ParameterArgumentTransformationException thrown before anything else happens

Mathias explains why your 3rd way of passing the parameter fails, but there is another way to pass parameters that lets you do roughly what you seem to be attempting here.
I've used a function here as it's a bit less to type when calling it, but your script file will work just the same:
PS C:\> function test1() {
param(
[switch] $param1 = $false
)
Write-Host "param1: $($param1.IsPresent)"
Write-Host
}
PS C:\> $param1 = $True
PS C:\> $potato = #{'param1'=$param1}
PS C:\> $potato
Name Value
---- -----
param1 True
PS C:\> test1 #potato
param1: True
So, instead of passing the argument and value as a single string, create a hash with the argument name as the key and the argument as the value. Then call the function or script using the # splatting operator. (See help about_Splatting for more detail).

Related

How to call a script with unknown parameters

I have a script that calls other scripts that other people manage. It's essentially a CI/CD script that gives users the ability to tap into the pipeline.
The issue I'm running into now is that I would like this calling script to implement a couple new parameters. However, the old scripts don't always implement those parameters.
If I call their script that doesn't implement the parameters, I get an error "A parameter cannot be found that matches parameter name 'newparameter'".
Is there a way to dynamically pass in a parameter so that it doesn't fail if the parameter doesn't exist? I don't mind if they don't implement it. It's a bonus parameter that they don't need to use.
Alternately, can I do something like a Get-Command for a custom .ps1 script, to get a list of accepted parameters? With that, I could confirm that a parameter is implemented before I pass it.
This might help you get started, you could use the Parser Class
to get all functions and it's parameters from a script, this answer shows a minimal reproduction. I'll leave it to you to investigate further.
Given myScript.ps1 that has these 3 functions:
function ExampleFunc {
param([int] $param1 = 123, [string] $param2)
}
function ExampleFunc2 {
param([object] $param3, [switch] $param4)
}
function ExampleFunc3 ($param5, [hashtable] $param6 = #{foo = 'var'}) {
}
You can use the ParseFile Method to get the AST, then you can use the .FindAll method to filter for all FunctionDefinitionAst and subsequently find all parameters filtering for all ParameterAst.
using namespace System.Management.Automation.Language
$ast = [Parser]::ParseFile('path\to\myScript.ps1', [ref] $null, [ref] $null)
$ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true) | ForEach-Object {
$out = [ordered]#{ Function = $_.Name }
$_.FindAll({ $args[0] -is [ParameterAst] }, $true) | ForEach-Object {
$out['ParameterName'] = $_.Name.VariablePath
$out['Type'] = $_.StaticType
$out['DefaultValue'] = $_.DefaultValue
[pscustomobject] $out
}
} | Format-Table
Above code would result in the following for myScript.ps1:
Function ParameterName Type DefaultValue
-------- ------------- ---- ------------
ExampleFunc param1 System.Int32 123
ExampleFunc param2 System.String
ExampleFunc2 param3 System.Object
ExampleFunc2 param4 System.Management.Automation.SwitchParameter
ExampleFunc3 param5 System.Object
ExampleFunc3 param6 System.Collections.Hashtable #{foo = 'var'}
The same could be accomplished using Get-Command:
(Get-Command 'fullpath\to\myScript.ps1').ScriptBlock.Ast.FindAll({
... same syntax as before ... }, $true # or $false for non-recursive search
)

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
}

How to pass a switch parameter to another PowerShell script?

I have two PowerShell scripts, which have switch parameters:
compile-tool1.ps1:
[CmdletBinding()]
param(
[switch]$VHDL2008
)
Write-Host "VHDL-2008 is enabled: $VHDL2008"
compile.ps1:
[CmdletBinding()]
param(
[switch]$VHDL2008
)
if (-not $VHDL2008)
{ compile-tool1.ps1 }
else
{ compile-tool1.ps1 -VHDL2008 }
How can I pass a switch parameter to another PowerShell script, without writing big if..then..else or case statements?
I don't want to convert the parameter $VHDL2008 of compile-tool1.ps1 to type bool, because, both scripts are front-end scripts (used by users). The latter one is a high-level wrapper for multiple compile-tool*.ps1 scripts.
You can specify $true or $false on a switch using the colon-syntax:
compile-tool1.ps1 -VHDL2008:$true
compile-tool1.ps1 -VHDL2008:$false
So just pass the actual value:
compile-tool1.ps1 -VHDL2008:$VHDL2008
Try
compile-tool1.ps1 -VHDL2008:$VHDL2008.IsPresent
Assuming you were iterating on development, it is highly likely that at some point you are going to add other switches and parameters to your main script that are going to be passed down to the next called script. Using the previous responses, you would have to go find each call and rewrite the line each time you add a parameter. In such case, you can avoid the overhead by doing the following,
.\compile-tool1.ps1 $($PSBoundParameters.GetEnumerator() | ForEach-Object {"-$($_.Key) $($_.Value)"})
The automatic variable $PSBoundParameters is a hashtable containing the parameters explicitly passed to the script.
Please note that script.ps1 -SomeSwitch is equivalent to script.ps1 -SomeSwitch $true and script.ps1 is equivalent to script.ps1 -SomeSwitch $false. Hence, including the switch set to false is equivalent to not including it.
According to a power shell team's blog (link below,) since V2 there is a technique called splatting. Basically, you use the automatic variable #PsBoundParameters to forward all the parameters. Details about splatting and the difference between # and $ are explained in the Microsoft Docs article (link below.)
Example:
parent.ps1
#Begin of parent.ps1
param(
[Switch] $MySwitch
)
Import-Module .\child.psm1
Call-Child #psBoundParameters
#End of parent.ps1
child.psm1
# Begin of child.psm1
function Call-Child {
param(
[switch] $MySwitch
)
if ($MySwitch){
Write-Output "`$MySwitch was specified"
} else {
Write-Output "`$MySwitch is missing"
}
}
#End of child.psm1
Now we can call the parent script with or without the switch
PS V:\sof\splatting> .\parent.ps1
$MySwitch is missing
PS V:\sof\splatting> .\parent.ps1 -MySwitch
$MySwitch was specified
PS V:\sof\splatting>
Update
In my original answer, I sourced the children instead of importing it as a module. It appears sourcing another script into the original just makes the parent's variables visible to all children so this will also work:
# Begin of child.ps1
function Call-Child {
if ($MySwitch){
Write-Output "`$MySwitch was specified"
} else {
Write-Output "`$MySwitch is missing"
}
}
#End of child.ps1
with
#Begin of parent.ps1
param(
[Switch] $MySwitch
)
. .\child.ps1
Call-Child # Not even specifying #psBoundParameters
#End of parent.ps1
Maybe, this is not the best way to make a program, nevertheless, this is the way it works.
About Splatting(Microsoft Docs)
How and Why to Use Splatting (passing [switch] parameters)
Another solution. If you declare your parameter with a default value of $false:
[switch] $VHDL2008 = $false
Then the following (the -VHDL2008 option with no value) will set $VHDL2008 to $true:
compile-tool1.ps1 -VHDL2008
If instead you omit the -VHDL2008 option, then this forces $VHDL2008 to use the default $false value:
compile-tool1.ps1
These examples are useful when calling a Powershell script from a bat script, as it is tricky to pass a $true/$false bool from bat to Powershell, because the bat will try to convert the bool to a string, resulting in the error:
Cannot process argument transformation on parameter 'VHDL2008'.
Cannot convert value "System.String" to type "System.Management.Automation.SwitchParameter".
Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.

When does Powershell honour default values when using $null splat parameters?

Consider the following function:
function f1{
param(
$sb = {},
$s = ''
)
if ($sb -isnot [scriptblock]) { 'scriptblock' }
if ($s -isnot [string] ) { 'string' }
}
Now invoke it with a splat parameter:
PS C:\> $splat = #{foo='bar'}
PS C:\> f1 #splat
As expected, nothing is returned. Now try it again with a $null splat parameter:
PS C:\> $splat = $null
PS C:\> f1 #splat
scriptblock
Oddly, scriptblock is returned. Clearly, at least for the [scriptblock] parameter, powershell is not honoring the default value when a $null splat parameter is used. But powershell does honor the default value for the [string]. What is going on here?
For what types does Powershell honour default values when using $null splat parameters?
Isn't this just normal application of positional parameters? You are splatting a single $null which is being applied to $sb.
Compare:
> function f{ param($sb = {}, $s = '') $PSBoundParameters }
> $splat = #(1,2)
> f #splat
Key Value
--- -----
sb 1
s 2
> f #flkejlkfja
Key Value
--- -----
sb
> function f{ param($aaa = 5, $sb = {}, $s = '') $PSBoundParameters }
> f #splat
Key Value
--- -----
aaa 1
sb 2
It's an old question but if it is still interesting...
As others have written with $splat = $null calling f1 #splat the first parameters will get the value $null instead it's default value.
If you want the parameters use their default value in this case you have to use $splat = #{} or $splat = #().
Here's a demonstration to help understand what's happening
$splat = #{foo='bar'}
"$(&{$args}#splat)"
-foo: bar
When you splat the hash table, it gets converted to -Key: Value string pairs that become the parameters to your function.
Now try:
$splat = $null
"$(&{$args}#splat)"
Nothing is returned. There are no keys to generate the parameter string from, so the end result is the same as not passing any parameters at all.
To complement Etan Reisner's helpful answer with a more direct demonstration that splatting $null indeed passes $null as the first (and only) positional argument:
$splat = $null
& { [CmdletBinding(PositionalBinding=$False)] param($dummy) } #splat
The above yields the following error:
A positional parameter cannot be found that accepts argument '$null'.
...
Decorating the param() block with [CmdletBinding(PositionalBinding=$False)] ensures that only named parameter values can be passed, causing the positional passing of $null from splatting to trigger the error above.
Note that using the special "null collection" value ([System.Management.Automation.Internal.AutomationNull]::Value) that you get from commands that produce no output for splatting is effectively the same as splatting $null, because that "null collection" value is converted to $null during parameter binding.
VargaJoe's helpful answer explains how to construct a variable for splatting so that no arguments are passed, so that the callee's default parameter values are honored.

Accepting an optional parameter only as named, not positional

I'm writing a PowerShell script that's a wrapper to an .exe. I want to have some optional script params, and pass the rest directly to the exe. Here's a test script:
param (
[Parameter(Mandatory=$False)] [string] $a = "DefaultA"
,[parameter(ValueFromRemainingArguments=$true)][string[]]$ExeParams # must be string[] - otherwise .exe invocation will quote
)
Write-Output ("a=" + ($a) + " ExeParams:") $ExeParams
If I run with the a named param, everything is great:
C:\ > powershell /command \temp\a.ps1 -a A This-should-go-to-exeparams This-also
a=A ExeParams:
This-should-go-to-exeparams
This-also
However, if I try to omit my param, the first unnamed param is assigned to it:
C:\ > powershell /command \temp\a.ps1 This-should-go-to-exeparams This-also
a=This-should-go-to-exeparams ExeParams:
This-also
I would expect:
a=DefaultA ExeParams:
This-should-go-to-exeparams
This-also
I tried adding Position=0 to the param, but that produces the same result.
Is there a way to achieve this?
Maybe a different parameter scheme?
By default, all function parameters are positional. Windows PowerShell assigns position numbers to parameters in the order in which the parameters are declared in the function. To disable this feature, set the value of the PositionalBinding argument of the CmdletBinding attribute to $False.
have a look at How to disable positional parameter binding in PowerShell
function Test-PositionalBinding
{
[CmdletBinding(PositionalBinding=$false)]
param(
$param1,$param2
)
Write-Host param1 is: $param1
Write-Host param2 is: $param2
}
The main answer still works in version 5 (according to comments, it may have been broken for a while in version 2).
There is another option: add Position to the ValueFromRemainingArgs parameter.
Sample CommandWrapper.ps1:
param(
$namedOptional = "default",
[Parameter(ValueFromRemainingArguments = $true, Position=1)]
$cmdArgs
)
write-host "namedOptional: $namedOptional"
& cmd /c echo cmdArgs: #cmdArgs
Sample output:
>commandwrapper hello world
namedOptional: default
cmdArgs: hello world
This appears to follow from PowerShell assigning parameter positions from the first parameter with a Position designated.