PowerShell bizarre variable process
Asked Answered
U

3

7

I've been banging my head, about this ...

Created this simple PowerShell script :

test.ps1:

       Write-Host    $args[0]
       Write-Host  $($args[0])
       Write-Host "$($args[0])"

Now, run the script twice, with these parameters :

      powershell .\test.ps1 0E2C
      powershell .\test.ps1 0E2D

The first run returns :

      0E2C
      0E2C
      0E2C

The second run returns :

      0E2D
      0
      0

Why is that ? Why is powershell converting 0E2D to 0 ? Is it considering that 0E2D is an hexadecimal value and trying to convert it somehow ? But why, and why doesn't it do the same with value 0E2C ?

EDIT :Thank you all.

I solved the issue by using a variable, and forcing it to string So, changing my TEST.PS1 :

Write-Host    $args[0]
Write-Host  $($args[0])
Write-Host "$($args[0])"
[string] $myvar = $args[0]
Write-Host "$myvar"

Now, when I run :

      powershell .\test.ps1 0E2D

I now get :

      0E2D
      0
      0
      0E2D
Unmentionable answered 6/10, 2022 at 17:39 Comment(1)
Your assumption is correct, as for 0E2C, PowerShell can't resolve it as a valid hex so, it will be passed as a string to Write-Host.Emigrate
A
2

tl;dr

  • An unquoted argument such as 0E2D is inherently ambiguous - it can be interpreted as either a string or a number. By default it is parsed as a number, but the original representation is cached - see next section.

  • To avoid ambiguity:

    • Either: quote an argument you want to be interpreted as a string, e.g. .\test.ps1 '0E2D'

      • This puts the responsibility on the caller to know when there's ambiguity.
    • Or, preferably: explicitly declare typed parameters for your script instead of relying on the automatic $args variable, such as param([string] $StringValue) or param([decimal] $Value)

      • This is preferable, because the caller is then free to pass the argument unquoted (unless it is a string and contains shell metacharacters), without having to worry about potential ambiguity.

  • The existing, helpful answers explain that 0E2D is interpreted as a number literal, namely a decimal value in scientific (exponential) notation, typed as a [decimal], due to suffix D (d would work too, as type-specifier suffixes are generally case-insensitive).

  • Let me complement them by explaining why $args[0] still showed the argument's original string representation, at least when used as-is.

In argument parsing mode, i.e when arguments are passed to command, simple string values (ones that contain neither spaces nor other shell metacharacters) need not be quoted.

This creates ambiguity, such as in the case at hand: is 0E2D meant to be a number, or meant to be a string ('0E2D')?

PowerShell's parameter (argument) parser handles this ambiguity as follows:

  • if the unquoted argument can be parsed as a number, it is.

  • if so, and if the default stringification of the number doesn't equal the argument as specified, the number is wrapped in a (mostly invisible) [psobject] instance that caches the original (string) representation, which can be recalled via .psobject.ToString()

  • if the unquoted argument binds to an explicitly declared typed parameter, the originally parsed form is ultimately irrelevant, but it does matter in unbound argument-passing, i.e. when arguments are passed positionally in the absence of predeclared parameters, via the automatic $args variable.

Here's an explicit illustration using 1L as a positional, unbound argument, which is parsed as [long] value 1, with the original representation, 1L, cached in the [psobject] wrapper:

PS> & { $args[0].ToString(), $args[0].psobject.ToString() } 1L
1   # default stringification of the [long] value that 1L was parsed as
1L  # original representation

The default display representation implicitly calls .psobject.ToString():

PS> & { $args[0] } 1L
1L  # original representation - even though it was parsed as [long]

This - commendably - also applies when passing the argument to external programs, as PowerShell should make no assumptions as to whether the argument represents a number or not - that is up the target program:

PS> cmd /c echo 1L
1L  # original representation

Unfortunately, however - as your question shows - both Windows PowerShell and PowerShell (Core) as of v7.2.6 - are inconsistent with respect to when they honor the cached string representation:

& { 
 $args[0]     # ditto for ($args[0])
 $($args[0])  # ditto for @($args[0])
 "$($args[0])"
} 1L

Arguably, all these commands should honor the original representation but only some of them do:

1L
1L
1

With 0E2D as the argument, as in your question, the two PowerShell editions even exhibit differences: $($args[0]) prints 0 in Windows PowerShell vs. O2ED in PowerShell (Core) 7.2.6.


Another inconsistency is that if you pass such an argument to -f, the format operator, it is only the cached string representation that is ever honored, not the numeric type that the argument was actually parsed as:

PS> & { '{0:N1}' -f $args[0] } 1.234e0 # Argument is parsed as a [double]
1.234e0  # !! Formatting the [double] as a number with 1 decimal place failed.

However, this behavior was declared to be by design - see GitHub issue #17199.

Alfredoalfresco answered 6/10, 2022 at 23:4 Comment(0)
F
3

Powershell interprets 0E2D as a Decimal 0E2 which is 0*10^2 which is 0
The d suffix is documented here:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_numeric_literals?view=powershell-7.2#real-literals

Fridge answered 6/10, 2022 at 18:9 Comment(0)
P
2

It is interpreting 02ED as a decimal number rather than a string due to the suffix d (see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_numeric_literals?view=powershell-7.2)

So it's 0 x 10^2 (0E2) which is 0.

But the C at the end of 0E2C is not a suffix literal, so it's just a string.

Psaltery answered 6/10, 2022 at 18:8 Comment(0)
A
2

tl;dr

  • An unquoted argument such as 0E2D is inherently ambiguous - it can be interpreted as either a string or a number. By default it is parsed as a number, but the original representation is cached - see next section.

  • To avoid ambiguity:

    • Either: quote an argument you want to be interpreted as a string, e.g. .\test.ps1 '0E2D'

      • This puts the responsibility on the caller to know when there's ambiguity.
    • Or, preferably: explicitly declare typed parameters for your script instead of relying on the automatic $args variable, such as param([string] $StringValue) or param([decimal] $Value)

      • This is preferable, because the caller is then free to pass the argument unquoted (unless it is a string and contains shell metacharacters), without having to worry about potential ambiguity.

  • The existing, helpful answers explain that 0E2D is interpreted as a number literal, namely a decimal value in scientific (exponential) notation, typed as a [decimal], due to suffix D (d would work too, as type-specifier suffixes are generally case-insensitive).

  • Let me complement them by explaining why $args[0] still showed the argument's original string representation, at least when used as-is.

In argument parsing mode, i.e when arguments are passed to command, simple string values (ones that contain neither spaces nor other shell metacharacters) need not be quoted.

This creates ambiguity, such as in the case at hand: is 0E2D meant to be a number, or meant to be a string ('0E2D')?

PowerShell's parameter (argument) parser handles this ambiguity as follows:

  • if the unquoted argument can be parsed as a number, it is.

  • if so, and if the default stringification of the number doesn't equal the argument as specified, the number is wrapped in a (mostly invisible) [psobject] instance that caches the original (string) representation, which can be recalled via .psobject.ToString()

  • if the unquoted argument binds to an explicitly declared typed parameter, the originally parsed form is ultimately irrelevant, but it does matter in unbound argument-passing, i.e. when arguments are passed positionally in the absence of predeclared parameters, via the automatic $args variable.

Here's an explicit illustration using 1L as a positional, unbound argument, which is parsed as [long] value 1, with the original representation, 1L, cached in the [psobject] wrapper:

PS> & { $args[0].ToString(), $args[0].psobject.ToString() } 1L
1   # default stringification of the [long] value that 1L was parsed as
1L  # original representation

The default display representation implicitly calls .psobject.ToString():

PS> & { $args[0] } 1L
1L  # original representation - even though it was parsed as [long]

This - commendably - also applies when passing the argument to external programs, as PowerShell should make no assumptions as to whether the argument represents a number or not - that is up the target program:

PS> cmd /c echo 1L
1L  # original representation

Unfortunately, however - as your question shows - both Windows PowerShell and PowerShell (Core) as of v7.2.6 - are inconsistent with respect to when they honor the cached string representation:

& { 
 $args[0]     # ditto for ($args[0])
 $($args[0])  # ditto for @($args[0])
 "$($args[0])"
} 1L

Arguably, all these commands should honor the original representation but only some of them do:

1L
1L
1

With 0E2D as the argument, as in your question, the two PowerShell editions even exhibit differences: $($args[0]) prints 0 in Windows PowerShell vs. O2ED in PowerShell (Core) 7.2.6.


Another inconsistency is that if you pass such an argument to -f, the format operator, it is only the cached string representation that is ever honored, not the numeric type that the argument was actually parsed as:

PS> & { '{0:N1}' -f $args[0] } 1.234e0 # Argument is parsed as a [double]
1.234e0  # !! Formatting the [double] as a number with 1 decimal place failed.

However, this behavior was declared to be by design - see GitHub issue #17199.

Alfredoalfresco answered 6/10, 2022 at 23:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.