I've run into some curious behaviour with a default argument that appears to change value (null
or an empty string) based on whether that function used a .
or &
to invoke a native command inside it.
Here is an example script with two identical functions, where the only difference is how they invoke a native command (cmd.exe
):
function Format-Type($value)
{
if ($value -eq $null) { '(null)' } else { $value.GetType().FullName }
}
function Use-Dot
{
param(
[string] $Arg = [System.Management.Automation.Language.NullString]::Value
)
Write-Host ".: $(Format-Type $Arg)"
. cmd.exe /c exit 0
}
function Use-Ampersand
{
param(
[string] $Arg = [System.Management.Automation.Language.NullString]::Value
)
Write-Host "&: $(Format-Type $Arg)"
& cmd.exe /c exit 0
}
Use-Dot
Use-Ampersand
On PowerShell 5.1 I get the following output which shows that the argument's value is different in the two cases:
.: (null)
&: System.String
It sounds ludicrous for this behaviour to be correlated in this way and therefore I'm sure I must be missing something obvious (or maybe very subtle) here, but what is that?
--
The question What is the .
shorthand for in a PowerShell pipeline? talks about the difference in scope between .
and &
but that doesn't mention why a default argument, which is not even referenced in the command invocation, might be affected by its use. In my example the caller appears to be affected before the command is even invoked.
Spent some digging into this and this is what I've observed.
First for clarity I do not believe that you should consider the NullString value the same as null in a basic comparison. Not sure why you need this either, as this is normally something I'd expect from c# development. You should be able to just use $null
for most work in PowerShell.
if($null -eq [System.Management.Automation.Language.NullString]::Value)
{
write-host "`$null -eq [System.Management.Automation.Language.NullString]::Value"
}
else
{
write-host "`$null -ne [System.Management.Automation.Language.NullString]::Value"
}
Secondly, the issue is not necessarily because of the call operator, ie &
. I believe instead you are dealing with underlying parameter binding coercion. Strong data typing is definitely a weak area for PowerShell, as even explicitly declared [int]$val
could end up being set to a string type by PowerShell automatically in the next line when writing Write-Host $Val
.
To identify the underlying behavior, I used the Trace-Command
function (Trace Command) .
I changed the Use-Dot to just call the function as no write-host was needed to output the string.
function Use-Ampersand
{
param(
[string]$NullString = [System.Management.Automation.Language.NullString]::Value
)
Format-Type $NullString
&cmd.exe /c exit 0
}
The Format-Type I modified to also use what is considered a better practice of $null
on the left, again due to type inference.
function Format-Type($v= [System.Management.Automation.Language.NullString]::Value)
{
if ($null -eq $v)
{
'(null)'
}
else {
$v.GetType().FullName
}
}
To narrow down the issue with the data types, I used the following commands, though this is not where I found insight into the issue. The both when called directly worked the same.
Trace-Command -Name TypeConversion -Expression { Format-Type $NullString} -PSHost
Trace-Command -Name TypeConversion -Expression { Format-Type ([System.Management.Automation.Language.NullString]$NullString) } -PSHost
However, when I ran the functions using TypeConversion tracing, it showed a difference in the conversions that likely explains some of your observed behavior.
Trace-Command -Name TypeConversion -Expression { Use-Dot} -PSHost
Trace-Command -Name TypeConversion -Expression { Use-Ampersand} -PSHost
# USE DOT
DEBUG: TypeConversion Information: 0 : Converting "" to "System.String".
DEBUG: TypeConversion Information: 0 : Converting object to string.
DEBUG: TypeConversion Information: 0 : Converting "" to "System.Object". <<<<<<<<<<<
DEBUG: TypeConversion Information: 0 : Converting ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW;.CPL" to "System.String".
DEBUG: TypeConversion Information: 0 : Result type is assignable from value to convert's type
OUTPUT: (null)
# Use-Ampersand
DEBUG: TypeConversion Information: 0 : Converting "" to "System.String".
DEBUG: TypeConversion Information: 0 : Converting object to string.
DEBUG: TypeConversion Information: 0 : Converting "" to "System.String". <<<<<<<<<<<
DEBUG: TypeConversion Information: 0 : Converting null to "". <<<<<<<<<<<
DEBUG: TypeConversion Information: 0 : Converting ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW;.CPL" to "System.String".
DEBUG: TypeConversion Information: 0 : Result type is assignable from value to convert's type
OUTPUT: System.String
The noticeable difference is in Use-Ampersand
it shows a statement of Converting null to ""
vs Converting "" to "System.Object"
. In PowerShell, $null <> [string]''
. An empty string comparison will pass the null check, resulting in the success of outputting GetType()
.
Why it's doing this, I'm not certain, but before you invest more time researching, let me provide one piece of advice based on learning the hard way.
If start dealing with issues due to trying to coerce data types in PowerShell, first consider if PowerShell is the right tool for the job
Yes, you can use type extensions. Yes, you can use .NET data types like $List = [System.Collections.Generic.List[string]]::new()
and some .NET typed rules can be enforced. However, PowerShell is not designed to be a strongly typed language like C#. Trying to approach it like this will result in a many difficulties. While I'm a huge fan of PowerShell, I've learned to recognize that it's flexibility should be appreciated, and it's limits respected.
If I really had issues that required mapping [System.Management.Automation.Language.NullString]::Value
so strongly, I'd consider my approach.
That said, this was a challenging investigation that I had to take a swing at, while providing my 10 cents afterwards.
After posting my answer, I found another answer that seemed relevant, and also backs up the mentioning of not using [NullString]
normally, as its usage in PowerShell is not really what it was designed for.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With