Inconsistent behavior in powershell with null parameters - powershell

I need to write a function in powershell that tells apart a 'parameter not being passed' from one passed with string empty (or any other string)
I wrote it like this:
function Set-X {
param(
[AllowNull()][string]$MyParam = [System.Management.Automation.Language.NullString]::Value
)
if ($null -ne $MyParam) { write-host 'oops' }
else { write-host 'ok' }
}
If I call Set-X without parameters from ISE, it works as I expect and prints 'ok'.
But if I do that from the normal console, it prints 'oops'.
What is going on? What is the proper way to do it?

Allowing the user to pass in a parameter argument value of $null does not change the fact that powershell will attempt to convert it to a [string].
Converting a $null value in powershell to a string results in an empty string:
$str = [string]$null
$null -eq $str # False
'' -eq $str # True
(same goes for $null -as [string] and "$null")
Remove the type constraint on the MyParam parameter if you not only want to allow $null but also accept $null as a parameter value:
function Set-X {
param(
[AllowNull()]$MyParam = [System.Management.Automation.Language.NullString]::Value
)
if ($null -ne $MyParam) { write-host 'oops' }
else { write-host 'ok' }
}

As Mathias and BenH have written, the culprit is casting $null to the [string] type, which results in an empty string:
[string]$null -eq '' #This is True
But for the sample code in Mathias answer to work correctly we also have to replace
[System.Management.Automation.Language.NullString]::Value
with $null
function Set-X {
param(
[AllowNull()]$MyParam = $null
)
if ($null -ne $MyParam) { write-host 'oops' }
else { write-host 'ok' }
}

Related

Powershell Write-Output strange behavior

I have a script below that behaves really strangely.
For the first case I try to return an empty list specifying -NoEnumarate flag. But for some reason, the function returns $null
For the second case I do precisely the same as for the first case with the only difference: rather than providing -InputObject as a positional parameter, I provide it explicitly.
Why in the first case I get $null and in the second case I get the expected result - empty List[string]. I'm really confused.
# case 1
function Foo {
$result = [System.Collections.Generic.List[string]]::new()
Write-Output -InputObject $result -NoEnumerate
}
$foo = Foo
if ($foo -is [System.Collections.Generic.List[string]]) {
Write-Host 'Foo'
}
if ($null -eq $foo) {
Write-Host '$foo is null. Whyyy?'
}
# case 2
function Bar {
$result = [System.Collections.Generic.List[string]]::new()
Write-Output $result -NoEnumerate
}
$bar = Bar
if ($bar -is [System.Collections.Generic.List[string]]) {
Write-Host '$bar is list[string]'
}

How to provide custom type conversion of a PowerShell parameter

I have a scenario where one of the parameters to my cmdlet is a tuple of a CSV file name and the name of a column header. I would like to properly import that as a tuple with type [System.Tuple[System.IO.FileInfo,String]]. The full code looks like this:
[Parameter(Mandatory=$false)]
[ValidateScript( {
if (-Not ($_.item1 | Test-Path) ) {
throw "File or folder does not exist"
}
if (-Not ($_.item1 | Test-Path -PathType Leaf) ) {
throw "The Path argument must be a file. Folder paths are not allowed."
}
return $true
})]
[System.Tuple[System.IO.FileInfo,String]]$SomePath
However, when you provide an argument like -SomePath book1.csv,'Some Column Header' you get:
Invoke-MyCmdlet.ps1: Cannot process argument transformation on parameter 'SomePath'. Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Tuple`2[System.IO.FileInfo,System.String]".
Microsoft provides some documentation on this subject, but if I'm honest I'm struggling to wrap my head around what is going on in that example. There's a lot of syntax and commands there that I'm not familiar with and don't understand their relevance to the type conversion process.
My question is: is there a way to tell PowerShell how to perform the type conversion correctly? If so, what is the syntax for that? Is it documented more clearly elsewhere and I've missed it?
You could implement and register your own PSTypeConverter (here using PowerShell classes):
class CustomTupleConverter : System.Management.Automation.PSTypeConverter
{
[bool]
CanConvertFrom([object]$value, [type]$targetType)
{
# We only convert TO [Tuple[FileInfo,string]]
if($targetType -ne [System.Tuple[System.IO.FileInfo,string]]){
return $false
}
# ... and only from 2-item arrays consisting of #([string],[string]) or #([FileInfo],[string])
if($value -is [array] -and $value.Length -eq 2){
if(($value[0] -is [string] -or $value[0] -is [System.IO.FileInfo]) -and $value[1] -is [string]){
return $true
}
}
return $false
}
[object]
ConvertFrom([object]$value, [type]$targetType, [IFormatProvider]$format, [bool]$ignoreCase)
{
# Resolve individual values in the input array
$fileInfo = if($value[0] -is [System.IO.FileInfo]){
$value[0]
}
else{
Get-Item -Path $value[0]
}
if($fileInfo -isnot [System.IO.FileInfo]){
throw "Path didn't resolve to a file."
}
$headerName = $value[1] -as [string]
# Create corresponding tuple and return
return [System.Tuple]::Create($fileInfo, $headerName)
}
[bool]
CanConvertTo([object]$value, [type]$targetType){
return $this.CanConvertFrom($value, $targetType)
}
[object]
ConvertTo([object]$value, [type]$targetType, [IFormatProvider]$format, [bool]$ignoreCase){
return $this.ConvertFrom($value, $targetType, $format, $ignoreCase)
}
}
Once defined, we'll need to register it as a possible type converter for [System.Tuple[System.IO.FileInfo,string]]:
$targetTypeName = [System.Tuple[System.IO.FileInfo,string]].FullName
$typeConverter = [CustomTupleConverter]
Update-TypeData -TypeName $targetTypeName -TypeConverter $typeConverter
Now we can bind 2 strings to a tuple:
function Test-TupleBinding
{
param(
[Parameter(Mandatory = $true)]
[System.Tuple[System.IO.FileInfo,string]] $PathAndColumnName
)
$PathAndColumnName
}
Coversion magic in action during parameter binding:
PS C:\> Test-TupleBinding -PathAndColumnName windows\system32\notepad.exe,ColumnNameGoesHere
Item1 Item2 Length
----- ----- ------
C:\windows\system32\notepad.exe ColumnNameGoesHere 2

How to check if a PowerShell switch parameter is absent or false

I am building a PowerShell function that builds a hash table. I am looking for a way I can use a switch parameter to either be specified as absent, true or false. How can I determine this?
I can resolve this by using a [boolean] parameter, but I didn't find this an elegant solution. Alternatively I could also use two switch parameters.
function Invoke-API {
param(
[switch]$AddHash
)
$requestparams = #{'header'='yes'}
if ($AddHash) {
$requestparams.Code = $true
}
How would I get it to display false when false is specified and nothing when the switch parameter isn't specified?
To check whether a parameter was either passed in by the caller or not, inspect the $PSBoundParameters automatic variable:
if($PSBoundParameters.ContainsKey('AddHash')) {
# switch parameter was explicitly passed by the caller
# grab its value
$requestparams.Code = $AddHash.IsPresent
}
else {
# parameter was absent from the invocation, don't add it to the request
}
If you have multiple switch parameters that you want to pass through, iterate over the entries in $PSBoundParameters and test the type of each value:
param(
[switch]$AddHash,
[switch]$AddOtherStuff,
[switch]$Yolo
)
$requestParams = #{ header = 'value' }
$PSBoundParameters.GetEnumerator() |ForEach-Object {
$value = $_.Value
if($value -is [switch]){
$value = $value.IsPresent
}
$requestParams[$_.Key] = $value
}
You can use PSBoundParameter to check
PS C:\ > function test-switch {
param (
[switch]$there = $true
)
if ($PSBoundParameters.ContainsKey('there')) {
if ($there) {
'was passed in'
} else {
'set to false'
}
} else {
'Not passed in'
}
}
If you have a parameter that can be $true, $false or unspecified, then you might not want the [Switch] parameter type because it can only be $true or $false ($false is the same as unspecified). As an alternative, you can use a nullable boolean parameter. Example:
function Test-Boolean {
param(
[Nullable[Boolean]] $Test
)
if ( $Test -ne $null ) {
if ( $Test ) {
"You specified -Test `$true"
}
else {
"You specified -Test `$false"
}
}
else {
"You did not specify -Test"
}
}
even simpler:
function test{
[CmdletBinding()]
param(
[Parameter(Mandatory=$False,Position=1)][switch]$set
)
write-host "commence normal operation"
if(-not $set){"switch is not set, i execute this"}
else{"switch is set"}
}
output
enter image description here

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

PowerShell string default parameter value does not work as expected

#Requires -Version 2.0
[CmdletBinding()]
Param(
[Parameter()] [string] $MyParam = $null
)
if($MyParam -eq $null) {
Write-Host 'works'
} else {
Write-Host 'does not work'
}
Outputs "does not work" => looks like strings are converted from null to empty string implicitly? Why? And how to test if a string is empty or really $null? This should be two different values!
Okay, found the answer # https://www.codykonior.com/2013/10/17/checking-for-null-in-powershell/
Assuming:
Param(
[string] $stringParam = $null
)
And the parameter was not specified (is using default value):
# will NOT work
if ($null -eq $stringParam)
{
}
# WILL work:
if ($stringParam -eq "" -and $stringParam -eq [String]::Empty)
{
}
Alternatively, you can specify a special null type:
Param(
[string] $stringParam = [System.Management.Automation.Language.NullString]::Value
)
In which case the $null -eq $stringParam will work as expected.
Weird!
You will need to use the AllowNull attribute if you want to allow $null for string parameters:
[CmdletBinding()]
Param (
[Parameter()]
[AllowNull()]
[string] $MyParam
)
And note that you should use $null on the left-hand side of the comparison:
if ($null -eq $MyParam)
if you want it to work predictably
seeing many equality comparisons with [String]::Empty, you could use the [String]::IsNullOrWhiteSpace or [String]::IsNullOrEmpty static methods, like the following:
param(
[string]$parameter = $null
)
# we know this is false
($null -eq $parameter)
[String]::IsNullOrWhiteSpace($parameter)
[String]::IsNullOrEmpty($parameter)
('' -eq $parameter)
("" -eq $parameter)
which yields:
PS C:\...> .\foo.ps1
False
True
True
True
True
Simply do not declare the param's type if you want a $null value to remain:
Param(
$stringParam
)
(None of the other solutions worked for me when declaring the type.)
So it seems a default value of $null for parameters of type [string] defaults to empty string, for whatever reason.
Option 1
if ($stringParam) { ... }
Option 2
if ($stringParam -eq "") { ... }
Option 3
if ($stringParam -eq [String]::Empty) { ... }