How do I delay expansion of variables in PowerShell strings?
Asked Answered
K

5

11

Whatever you want to call it, I'm trying to figure out a way to take the contents of an existing string and evaluate them as a double-quoted string. For example, if I create the following strings:

$string = 'The $animal says "meow"'
$animal = 'cat'

Then, Write-Host $string would produce The $animal says "meow". How can I have $string re-evaluated, to output (or assign to a new variable) The cat says "meow"?


How annoying...the limitations on comments makes it very difficult (if it's even possible) to include code with backticks. Here's an unmangled version of the last two comments I made in response to zdan below:

----------

Actually, after thinking about it, I realized that it's not reasonable to expect The $animal says "meow" to be interpolated without escaping the double quotes, because if it were a double-quoted string to begin with, the evaluation would break if the double quotes weren't escaped. So I suppose the answer would be that it's a two step process:

$newstring = $string -replace '"', '`"'
iex "`"$string`""

One final comment for posterity: I experimented with ways of getting that all on one line, and almost anything that you'd think works breaks once you feed it to iex, but this one works:

iex ('"' + ($string -replace '"', '`"') + '"')
Killen answered 1/3, 2013 at 23:36 Comment(0)
S
8

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"
Skywriting answered 1/3, 2013 at 23:48 Comment(9)
I tried playing around with iex, but ran into two problems. The first one was that if the $string is a command, iex tries to invoke it. What you suggested here, putting it in escaped inner double quotes (iex ""$string"" rather than iex "$string"), gets around that. In fact, iex '"$string"' also works. However...Killen
...the second problem is that it breaks if $string contains double quotes. In fact, that's why I used a string that includes double quotes as an example. Modifying $string in order to make it work defeats the purpose. I didn't want to over-complicate the example, because I wanted an answer in general terms rather than workarounds to accomplish one specific task, but $string doesn't exist for the sole purpose of being re-evaluated later. It needs to be whatever it is, not tailored to fit a particular solution.Killen
Another note: In the function in which I encountered this issue, $string itself is a command that's later invoked by iex. The function builds the command line, which contains variables and double quoted text, then invokes it with iex $string. I also want to have it show the full command that's being invoked, without having to rebuild the command for that purpose. If I escape the double quotes, then iex $string won't work. (I didn't provide the function because I was interested in a generally applicable solution, not workarounds for this one specific case).Killen
Actually, after thinking about it, I realized that it's not reasonable to expect The $animal says "meow" to be interpolated without escaping the double quotes, because if it were a double-quoted string to begin with, the evaluation would break if the double quotes weren't escaped. So I suppose the answer would be that it's a two step process: $newstring = $string -replace '"', '"', then iex ""$string""`. I'll mark this question as answered, because the double-double-quoting resolves the main issue that caused me to give up on iex and come here for a solution.Killen
One final comment for posterity: I experimented with ways of getting that all on one line, and I did come up with a way to do it, but it's so convoluted it's probably not worth it: iex ('"' + ($string -replace '"', '"') + '"')`. Almost anything else you'd think would work breaks when you feed it to iex.Killen
...except it doesn't work if Invoke-String is implemented in a separate module from Invoke-String $string.Sankaran
This solution works even when the function and call location are in different modules.Sankaran
Neither write-host nor write-output are needed, just Invoke-Expression "`"$string`"" will do?Couldst
This answer needs a giant neon warning about the danger of invoking any user input as code.Columbite
R
11

Probably the simplest way is

$ExecutionContext.InvokeCommand.ExpandString($var)
Rialto answered 2/3, 2013 at 15:30 Comment(3)
Thanks, that is useful, although I wouldn't necessarily say it's simpler. I tried it out, and the double quotes get stripped out, unless they're escaped, so the extra step of adding a backtick in front of each double quote is still necessary. $ExecutionContext.InvokeCommand.ExpandString($string) returns The cat says meow, not The cat says "meow", unless the double quotes are escaped.Killen
This function has all sorts of pitfalls as of PS3. Strongly recommend avoiding, especially if you need to support PS2 and PS3 (where behaviors are different).Columbite
I ended up writing an Expand-String cmdlet that handles the version differences. It's over here on github.Sankaran
S
8

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"
Skywriting answered 1/3, 2013 at 23:48 Comment(9)
I tried playing around with iex, but ran into two problems. The first one was that if the $string is a command, iex tries to invoke it. What you suggested here, putting it in escaped inner double quotes (iex ""$string"" rather than iex "$string"), gets around that. In fact, iex '"$string"' also works. However...Killen
...the second problem is that it breaks if $string contains double quotes. In fact, that's why I used a string that includes double quotes as an example. Modifying $string in order to make it work defeats the purpose. I didn't want to over-complicate the example, because I wanted an answer in general terms rather than workarounds to accomplish one specific task, but $string doesn't exist for the sole purpose of being re-evaluated later. It needs to be whatever it is, not tailored to fit a particular solution.Killen
Another note: In the function in which I encountered this issue, $string itself is a command that's later invoked by iex. The function builds the command line, which contains variables and double quoted text, then invokes it with iex $string. I also want to have it show the full command that's being invoked, without having to rebuild the command for that purpose. If I escape the double quotes, then iex $string won't work. (I didn't provide the function because I was interested in a generally applicable solution, not workarounds for this one specific case).Killen
Actually, after thinking about it, I realized that it's not reasonable to expect The $animal says "meow" to be interpolated without escaping the double quotes, because if it were a double-quoted string to begin with, the evaluation would break if the double quotes weren't escaped. So I suppose the answer would be that it's a two step process: $newstring = $string -replace '"', '"', then iex ""$string""`. I'll mark this question as answered, because the double-double-quoting resolves the main issue that caused me to give up on iex and come here for a solution.Killen
One final comment for posterity: I experimented with ways of getting that all on one line, and I did come up with a way to do it, but it's so convoluted it's probably not worth it: iex ('"' + ($string -replace '"', '"') + '"')`. Almost anything else you'd think would work breaks when you feed it to iex.Killen
...except it doesn't work if Invoke-String is implemented in a separate module from Invoke-String $string.Sankaran
This solution works even when the function and call location are in different modules.Sankaran
Neither write-host nor write-output are needed, just Invoke-Expression "`"$string`"" will do?Couldst
This answer needs a giant neon warning about the danger of invoking any user input as code.Columbite
C
4

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.

Columbite answered 9/3, 2016 at 0:15 Comment(0)
S
3

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.

Sankaran answered 20/2, 2015 at 22:25 Comment(0)
C
0
Invoke-Expression "`"$string`""
Couldst answered 6/4, 2017 at 11:56 Comment(1)
but quotes are still irritating. Invoke-Expression "`"$($string -replace '"', '``"')`"" deals with quotes but not with backtick-quoteCouldst

© 2022 - 2024 — McMap. All rights reserved.