Ciao all -
I'm using Powershell 7.2 to automate some hardware configuration through the hardware's CLI. I am using a loop to generate strings that include "0x" prefixes to express hex bytes, but having an issue where any consecutive iterations after the first pass of the loop do not print the "0x" prefix.
The following will produce the issue:
function fTest($id)
{
foreach($n in @(1, 2, 3))
{
write-host $id.gettype()
write-host ("{0:x}" -f $id)
$id
}
}
fTest 0x1a
Actual output:
System.Int32
0x1a
System.Int32
1b
System.Int32
1c
The 0x
prefixes are omitted in iters 2 and 3.
- Why is this happening?
- What is a clean way to correct the issue?
I'm a PowerShell noob, so I am happy to receive suggestions or examples of entirely different approaches.
Thanks in advance for the help!
CodePudding user response:
tl;dr
Type-constrain your
$p
parameter to unambiguously make it a number (integer), as Theo suggests:function fTest($id)
->function fTest([int] $id)
Build the
0x
prefix into the format string passed to-f
:"{0:x}" -f $id
->'0x{0:x}' -f $id
Building on the helpful comments:
Why is this happening?
Format string
{0:x}
, when applied to a number, only ever produces a hexadecimal representation without a0x
prefix; e.g.:PS> '{0:x}' -f 10 a # NOT '0xa'
If the operand is not a number, the numeric
:x
specification is ignored:PS> '{0:x}' -f 'foo' foo
The problem in your case is related to how PowerShell handles arguments passed to parameters that are not type-constrained:
Argument
0x1a
is ambiguous: it could be a number - expressed as hexadecimal constant0x1a
, equivalent to decimal26
- or a string.- While in expression-parsing mode this ambiguity would not arise (strings must be quoted there), it does in argument-parsing mode, where quoting around strings is optional (except if the string contains metacharacters) - see the conceptual about_Parsing topic.
What PowerShell does in this case is to create a hybrid argument value: The value is parsed as a number, but it caches its original string representation behind the scenes, which is used for display formatting, for instance:
PS> & { param($p) $p; $p.ToString() } 0x1a 0x1a # With default output formatting, the original string form is used. 26 # $p is an [int], so .ToString() yields its decimal representation
As of PowerShell 7.2.2, surprisingly and problematically, in the context of
-f
, the string-formatting operator, such a hybrid value is treated as a string, even though it self-reports as a number:PS> & { param($p) $p.GetType().FullName; '{0:N2}' -f $p } 0x1a System.Int32 # $p is of type [int] == System.Int32 0x1a # !! With -f $p is unexpectedly treated *as a string*, # !! yielding the cached original string representation.
- This unexpected behavior has been reported in GitHub issue #17199.
Type-constraining the parameter to which such a hybrid argument is passed, as shown at the top, avoids the ambiguity: on invocation, the argument is converted to an unwrapped instance of the parameter's type (see next point).
As for why the output changed starting with the 2nd iteration:
The cached string representation is implemented by way of an invisible
[psobject]
wrapper around the instance of the numeric type stored in$id
, in this case.When you update this value by way of an increment operation (
[psobject]
wrapper is lost, and the variable is updated with an unwrapped number (the original value 1).Therefore, starting with the 2nd iteration,
$id
contained an unwrapped[int]
instance, resulting in the{0:x}
number format being honored and therefore yielding a hexadecimal representation without a0x
prefix.The only reason the 1st iteration yielded a
0x
prefix was that it was present in the original string representation of the argument; as stated above, the numeric:x
format specifier was ignored in this case, given that the-f
operand was (unexpectedly) treated as a string.