问题
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?
回答1:
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).
回答2:
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 via an Expand-String
cmdlet is the subject of this GitHub feature request.
来源:https://stackoverflow.com/questions/51555635/using-expanding-strings-as-powershell-function-parameters