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

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.

Related

Looping Register-ArgumentCompleter produces incorrect parameter completions

I have a module with a hashtable of dynamically derived enum values, that I thought would be slick to incorporate into Register-ArgumentCompleter for tab completion.
The motivation here is that I can't directly set the module function's input parameters to autoconvert into the enum type (which would properly enable tab completion), because I wish to dynamically derive the enums to save users from manually managing the enum values, as well as due to limitations with the .NET implementation of enums -- I need to allow for strings with dashes or starting with numbers, and potentially null values, all of which enums sadly don't allow. My idea is to do a workaround by adding tab-completed parameter values via Register-ArgumentCompleter.
Problem: I build this workaround as a script that's loaded in the first position of the ScriptsToProcess member of the module manifest, whereupon I discovered that incorrect values are being set when I loop over the hashtable keys and run Register-ArgumentCompleter.
Sample code to reproduce:
function test {param($a, $b, $c, $d )}
$ht = #{
'1' = #('a', #('a1','a2'))
'2' = #('b', #('b1','b2'))
'3' = #('c', #('c1','c2'))
'4' = #('d', #('d1','d2'))
}
Foreach ($enum in $ht.Keys){
$paramName = $ht.$enum[0]
$paramValue = $ht.$enum[1]
write-host $paramName
write-host $paramValue
Register-ArgumentCompleter -CommandName test2 -ParameterName $paramName -ScriptBlock {$paramValue}
}
PS> test -a <tab>
b1 b2
This is PS 7.2.5. In Windows PowerShell 5.1.19041 I get c1 c2 as suggested values. You can see from the host writes that it's down to whichever key is parsed last in the ht loop.
I also tried $ht.["$enum"][0|1] to cast the key type explicitly to a string, to no avail. When I write-host in the loop, all the values seem correct.
Does this seem like an error from me or a bug?
By the time the loop completes, $enum will have a value of whatever the last key in its sort order is.
Use ScriptBlock.GetNewClosure() to close over the value of $ht and $enum by the time GetNewClosure() is called, making the scriptblock retain the original values of $ht and $enum:
function test {param($a, $b, $c, $d )}
$ht = #{
'1' = #('a', #('a1','a2'))
'2' = #('b', #('b1','b2'))
'3' = #('c', #('c1','c2'))
'4' = #('d', #('d1','d2'))
}
Foreach ($enum in $ht.Keys){
Register-ArgumentCompleter -CommandName test -ParameterName $ht.$enum[0] -ScriptBlock { $ht.$enum[1] }.GetNewClosure()
}
FWIW you can simplify the $ht table significantly:
$ht = #{
'a' = #('a1','a2')
'b' = #('b1','b2')
'c' = #('c1','c2')
'd' = #('d1','d2')
}
Foreach ($enum in $ht.Keys){
Register-ArgumentCompleter -CommandName test -ParameterName $enum -ScriptBlock { $ht[$enum] }.GetNewClosure()
}

How to add -confirm:$false in a powershell hashtable?

$cmdlet="Disable-RemoteMailBox"
$arguments = #{Identity=$identity;DomainController=$domaincontroller;Archive=""}
$command_args=""
$arguments.keys | ForEach-Object{
$message = '-{0} {1} ' -f $_, $arguments[$_]
$command_args+= $message
}
$result=& $cmdlet #arguments 2>&1
In the end this is executed:
Disable-RemoteMailBox -Identity abc#corp.com -DomainController dc.corp.local -Archive
but i need to add a confirm:$false
Disable-RemoteMailBox -Identity abc#corp.com -DomainController dc.corp.local -Archive -Confirm:$false
How to add this $false in the Hashtable?
Change the $arguments hashtable from:
$arguments = #{Identity=$identity;DomainController=$domaincontroller;Archive=""}
to
$arguments = #{Identity=$identity;DomainController=$domaincontroller;Archive="";Confirm=$false}
Adding to Mathias's concise answer
Excepting for the confirm functionality's integration with the $ConfirmPreference preference variable, the -Confirm common parameter can be looked at as a simple switch parameter. It is either present or not present. However, PowerShell's internal type conversion engine will evaluate a [Switch] more like a [Boolean] You can see this if you cast a [Bool] to a [Switch].
[Switch]$true or [Switch]$false will return IsPresent True/False respectively.
If you specify Confirm = $false in a splatting hash table, the type coercion (casting) that occurs during the parameter binding will handle it correctly. This is also true for any other switch parameter, even custom ones you define in your custom functions. This type conversion is also noticiable when you need to evaluate a switch parameter internal to a function.
If I specify a switch parameter named $Delete
Param( [Switch]$Delete )
Then internally I can execute logic like:
If( $Delete -eq $true ) {
# Delete the file or whatever...
}
Of course, you can shorten to:
If( $Delete ) {
# Delete the file or whatever...
}
However, you don't need a deep understanding of PowerShell's type conversion system to use Boolean or Switch parameters in splatting hash tables. It's documented in about_Splatting. The first few lines will explain hash table splatting of switch parameters.

Pass null datetime parameter in powershell

So I have a powershell script that takes region and datetime as input and internally in the script it calls a stored procedure.
The stored procedure takes two inputs - region and datetimestamp; the datetimestamp is always null. How do I go about parsing it?
function Reset {
param ([string]$region,[nullable[Datetime]]$statustimestamp)
$conn = New-Object System.Data.SqlClient.SqlConnection("Server=SQL,15010; Database='STG';User ID=SVC;Password=password;Integrated Security=FALSE")
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = "dbo.Reset'$region' ,'$statustimestamp'"
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
$dataset = New-Object System.Data.DataSet
[void]$adapter.Fill($dataset)
$dataset.tables[0]
$cmd.CommandText
$dataset.Tables[0] | Export-CSV M:\MyReport.csv -encoding UTF8 -NoTypeInformation
Write-Host 'New report M:\MyReport.csv has been successfully generated'
}
I execute it as
Rest -region IN -statustimestamp NULL
and I get the following error
Reset : Cannot process argument transformation on parameter 'statustimestamp'. Cannot convert value "NULL" to type "System.DateTime".
Error: The string was not recognized as a valid DateTime. There is an unknown word starting at index 0.
At line:1 char:59
+ Reset -region AU -statustimestamp NULL
+ ~~~~
+ CategoryInfo : InvalidData: (:) [Reset], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Reset
To complement dee-see's helpful answer:
Note: However your parameters are declared, $PSBoundParameters.ContainsKey('<parameter-name>') inside a script/function tells you whether an argument was explicitly passed to a given parameter (a default value doesn't count); e.g., with the invocation in your question (had it succeeded), $PSBoundParameters.ContainsKey('statustimestamp') would indicate $true.
If you want your parameter value to be $null by omission:
Declare your parameter simply as [Datetime] $statustimestamp and pass no argument to it on invocation; $statustimestamp will then implicitly be $null.
# Declare a parameter in a script block (which works like a function)
# and invoke the script block without an argument for that parameter:
PS> & { param([Datetime] $statustimestamp) $null -eq $statustimestamp }
True # $statustimestamp was $null
If you want to support explicitly passing $null as an argument:
This may be necessary if you declare a mandatory parameter, yet you want to allow $null as an explicit signal that a default value should be used.
Unfortunately, the specifics of the parameter declaration currently depend on whether the data type of the parameter is a reference type (such as [string] or [System.IO.FileInfo]) or a value type (such as [int] or [datetime]).
You can inspect a given type's .IsValueType property to learn whether it is a value type ($true) or a reference type ($false); e.g.: [datetime].IsValueType yields $true).
If the parameter type is a reference type, you can use the [AllowNull()] attribute:
PS> & {
param(
[AllowNull()]
[Parameter(Mandatory)]
[System.IO.FileInfo] $Foo # System.IO.FileInfo is a *reference type*
)
$null -eq $Foo
} -Foo $null
True # $Foo was $null
Unfortunately, the same technique doesn't work with value types such as [DateTime], so your parameter must indeed be typed as [Nullable[DateTime], as in your question:
PS> & {
param(
[Parameter(Mandatory)]
[AllowNull()] # Because the parameter is mandatory, this is *also* needed.
[Nullable[DateTime]] $Foo # System.DateTime is a *value type*
)
$null -eq $Foo
} -Foo $null
True # $Foo was $null
Note: These requirements - needing to pay attention to the difference between value types and reference types and needing to use a [Nullable[T]] type - are obscure and uncharacteristic for PowerShell.
Doing away with these requirements in favor of a unified approach (making it work for value types the way it already does for reference types) is the
subject of this proposal on GitHub.
Null in PowerShell is represented by $null and not NULL, that's why the error message is saying the string NULL cannot be converted to a (nullable) DateTime.
Rest -region IN -statustimestamp $null
You can also omit the -statustimestamp parameter altogether.

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
}

Pass an unspecified set of parameters into a function and thru to a cmdlet

Let's say I want to write a helper function that wraps Read-Host. This function will enhance Read-Host by changing the prompt color, calling Read-Host, then changing the color back (simple example for illustrative purposes - not actually trying to solve for this).
Since this is a wrapper around Read-Host, I don't want to repeat the all of the parameters of Read-Host (i.e. Prompt and AsSecureString) in the function header. Is there a way for a function to take an unspecified set of parameters and then pass those parameters directly into a cmdlet call within the function? I'm not sure if Powershell has such a facility.
for example...
function MyFunc( [string] $MyFuncParam1, [int] $MyFuncParam2 , Some Thing Here For Cmdlet Params that I want to pass to Cmdlet )
{
# ...Do some work...
Read-Host Passthru Parameters Here
# ...Do some work...
}
It sounds like you're interested in the 'ValueFromRemainingArguments' parameter attribute. To use it, you'll need to create an advanced function. See the about_Functions_Advanced and about_Functions_Advanced_Parameters help topics for more info.
When you use that attribute, any extra unbound parameters will be assigned to that parameter. I don't think they're usable as-is, though, so I made a little function that will parse them (see below). After parsing them, two variables are returned: one for any unnamed, positional parameters, and one for named parameters. Those two variables can then be splatted to the command you want to run. Here's the helper function that can parse the parameters:
function ParseExtraParameters {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$ParamHashTable = #{}
$UnnamedParams = #()
$CurrentParamName = $null
$ExtraParameters | ForEach-Object -Process {
if ($_ -match "^-") {
# Parameter names start with '-'
if ($CurrentParamName) {
# Have a param name w/o a value; assume it's a switch
# If a value had been found, $CurrentParamName would have
# been nulled out again
$ParamHashTable.$CurrentParamName = $true
}
$CurrentParamName = $_ -replace "^-|:$"
}
else {
# Parameter value
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName += $_
$CurrentParamName = $null
}
else {
$UnnamedParams += $_
}
}
} -End {
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName = $true
}
}
,$UnnamedParams
$ParamHashTable
}
You could use it like this:
PS C:\> ParseExtraParameters -NamedParam1 1,2,3 -switchparam -switchparam2:$false UnnamedParam1
UnnamedParam1
Name Value
---- -----
switchparam True
switchparam2 False
NamedParam1 {1, 2, 3}
Here are two functions that can use the helper function (one is your example):
function MyFunc {
[CmdletBinding()]
param(
[string] $MyFuncParam1,
[int] $MyFuncParam2,
[Parameter(Position=0, ValueFromRemainingArguments=$true)]
$ExtraParameters
)
# ...Do some work...
$UnnamedParams, $NamedParams = ParseExtraParameters #ExtraParameters
Read-Host #UnnamedParams #NamedParams
# ...Do some work...
}
function Invoke-Something {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string] $CommandName,
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$UnnamedParameters, $NamedParameters = ParseExtraParameters #ExtraParameters
&$CommandName #UnnamedParameters #NamedParameters
}
After importing all three functions, try these commands:
MyFunc -MyFuncParam1 Param1Here "PromptText" -assecure
Invoke-Something -CommandName Write-Host -Fore Green "Some text" -Back Red
One word: splatting.
Few more words: you can use combination of $PSBoundParameters and splatting to pass parameters from external command, to internal command (assuming names match). You would need to remove any parameter that you don't want to use though from $PSBoundParameters first:
$PSBoundParameters.Remove('MyFuncParam1')
$PSBoundParameters.Remove('MyFuncParam2')
Read-Host #PSBoundParameters
EDIT
Sample function body:
function Read-Data {
param (
[string]$First,
[string]$Second,
[string]$Prompt,
[switch]$AsSecureString
)
$PSBoundParameters.Remove('First') | Out-Null
$PSBoundParameters.Remove('Second') | Out-Null
$Result = Read-Host #PSBoundParameters
"First: $First Second: $Second Result: $Result"
}
Read-Data -First Test -Prompt This-is-my-prompt-for-read-host