How do I delay expansion of variables in PowerShell strings?

Edit: It turns out that string interpolation behavior is different depending on the version of PowerShell. I wrote a better version of the xs (Expand-String) cmdlet with unit tests to deal with that behavior over here on GitHub.


This solution is inspired by this answer about shortening calls to object methods while retaining context. You can put the following function in a utility module somewhere, and it still works when you call it from another module:

function xs
{
    [CmdletBinding()]
    param
    (

        # The string containing variables that will be expanded.
        [parameter(ValueFromPipeline=$true,
                   Position=0,
                   Mandatory=$true)]
        [string]
        $String
    )
    process
    {
        $escapedString = $String -replace '"','`"'
        $code = "`$ExecutionContext.InvokeCommand.ExpandString(`"$escapedString`")"
        [scriptblock]::create($code)
    }
}

Then when you need to do delayed variable expansion, you use it like this:

$MyString = 'The $animal says $sound.'
...
$animal = 'fox'
...
$sound = 'simper'

&($MyString | xs)
&(xs $MyString)

PS> The fox says simper.
PS> The fox says simper.

$animal and $sound aren't expanded until the last two lines. This allows you to set up a $MyString up front and delay expansion until the variables have the values you want.


Probably the simplest way is

$ExecutionContext.InvokeCommand.ExpandString($var)

You could use Invoke-Expression to have your string reparsed - something like this:

$string = 'The $animal says `"meow`"'
$animal = 'cat'
Invoke-Expression "Write-Host `"$string`""

Note how you have to escape the double quotes (using a backtick) inside your string to avoid confusing the parser. This includes any double quotes in the original string.

Also note that the first command should be a command, if you need to use the resulting string, just pipe the output using write-output and assign that to a variable you can use later:

$result = Invoke-Expression "write-output `"$string`""

As noted in your comments, if you can't modify the creation of the string to escape the double quotes, you will have to do this yourself. You can also wrap this in a function to make it look a little clearer:

function Invoke-String($str) { 
    $escapedString =  $str -replace '"', '`"'
    Invoke-Expression "Write-Output `"$escapedString`""
}

So now it would look like this:

# ~> $string = 'The $animal says "meow"'
# ~> $animal = 'cat'
# ~> Invoke-String $string
The cat says "meow"

You can use the -f operator. This is the same as calling [String]::Format as far as I can determine.

PS C:\> $string = 'The {0} says "meow"'
PS C:\> $animal = 'cat'
PS C:\> Write-Host ($string -f $animal)
The cat says "meow"

This avoids the pitfalls associated with quote stripping (faced by ExpandString and Invoke-Expression) and arbitrary code execution (faced by Invoke-Expression).

I've tested that it is supported in version 2 and up; I am not completely certain it's present in PowerShell 1.