Search code examples
powershellsyntaxparameter-passing

add-member weirdness with type assignment


In the code below, why does the [int] type assignment for name1 get ignored and is actually treated as part of the value? Bug?

$name1 = 4
$name2 = 4
$name3 = 4

$myObject = New-Object System.Object
$myObject | Add-Member -type NoteProperty -name name1 -Value [int]$name1
$myObject | Add-Member -type NoteProperty -name name2 -Value $($name2 -as [int])
$myObject | Add-Member -type NoteProperty -name name3 -Value $([int]$name3)

$myObject

Output:

name1  name2 name3
-----  ----- -----
[int]4     4     4

Powershell version:

get-host | select-object version

Version       
-------       
5.1.19041.1023

Solution

  • There's good information in the existing answers, but let me attempt a systematic overview:

    tl;dr

    In order to pass the output from an expression (e.g. [int] $name1 or
    $env:HOME + '\foo'), or (nested) command (e.g. Get-Date -Format yyyy) as an argument to a command (e.g. Add-Member), enclose it in (...), the grouping operator:

    # Note the required (...) around expression [int] $name1
    Add-Member -type NoteProperty -name name1 -Value ([int] $name1)
    

    By contrast, $(...), the subexpression operator is typically not needed in this scenario, and its use can have side effects - see this answer.

    • In short: (Outside of expandable strings, "..."), you only ever need $(...) to either enclose a language statement (such as an if statement or foreach loop) or multiple statements (any mix of commands, expressions, and language statements separated with ;).

    PowerShell's parsing modes:

    PowerShell has two fundamental parsing modes:

    • argument mode, which works like shells.

      • In argument mode, the first token is interpreted as a command name (the name of cmdlet, function, alias, or the name of path of an external executable or .ps1 script), followed by a whitespace-separated list of arguments, where strings may be unquoted[1] and arguments composed of a mix of literal parts and variable references are typically treated like expandable strings (as if they were enclosed in "...").
    • expression mode, which works like programming languages, where strings must be quoted, and operators and language statements such as assignments, foreach and while loops, casts can be used.

    The conceptual about_Parsing provides an introduction to these modes; in short, it is the first token in a given context that determines which mode is applied.

    A given statement may be composed of parts that are parsed in either mode, which is indeed what happens above:

    • Because your statement starts with a command name (Add-Member), it is parsed in argument mode.

    • (...) forces a new parsing context, which in the case at hand ([int] $name1) is parsed in expression mode, due to starting with [).

    What is considered a metacharacter (a character with special, syntactic meaning) differs between the parsing modes:

    • [ and = are special only in expression mode, not in argument mode, where they are used verbatim.

    • Conversely, a token-initial @ followed by a variable name is only special in argument mode, where it is used for parameter splatting.

    Compound argument [int]$name1 is therefore treated as if it were an expandable string, and results in verbatim string [int]4.

    Some expressions do not require enclosing in (...) when used as command arguments (assume $var = 'Foo'):

    • A stand-alone variable reference (e.g. Write-Output $var or Write-Output $env:OS)
    • Property access on such a reference (e.g. Write-Output $var.Length)
    • Method calls on such a reference (e.g. Write-Output $var.ToUpper())

    Note that these arguments are passed with their original data type, not stringified (although stringification may be performed by the receiving command).

    Pitfalls:

    • You sometimes need to use "..." explicitly in order to suppress property-access interpretation and have a . following a variable reference be interpreted verbatim (e.g. Write-Output "$var.txt" in order to get verbatim foo.txt).

    • If you use $(...) as part of a compound argument without explicit "..." quoting, that argument is broken into multiple arguments if the $(...) subexpression starts the argument (e.g., Write-Output $('a' + 'b')/c passes two arguments, verbatim ab and /c, whereas Write-Output c/$('a' + 'b') passed just one, verbatim c/ab).

    • Similarly, mixing quoted and unquoted strings to form a single argument only works if the argument starts with an unquoted token (e.g., Write-Output One"$var"'$Two' works as expected and yields verbatim OneFoo$Two, but Write-Output 'One'"$var"'$Two' is passed as three arguments, verbatim One, Foo, and $Two).

    In short:

    • The exact rules for how arguments are parsed are complex:

      • This answer summarizes the rules for unquoted arguments.
      • This answer summarizes mixing quoted and unquoted strings in a single argument
      • This answer) (bottom section) summarizes PowerShell's string literals in general.
    • To be safe, avoid use of $(...) outside "..." and avoid mixing quoting styles in a single string argument; either use a (single) "..." string (e.g. Write-Output "$(Split-Path $PROFILE)/foo.txt" or ) or string concatenation in an expression (Write-Output ('One' + $var + '$Two')


    [1] Assuming they contain neither spaces nor any of PowerShell's metacharacters (see this answer). While quoting typically takes the form of enclosing the entire argument in single or double-quotes, as appropriate (e.g. 'foo bar', "foo $var"), it is also possible to quote (escape) individual characters (e.g. foo` bar), using the backtick (`), PowerShell's escape character.