how do I pass a range of values on the command line - passing an expression as an argument
Asked Answered
J

1

7

I have the following code:

$srv_range = 29..30+40+50..52
$srv_range.GetType()
$NewVMTemplate = New-Object psobject
$NewVMTemplate | Add-Member -MemberType NoteProperty -Name Name -Value $null

$srv_range | % {
    $pod= $_
    $servers = @()
    1..2 | % {
        $server = $NewVMTemplate | Select-Object *
        $server.Name = "pod" + "{0:D2}" -f $pod + "-srv" + $_
        $servers += $server
    }
    ForEach ( $server in $servers) {
        write-host $server.Name
    }
} 

output:

PowerCLI C:\ .\eraseme.ps1

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
pod29-srv1
pod29-srv2
pod30-srv1
pod30-srv2
pod40-srv1
pod40-srv2
pod50-srv1
pod50-srv2
pod51-srv1
pod51-srv2
pod52-srv1
pod52-srv2

I want to input the range from CLI, but I get the following output with this code

param(

    [Parameter(Mandatory=$False)] $srv_range

)
#$srv_range = 29..30+40+50..52
$srv_range.GetType()
$NewVMTemplate = New-Object psobject
$NewVMTemplate | Add-Member -MemberType NoteProperty -Name Name -Value $null

$srv_range | % {
    $pod= $_
    $servers = @()
    1..2 | % {
        $server = $NewVMTemplate | Select-Object *
        $server.Name = "pod" + "{0:D2}" -f $pod + "-srv" + $_
        $servers += $server
    }
    ForEach ( $server in $servers) {
        write-host $server.Name
    }
} 

PowerCLI C:\ .\eraseme.ps1 29..30+40+50..52

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object
pod29..30+40+50..52-srv1
pod29..30+40+50..52-srv2

How can I input the range from CLI and get the same result as the first code?

Jessamyn answered 21/12, 2016 at 2:41 Comment(0)
M
17

Your problem is that argument 29..30+40+50..52 is treated as a string literal in your .\eraseme.ps1 29..30+40+50..52 call - it is not recognized as an expression.

To force recognition as an expression, enclose the argument in (...), the grouping operator:

.\eraseme.ps1 (29..30+40+50..52)

The same applies if you want to use output from (another) command as a command argument; e.g.:

# Pass the lines read from file paths.txt as an array to Get-ChildItem
# (Parameter -Path is implied in both commands).
Get-ChildItem (Get-Content paths.txt)

Two asides:
$(...), the subexpression operator, is only ever needed in two cases: (a) to embed entire statement(s), notably loops and conditionals, in another statement, and (b) to embed an expression, command, or statement(s) inside "...", an expandable (interpolating) string. Just (...) is enough to embed a single command or expression in a statement (and even that isn't needed on the RHS of a variable assignment). While not likely, the unnecessary use of $(...) can have side effects - see this answer.
• You can make your script more robust by declaring your parameter with a more specific type, in which case an attempt to call it with a string would fail right away:
[Parameter(Mandatory=$False)] [int[]] $srv_range
(Other optimizations could be applied to your script as well.)


Optional background information

As for when an unquoted token is treated as an expression or nested command vs. an (expandable) string in argument mode (see also: about_Parsing):

  • (...), $(...), and @(...) by themselves or at the start of a token create a new parsing context, in which expressions or even nested commands can be used:

    • (...) is sufficient for a single expression or command. $(...) (the subexpression operator) can enclose multiple expressions / commands; so can @() (the array subexpression operator), and it additionally ensures that its output is always treated as an array.

    • Notably, the following expressions are not recognized without being enclosed in one of the above:

      • [...] (type literals) and access to their members, such as [Environment]::Version
      • .. (range expressions) such as 1..10
    • If, at the start of a token, (...), $(...), or @(...) are followed by additional characters, the first additional character is considered the start of a new, separate argument.

    • By contrast, if they're preceded by an unquoted literal or a variable-only reference, $(...) works like inside "..." (an expandable string), (...) starts a new argument that is an expression, and @(...) is taken as literal @ with (...) again starting a new argument that is an expression.

  • A @ followed by the name of a variable (e.g., @params) containing a collection or hashtable of parameter values initiates parameter splatting.

  • @{ ... } can be used to pass a hashtable literal (e.g., @{ key = 'value' }).

  • { ... } creates a script block ([scriptblock]).

  • By themselves or at the start of a token, variable references, including member access (property access, method calls, indexing) can be used as-is:

    • Expressions such as $HOME, $PSVersionTable.PSVersion, $someArray[0], and $someString.ToUpper() are recognized, and returned as their inherent type.
  • Without member access, i.e., with a simple variable reference such as $HOME, subsequent characters are (potentially) considered part of the same argument that is then interpreted as an expandable string - see below.

  • With member access, the first of any additional characters is considered the start of a new argument (e.g., $foo.Length-more results in two arguments: the value of $foo.Length and string literal -more).

  • Everything else is treated as an expandable string, i.e., similar to the contents of a double-quoted string, except that metacharacters[1] still need escaping and certain tokens are interpreted as multiple arguments.

    • Expandable means that embedded simple variable references (e.g., $HOME\Desktop or $env:APPDATA\Test) are interpolated (replaced with their stringified values).
      Note that this can result in a representation that differs from a given value's default output format as shown in the console, for instance (again, see this answer for more information).

      • Enclose a variable name in {...} to disambiguate it from subsequent characters, if necessary (e.g., ${HOME}).
    • To access a variable value's property or use an index or call a method or embed arbitrary commands, you must enclose the expression in $(...), e.g., v$($PSVersionTable.PSVersion)

    • Generally, it is safest to enclose tokens with embedded variable references / expressions in "...", because it avoids the following edge cases: * $(...) at the start of an unquoted token is not interpreted as part of an expandable string, it is treated as a separate argument (e.g., Write-Output $('ab')c results in two arguments: the result of $('ab') and literal c). * . at the start of a token immediately followed by a simple variable reference or subexpression results in separate arguments too.
      (E.g., .$HOME results in two arguments: literal ., and the value of $HOME)

      • Note: Even though the result of the expansion is a string, it doesn't necessarily remain one: the final type is determined by the type of to the parameter of the command at hand to which the expanded value is bound.

      • Escaping / quoting:

        • PowerShell has many more metacharacters than cmd.exe, and a notable pitfall is that , must be escaped to be treated a literal, because , is PowerShell's array-construction operator.
      • To escape a single character, prefix it with ` (backtick).

      • To avoid the need for escaping metacharacters individually, enclose the value in "..." (double quotes) or '...' (single quotes):

        • Use double quotes if you want the string to be interpolated (expanded), i.e., if you want to be able to embed variable references and subexpressions.

          • Inside a double-quoted string, `-escape the following chars. to treat them as literals: ` " $
        • Use single quotes to treat the value as a literal.

          • Inside a single-quoted string, escape a ' as ''
      • Single- or double-quoting is usually the easiest way to escape spaces in a value.

  • Finally, note that --%, the so-called stop-parsing symbol (PSv3+), completely changes the interpretation of all remaining arguments: designed for use with legacy cmd.exe command lines, it stops interpreting the rest of the line except for expansion of cmd.exe-style %...% environment variables. See Get-Help about_Parsing


As for using quoted tokens:

  • '...' or "..." by themselves or at the start of a token:

    • These are parsed as as usual: as a literal ('...') or expandable ("...") string.
    • Any additional characters cause the first additional character to be considered the start of a new, separate argument.
  • '...' or "..." being preceded by an unquoted literal or variable-only reference:

    • They are evaluated as usual and the result (i.e., with quotes removed) is appended to what precedes them (evaluated to).

[1] The argument-mode metacharacters (characters with special syntactic meaning) are:
<space> ' " ` , ; ( ) { } | & < > @ #.
Of these, < > @ # are only special at the start of a token.


Examples

Write-Output 1..10    # STRING: -> '1..10'
Write-Output (1..10)  # EXPRESSION: -> @(1, 2, ...)
# Write-Output $(1..10) would work too, but is only necessary if 
# the enclosed expression comprises *multiple* statements.

Write-Output [Environment]::Version  # STRING: -> '[Environment]::Ticks'
Write-Output ([Environment]::Version)  # EXPRESSION: -> a [System.Version] instance.

Write-Output a,b    # !! ARRAY @(1, 2), because "," is not escaped.
Write-Output a`,b   #`# STRING 'ab'                                 
Write-Output "a,b"  # ditto
Write-Output 'a,b'  # ditto

Write-Output $HOME\Desktop   # EXPANDED string (e.g.) 'C:\Users\jdoe\Desktop'
Write-Output "$HOME\Desktop" # ditto
Write-Output '$HOME\Desktop' # LITERAL string '$HOME\Desktop'
Write-Output dir=$HOME       # EXPANDED string (e.g.) 'dir=C:\Users\jdoe\Desktop'

Write-Output $PSVersionTable.PSVersion           # a [System.Version] instance
Write-Output "$($PSVersionTable.PSVersion)/more" # a [string]; e.g., '5.1.14393.576/more'
Write-Output "v$($PSVersionTable.PSVersion)"     # ditto; e.g., 'v5.1.14393.576'

# !!! These DO NOT WORK as intended.
Write-Output $($PSVersionTable.PSVersion)/more # $(...) at the *start*
Write-Output $PSVersionTable.PSVersion/more    # $(...) missing
Write-Output "$PSVersionTable.PSVersion/more"  # $(...) missing
Write-Output .$HOME # Specifically, .$ at the beginning is the problem; escaping . works
Matelote answered 21/12, 2016 at 2:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.