How do I delay expansion of variables in PowerShell strings? - string

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 '"', '`"') + '"')

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.

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.

Invoke-Expression "`"$string`""

Related

Powershell: difference between assignment and parameter for cmdlet (parsing)

I am new to powershell language and I have problems understanding some basic concepts regarding string concatenation.
I tried to concat a string with the + char as I knew it from other programming languages i. e. Java.
line 1: $result = 7
line 2: Write-Host "Result: " + $result + "!" # Result: + 7 + !
I then realized (i. e. in this question How do I concatenate strings and variables in PowerShell?) that I need to do it (in one of) the powershell way(s); for example like this.
line 3: Write-Host "Result: $result!" # Result: 7!
As I experimented a little I found out that if I assign the expression in line 2 to a variable it somehow works as I anticipated it in the first place.
line 4: $str = "Result: " + $result + "!"
line 5: Write-Host $str # Result: 7!
So my question is, why is there a difference if I pass a Java-style concatenated string to Write-Output cmdlet or if I assign the same string to a variable?
String concatenation and expansion is a bit different in PowerShell, here are several ways to accomplish it-
Format operator:
PS C:\> 'This exhibits {0} string expansions! {1} {0}!' -f #(2,'Wow!')
This exhibits 2 string expansions! Wow! 2!
Each item in the array is accessed by the number in the braces.
Subexpression:
PS C:\> "Sometimes you need calculations in a string. 5 + 3 = $(5 + 3)"
Sometimes you need calculations in a string. 5 + 3 = 8
An explanation here: this will not work with string literals, i.e., ''.
Everything contained in the subexpression will be evaluated and converted to a string if possible utilizing an object's ToString() method.
String Expansion:
PS C:\> $Var = 'This string'
PS C:\> "$Var is amazing!"
This string is amazing!
This will also not work with string literals, i.e., ''. If you need to concatenate a variable-qualifying character next to the variable call, you can use curly braces to avoid a null value, i.e., ${Var}_notattached
String Concatenation:
Tried and true:
PS C:\> 'Sometimes you just ' + 'need to add. 8 = ' + 8
Sometimes you just need to add. 8 = 8

Add to integer within string?

I would like to make a string by incrementing a variable within the string.
eg.
$result = "Result: $amount++";
How can this be achieved?
It can be done using trickery.
$result = "Result: ${\( $amount++ )}";
But why would you want to???
$result = "Result: ".$amount++;
If you want to modify a number in a string, you have to use the e modifier for the s operation. This makes Perl evaluating the replacement as an expression.
#! /usr/bin/perl
$_ = "Result: 1\n";
s/\d+/$&+1/e;
print;
It is documented in the Perl manual.
I take it that you have a string that already contains a 'number' (string of digits), and you want to increment that number within.
You'd have to extract the "number" first, in one way or another, since it is merely a string of chars when inside a string; then increment it and join it all back. I'll take it that it is a string of digits bounded by non-digits
my ($pre, $num, $post) = $str =~ m/(\D*)(\d+)(\D*)/;
$str = $pre . ($num+1) . $post;
This makes a critical assumption that the word contains a string of digits in only one place and no digits elsewhere, since if that were not the case the problem would be ill posed.
Just for the curiousity of it I'd like to add a bit to this. A part of a string can be accessed by substr, and that function can be manipulated as an lvalue (can be assigned to). So, if you were to know the starting position and the length of your "number" (what can be found in various ways) you could cram the above process in one statement, if you must
substr($str, $num_beg, $num_len) = substr($str, $num_beg, $num_len) + 1;
or, equally bad
substr($str, $num_beg, $num_len) = ($str =~ m/(\d+)/)[0] + 1;
Now your starting $str string contains the "number" within it incremented. However, this is plain nasty and I cannot recommend any of it. Finally, you can of course find $num_beg and $num_len on the fly, inside of substr, but that is just too much as the poor string would be processed three times in a single statement. (Also, this changes your $str in place, which your question hints is not what you want.)
Added Regex provide the capability to run code in the replacement part, by using /e modifier.
my $str = "ah20bah";
$str =~ s/(\d+)/$1+1/e;
say $str; # it's 'ah21bah'
See this in perlrequick and in perlop.

How explicitly resolve variables in a perl string?

In my perl script I want to have both versions of $config directory:
my $config='$home/client/config';
and
my $config_resolved="$home/client/config";
But I want to get $config_resolved from $config, i.e. something like this:
my $config_resolved=resolve_vars($config);
How can I do such thing in perl?
From the Perl FAQ (which every Perl programmer should read at least once):
How can I expand variables in text strings?
(contributed by brian d foy)
If you can avoid it, don't, or if you can
use a templating system, such as Text::Template or Template Toolkit,
do that instead. You might even be able to get the job done with
sprintf or printf:
my $string = sprintf 'Say hello to %s and %s', $foo, $bar;
However, for the one-off simple case where I don't want to pull out a
full templating system, I'll use a string that has two Perl scalar
variables in it. In this example, I want to expand $foo and $bar to
their variable's values:
my $foo = 'Fred';
my $bar = 'Barney';
$string = 'Say hello to $foo and $bar';
One way I can do this involves the substitution operator and a double /e flag. The
first /e evaluates $1 on the replacement side and turns it into $foo. The
second /e starts with $foo and replaces it with its value. $foo,
then, turns into 'Fred', and that's finally what's left in the string:
$string =~ s/(\$\w+)/$1/eeg; # 'Say hello to Fred and Barney'
The /e will also silently ignore violations of strict, replacing undefined
variable names with the empty string. Since I'm using the /e flag
(twice even!), I have all of the same security problems I have with
eval in its string form. If there's something odd in $foo, perhaps
something like #{[ system "rm -rf /" ]}, then I could get myself in
trouble.
To get around the security problem, I could also pull the
values from a hash instead of evaluating variable names. Using a
single /e, I can check the hash to ensure the value exists, and if it
doesn't, I can replace the missing value with a marker, in this case
??? to signal that I missed something:
my $string = 'This has $foo and $bar';
my %Replacements = (
foo => 'Fred',
);
# $string =~ s/\$(\w+)/$Replacements{$1}/g;
$string =~ s/\$(\w+)/
exists $Replacements{$1} ? $Replacements{$1} : '???'
/eg;
print $string;
I use eval for this.
So, you must replace all scalars (their names) with their values.
$config = 'stringone';
$boo = '$config/any/string';
$boo =~ s/(\$\w+)/eval($1)/eg;
print $boo;
Because you are using my to declare it as private variable, you might as well use a /ee modifier. This can find variables declared to be in local scope:
$boo =~ s/(\$\w+)/$1/eeg;
This is most tidily and safely done by the double-eval modifier on s///.
In the program below, the first /e evaluates the string $1 to get $home, while the second evaluates $home to get the variable's value HOME.
use strict;
my $home = 'HOME';
my $config = '$home/client/config';
my $config_resolved = resolve_vars($config);
print $config_resolved, "\n";
sub resolve_vars {
(my $str = shift) =~ s/(\$\w+)/$1/eeg;
return $str;
}
output
HOME/client/config

Powershell replace function strange behavior

a simple example and i don't know how to get it to work...
function replace($rep, $by){
Process { $_ -replace $rep, $by }
}
when I do
"test" | replace("test", "foo")
the result is
test
When I do
function replace(){
Process { $_ -replace "test", "foo" }
}
"test" | replace()
the result is
foo
any idea ?
Functions in PowerShell follow the same argument rules as cmdlets and native commands, that is, arguments are separated by spaces (and yes, this also means you don't need to quote your arguments, as they are automatically interpreted as strings in that parsing mode):
'test' | replace test foo
So if you call a PowerShell function or cmdlet with arguments in parentheses you'll get a single argument that is an array within the function. Invocations of methods on objects follow other rules (that look roughly like in C#).
To elaborate a little: PowerShell has two different modes in which it parses a line: expression mode and command mode. In expression mode PowerShell behaves like a REPL. You can type 1+1 and get 2 back, or type 'foo' -replace 'o' and get f back. Command mode is for mimicking a shell's behaviour. That's when you want to run command, e.g. Get-ChildItem or & 'C:\Program Files\Foo\foo.exe' bar blah. Within parentheses mode determination starts anew which is why Write-Host (Get-ChildItem) is different from Write-Host Get-ChildItem.
Remove the () in your function call and remove comma.
"test" | replace "test" "foo"

Include code in string definition?

I have a string
$string = "Active Directory"
and I want to make another string
Active_Directory_Results.txt
I would like to just do
$otherstring = "$string.Replace(" ","_")_Results.txt"
but that doesn't work out. What would be the correct way to pull this off?
You should not use invoke-expression for that. The original answer is good:
$otherstring = $string.Replace(" ","_") + "_Results.txt"
But really, you can just use a $(subexpression):
$otherstring = "$($string.Replace(" ","_"))_Results.txt"
The $() tells PowerShell to evaluate that BEFORE defining the string.
As an alternative, you can also use string formatting:
$otherstring = "{0}_Results.txt" -f $string.Replace(" ","_")
Proving once again that with scripting languages, there's always more than one right way ...
I'm not on my windows machine right now, but how does $otherstring = $string.Replace(" ","_") + "_Results.txt" work?
Check the invoke-expression command. It allows you to execute code in a string.
Like:
PS> $command = '$otherstring = $string.Replace(" ","_") + "_Results.txt"'
PS> Invoke-Expression $command

Resources