Note: This suggestion fleshes out the suggestion initially made by @sheldonhull in #11412.
PowerShell's expandable strings offer a flexible way of integrating both variable values and the output from expression and even entire commands in strings:
PS> $foo = 'bar'; "Variable `$foo contains '$foo' and contains $($foo.Length) character(s)."
Variable $foo contains 'bar' and contains 3 character(s).
Expandable strings expand _instantly_, whereas it would be helpful to also be able to define them as _templates_, for later _on-demand_ expansion based on _then-current_ variable values / expression or command output.
While this functionality _is_ currently available, it somewhat obscurely requires the use of $ExecutionContext.InvokeCommand.ExpandString():
# Define the template string, with *single quotes*, to avoid instant expansion.
# E.g., such a string could be read from a *file*.
$template = 'Variable `$foo contains ''$foo'' and contains $($foo.Length) character(s).'
# Give $foo different values in sequence, and expand the template with each.
foreach ($foo in 'bar', 'none') {
$ExecutionContext.InvokeCommand.ExpandString($template)
}
Result:
Variable $foo contains 'bar' and contains 3 character(s).
Variable $foo contains 'none' and contains 4 character(s).
The main advantage over using an expandable string is that _you can be given a string from an outside source_, such as a template string _read from a file_, which then obviously cannot be an expandable string _literal ("...").
I suggest exposing $ExecutionContext.InvokeCommand.ExpandString($template) as a cmdlet named Expand-String, which would enable the following, with the same output as above:
$template = 'Variable `$foo contains ''$foo'' and contains $($foo.Length) character(s).'
foreach ($foo in 'bar', 'none') {
# WISHFUL THINKING
Expand-String $template
}
# Or, via the pipeline:
# WISHFUL THINKING
'bar', 'none' | Expand-String 'Variable `$_ contains ''$_'' and contains $($_.Length) character(s).'
Security considerations:
Given that it is conceivable that template strings may come from an outside source (user-supplied), it is desirable to be able to _prevent evaluation of arbitrary expressions and commands_.
-NoExpressions specified, the presence of $(...) constructs _other than the following_ should refuse expansion and result in a non-terminating error:$($var) (same as just $var)$($var.Foo))If evaluating expressions and commands _by default_ is considered too risky, the logic could be reversed:
-AllowExpressions as an opt-in.@mklement0 I've always thought that this API was an under-appreciated feature given that template reification is such a common task in this business. Historically, since ExpandString is "eval equivalent" it was left as API only for "security-ish" reasons. So +1 for suggesting it. As for naming, how about Expand-Template as a more evocative name? Expand-Template -path template.json.
FYI, one of the things we did in Plaster to make template expansion safer was to create a constrained runspace with just a few commands we figured folks needed for template expansion within the context of Plaster. See https://github.com/PowerShell/Plaster/blob/16787a8fed9f425a35c4af6f89ba852d177094e2/src/InvokePlaster.ps1#L229-L302
I wonder if this could be applied to an Expand-Template command (at least by default) to make it safer? You could always provide some sort of -Force parameter to allow access to the current runspace and all commands.
GitHub
Plaster is a template-based file and project generator written in PowerShell. - PowerShell/Plaster
The ExpandString() returns just null for untrusted source. Since we have Ast for the template we should do a check in Begin block of the Expand-Template with a "security" Visitor. If we add a list of trusted variable names in Expand-Template with a parameter the "security" Visitor can do very strong check on the template Ast.
Anybody with Visitor creation experience could easily create a Expand-Template prototype on PowerShell script.
Most helpful comment
FYI, one of the things we did in
Plasterto make template expansion safer was to create a constrained runspace with just a few commands we figured folks needed for template expansion within the context of Plaster. See https://github.com/PowerShell/Plaster/blob/16787a8fed9f425a35c4af6f89ba852d177094e2/src/InvokePlaster.ps1#L229-L302I wonder if this could be applied to an
Expand-Templatecommand (at least by default) to make it safer? You could always provide some sort of-Forceparameter to allow access to the current runspace and all commands.