Using expanding strings as Powershell function parameters
Asked Answered
O

2

6

I'm trying to write a function that will print a user-supplied greeting addressed to a user-supplied name. I want to use expanding strings the way I can in this code block:

$Name = "World"  
$Greeting = "Hello, $Name!"
$Greeting

Which successfully prints Hello, World!. However, when I try to pass these strings as parameters to a function like so,

function HelloWorld
{  
    Param ($Greeting, $Name)
    $Greeting
}
HelloWorld("Hello, $Name!", "World")

I get the output

Hello, !  
World  

Upon investigation, Powershell seems to be ignoring $Name in "Hello, $Name!" completely, as running

HelloWorld("Hello, !", "World")

Produces output identical to above. Additionally, it doesn't seem to regard "World" as the value of $Name, since running

function HelloWorld
{  
    Param ($Greeting, $Name)
    $Name
}
HelloWorld("Hello, $Name!", "World")

Produces no output.

Is there a way to get the expanding string to work when passed in as a function parameter?

Obstruct answered 27/7, 2018 at 10:6 Comment(2)
First, variable substitution happens when you construct the string, not when you display it. Second, you are not calling HelloWorld properly: you are passing array with two strings as $Greeting and nothing to $Name.Becharm
@PetSerAl Thanks, I see what you mean - I should be calling HelloWorld "Hello, $Name!" "World" to assign the variables correctly. That of course still produces Hello, !.Obstruct
G
2

Your issue occurs because the $Name string replacement is happening outside of the function, before the $Name variable is populated inside of the function.

You could do something like this instead:

function HelloWorld
{  
    Param ($Greeting, $Name)
    $Greeting -replace '\$Name',$Name
}

HelloWorld -Greeting 'Hello, $Name!' -Name 'World'

By using single quotes, we send the literal greeting of Hello, $Name in and then do the replacement of this string inside the function using -Replace (we have to put a \ before the $ in the string we're replace because $ is a regex special character).

Galoot answered 27/7, 2018 at 10:48 Comment(0)
C
9

In order to delay string interpolation and perform it on demand, with then-current values, you must use $ExecutionContext.InvokeCommand.ExpandString()[1] on a single-quoted string that acts as a template:

function HelloWorld
{  
    Param ($Greeting, $Name)
    $ExecutionContext.InvokeCommand.ExpandString($Greeting)
}

HelloWorld 'Hello, $Name!' 'World'   # -> 'Hello, World!'

Note how 'Hello, $Name!' is single-quoted to prevent instant expansion (interpolation).

Also note how HelloWorld is called with its arguments separated with spaces, not ,, and without (...).

In PowerShell, functions are invoked like command-line executables - foo arg1 arg2 - not like C# methods - foo(arg1, arg2) - see Get-Help about_Parsing.
If you accidentally use , to separate your arguments, you'll construct an array that a function sees as a single argument.
To help you avoid accidental use of method syntax, you can use Set-StrictMode -Version 2 or higher, but note that that entails additional strictness checks.

Note that since PowerShell functions by default also see variables defined in the parent scope (all ancestral scopes), you could simply define any variables that the template references in the calling scope instead of declaring individual parameters such as $Name:

function HelloWorld
{  
    Param ($Greeting) # Pass the template only.
    $ExecutionContext.InvokeCommand.ExpandString($Greeting)
}

$Name = 'World'  # Define the variable(s) used in the template.
HelloWorld 'Hello, $Name!'     # -> 'Hello, World!'

Caveat:

  • PowerShell string interpolation supports embedding arbitrary commands, via $(...), the subexpression operator, e.g., "Today is $(Get-Date)" - so unless you fully control or trust the template string, this technique can be security risk. For a solution that suppresses command expansion, i.e. allows variable references only, see the bottom section of this answer.

Ansgar Wiechers proposes a safe alternative based on .NET string formatting via PowerShell's -f operator and indexed placeholders ({0}, {1}, ...):

Note that you can then no longer apply transformations on the arguments as part of the template string or embed commands in it in general.

function HelloWorld
{  
    Param ($Greeting, $Name)
    $Greeting -f $Name
}

HelloWorld 'Hello, {0}!' 'World'     # -> 'Hello, World!'

Pitfalls:

  • PowerShell's string expansion uses the invariant culture, whereas the -f operator performs culture-sensitive formatting (snippet requires PSv3+):

      $prev = [cultureinfo]::CurrentCulture
      # Temporarily switch to culture with "," as the decimal mark
      [cultureinfo]::CurrentCulture = 'fr-FR' 
    
      # string expansion: culture-invariant: decimal mark is always "."
      $v=1.2; "$v";  # -> '1.2'
    
      # -f operator: culture-sensitive: decimal mark is now ","
      '{0}' -f $v    # -> '1,2'  
    
      [cultureinfo]::CurrentCulture = $prev
    
  • PowerShell's string expansion supports expanding collections (arrays) - it expands them to a space-separated list - whereas the -f operator only supports scalars (single values):

      $arr = 'one', 'two'
    
      # string expansion: array is converted to space-separated list
      "$var" # -> 'one two'
    
      # -f operator: array elements are syntactically treated as separate values
      # so only the *first* element replaces {0}
      '{0}' -f $var  # -> 'one'
      # If you use a *nested* array to force treatment as a single array-argument,
      # you get a meaningless representation (.ToString() called on the array)
      '{0}' -f (, $var)  # -> 'System.Object[]'
    

[1] Surfacing the functionality of the $ExecutionContext.InvokeCommand.ExpandString() method in a more discoverable way, namely via an Expand-String cmdlet, is the subject of GitHub feature-request issue #11693.

Chit answered 27/7, 2018 at 10:44 Comment(0)
G
2

Your issue occurs because the $Name string replacement is happening outside of the function, before the $Name variable is populated inside of the function.

You could do something like this instead:

function HelloWorld
{  
    Param ($Greeting, $Name)
    $Greeting -replace '\$Name',$Name
}

HelloWorld -Greeting 'Hello, $Name!' -Name 'World'

By using single quotes, we send the literal greeting of Hello, $Name in and then do the replacement of this string inside the function using -Replace (we have to put a \ before the $ in the string we're replace because $ is a regex special character).

Galoot answered 27/7, 2018 at 10:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.