Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using expanding strings as Powershell function parameters

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?

like image 270
vazbyte Avatar asked Mar 06 '23 17:03

vazbyte


2 Answers

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 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.

like image 186
mklement0 Avatar answered Apr 26 '23 16:04

mklement0


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).

like image 21
Mark Wragg Avatar answered Apr 26 '23 16:04

Mark Wragg