Powershell: Make string templating a first-class feature by exposing $ExecutionContext.InvokeCommand.ExpandString() as cmdlet Expand-String

Created on 27 Jan 2020  路  3Comments  路  Source: PowerShell/PowerShell

Summary of the new feature/enhancement

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_.

  • With a switch named, say, -NoExpressions specified, the presence of $(...) constructs _other than the following_ should refuse expansion and result in a non-terminating error:

    • Mere variable references: e.g., $($var) (same as just $var)

    • Variable _property_ references (but not method calls): e.g., $($var.Foo))

If evaluating expressions and commands _by default_ is considered too risky, the logic could be reversed:

  • Require a switch named, say, -AllowExpressions as an opt-in.
Area-Cmdlets-Utility Issue-Enhancement

Most helpful comment

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

All 3 comments

@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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

garegin16 picture garegin16  路  3Comments

Michal-Ziemba picture Michal-Ziemba  路  3Comments

rkeithhill picture rkeithhill  路  3Comments

alx9r picture alx9r  路  3Comments

manofspirit picture manofspirit  路  3Comments