Using expanding strings as Powershell function parameters

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 executablesfoo arg1 arg2 – not like C# methodsfoo(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 full commands – e.g., "Today is $(Get-Date)" – so unless you fully control or trust the template string, this technique can be security risk.


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.

Leave a Comment