Function parameter validation in Powershell - powershell

Why does a [string] casted parameter with the 'value' $null in this example never throw an error (empty or $null), but a string with the value '$null' always throws? I would expect if passing a mandatory parameter, it is checked for $null/emptyness and thus an error is always thrown in these cases:
Function test_M_NoE ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $x ) {}
# test cases. Uncomment one:
[string]$x = [string]$null
# $x = [string]$null
# [string]$x = $null
# $x = $null
"1:"; test_M_NoE [string]$x # never error
"2:"; test_M_NoE $x # always error

The reason this works:
test_M_NoE [string]$x
Is that [string]$x is not being interpreted the way you expect.
Let's change your test function definition to help us better see what's actually going on:
function test_M_NoE {
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$x
)
Write-Host "Argument value passed was: '$x'"
}
Now, let's try again:
PS ~> $x = $null
PS ~> test_M_NoE [string]$x
Argument value passed was: '[string]'
Aha! The argument expression [string]$x did not result in an empty string - it resulted in the literal string value [string].
This is due to the fact that PowerShell attempts to parse command arguments differently from anything else. From the about_Parsing help topic:
Argument mode is designed for parsing arguments and parameters for commands in a shell environment. All input is treated as an expandable string unless it uses one of the following syntaxes: [...]
So really, PowerShell interprets our argument expression like a double-quoted string:
test_M_NoE "[string]$x"
At which point the behavior makes sense - $x is $null, so it evaluates to an empty string, and the result of the expression "[string]$x" is therefore just [string].
Enclose the argument expression in the $(...) subexpression operator to have it evaluated as a value expression instead of as an expandable string:
test_M_NoE $([string]$x)

Related

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.

Converting empty string to $null back to the same variable

We have a few imports via PowerShell in to Active Directory that have a couple fields that come across as an empty string from the datasource, but need to be set as $null within Active Directory.
Since there are quite a few of these fields, I attempted to create a function that will convert an empty string to $null.
The trouble is that if I set the variable back to itself, it remains an empty string. If I set it as a new variable, it works fine.
function Get-ValueOrNull
{
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
[string]$Value
)
if ([string]::IsNullOrEmpty($Value))
{
return $null
}
return [string]$Value
}
function Test-Function
{
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
[string]$TestValue
)
$TestValue = Get-ValueOrNull -Value $TestValue
$TestValue2 = Get-ValueOrNull -Value $TestValue
Write-Host "TestValue: $($TestValue -eq $null)"
Write-Host "TestValue2: $($TestValue2 -eq $null)"
}
Test-Function -TestValue ""
Here the output is
PS C:\> .\Test-Function.ps1
TestValue: False
TestValue2: True
This is clearly something I'm not understanding about Types in PowerShell function parameters. I can change the [string]$TestValue to $TestValue, and it will work.
function Test-Function
{
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
$TestValue
)
...
}
...
Output:
PS C:\> .\Test-Function.ps1
TestValue: True
TestValue2: True
The reason I'd like to preserve the [string] parameter type is to enforce that it should be a string or an empty string. Can someone explain what is going on here?
Once you've casted the variable as opposed to the value being assigned you are strictly typing that variable.
This is easier to see with an [int] because basically anything can be casted to a [string] successfully:
$v = [int]'5'
$v.GetType() # int
$v = 'hello'
$v.GetType() # string
[int]$v = '5'
$v.GetType() # int
$v = 'hello'
# Exception:
# Cannot convert value "hello" to type "System.Int32". Error: "Input string was not in a correct format."
When you type a parameter, the variable that contains the parameter is the same way; you can reassign it, but the right side must be assignable / castabale / convertible to the left side's type.
A $null cast as [string] is an empty string:
([string]$null) -eq ([string]::Empty) # True
You can still strongly type your parameter, if you use a different intermediate variable in the function that isn't, as you demonstrated with $TestValue2.

Can I specify conditional default values for a parameter in PowerShell?

I thought if this was possible it might work using parameter sets so I tried the following:
Function New-TestMultipleDefaultValues {
[CmdletBinding(DefaultParameterSetName="Default1")]
param (
[Parameter(Mandatory,ParameterSetName="Default1")]$SomeOtherThingThatIfSpecifiedShouldResultInTest1HavingValue1,
[Parameter(ParameterSetName="Default1")]$Test1 = "Value1",
[Parameter(ParameterSetName="Default2")]$Test1 = "Value2"
)
$PSBoundParameters
}
Executing this to create the function results in the error Duplicate parameter $test1 in parameter list. so it doesn't look like this way is an option.
The only thing I can think of at this point is to do something like this:
Function New-TestMultipleDefaultValues {
param (
$SomeOtherThingThatIfSpecifiedShouldResultInTest1HavingValue1,
$Test1
)
if (-not $Test1 -and $SomeOtherThingThatIfSpecifiedShouldResultInTest1HavingValue1) {
$Test1 = "Value1"
} elseif (-not $Test1 -and -not $SomeOtherThingThatIfSpecifiedShouldResultInTest1HavingValue1) {
$Test1 = "Value2"
}
$Test1
}
Which works but seems ugly:
PS C:\Users\user> New-TestMultipleDefaultValues -SomeOtherThingThatIfSpecifiedShouldResultInTest1HavingValue1 "thing"
Value1
PS C:\Users\user> New-TestMultipleDefaultValues
Value2
PS C:\Users\user> New-TestMultipleDefaultValues -Test1 "test"
test
Any better way to accomplish this?
The following should work:
Since there is then no longer a need for explicit parameter sets, I've omitted them; without specific properties, the [Parameter()] attributes aren't strictly needed anymore either.
Function New-TestMultipleDefaultValues {
[CmdletBinding()]
param (
[Parameter()] $SomeOtherThing,
[Parameter()] $Test1 =
('Value2', 'Value1')[$PSBoundParameters.ContainsKey('SomeOtherThing')]
)
# * As expected, if -Test1 <value> is explicitly specified,
# parameter variable $Test1 receives that value.
# * If -Test1 is omitted, the expression assigns 'Value1` to $Test1
# if -SomeOtherThing was specified, and 'Value2' otherwise.
$Test1 # Output the effective value of $Test1
}
It is possible to use expressions as parameter default values.
The above code is an expression and therefore can be used as-is.
To use a single command (a call to a PowerShell cmdlet, function, script or to an external program) as an expression, enclose it in (...), the grouping operator.
In all other cases you need $(...), the subexpression operator (or #(...), the array-subexpression operator) to convert the code to an expression; these cases are:
A Throw statement (and, hypothetically, exit and return statements, but you wouldn't use them in this context)
A compound construct such as foreach, while, ...
Multiple commands, expressions, or compound constructs, separated with ;
However, it is safe to always use $(...) (or #(...)) to enclose the code that calculates the default value, which you may opt to do for simplicity.
These expressions are evaluated after the explicitly specified parameters have been bound, which allows an expression to examine what parameters have been bound, via the automatic $PSBoundParameters variable:
('Value2', 'Value1')[$PSBoundParameters.ContainsKey('SomeOtherThing')] is simply a more concise reformulation of
if ($PSBoundParameters.ContainsKey('SomeOtherThing')) { 'Value1' } else { 'Value2' }
that takes advantage of [bool] values mapping onto 0 ($false) and 1 ($true) when used as an array index (integer).
In PowerShell v7+ you could use a ternary conditional instead, which has the added advantage of short-circuiting the evaluation:
$PSBoundParameters.ContainsKey('SomeOtherThing') ? 'Value1' : 'Value2'
You may want to look at dynamic parameters. You declare a section called dynamicparams {} and inside you can create parameters on the fly.

Beginner Problems with Equals Operator

everyone trying to learn Powershell off and on and I'm stuck on this problem. I cannot seem to find an equals operator that this code will accept at the = true portion .
Ive tried -eq, =, ==, and === .
Trying to get the Msg box to pop up if this Test-path command returns a true condition.
$wshell = New-Object -ComObject Wscript.Shell
If( Test-Path 'C:\wmw\~$test.xlsx' **= True)**
{
$wshell.Popup("Hey $Env:ComputerName This file is in use!",0,"test")}
else
{$wshell.Popup("Hey $Env:ComputerName This file is not in use!",0,"test")}
First of all, the literal for true is $true in PowerShell. And the operator for equality comparison is -eq. Then there is the issue that parameters to cmdlets start with - and you'd need to wrap the command in parentheses. Otherwise -eq would be interpreted as a (non-existent) parameter to Test-Path. So putting that all together:
If( (Test-Path 'C:\wmw\~$test.xlsx') -eq $True) { ... }
or, since if just needs a value that can be coerced to a boolean you don't even need the explicit comparison in most cases:
if (Test-Path 'C:\wmw\~$test.xlsx') { ... }
One hint for future exploration of the shell: Read the error messages. Most of the time they are helpful.
Omitting the parentheses and using -eq tells you about the fact that it's interpreted as a parameter:
Test-Path : A parameter cannot be found that matches parameter name 'eq'.
Same with = which is interpreted as a parameter value here:
Test-Path : A positional parameter cannot be found that accepts argument '='.
Using parentheses correctly and using -eq breaks the parser, admittedly:
You must provide a value expression following the '-eq' operator.
Unexpected token 'True' in expression or statement.
Missing closing ')' after expression in 'if' statement.
Unexpected token ')' in expression or statement.
Using parentheses and = is helpful again:
The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a variable or a property.

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.