ExpandString in Powershell

Filed under: PowerShell

A quick and dirty way to have string templates in PowerShell is to invoke $ExecutionContext.InvokeCommand.ExpandString to interpolate strings. The tricky part is that variables have to be within the scope of calling ExpandString to replace values.

Managing scope makes it tricky to wrap it in a function. While working on choco-bear, I wanted to refactor the ability to expand a string into a function in a way that was almost like model binding.

I came across the fact that you can get the previous scope for Get-Variable by specifying integer levels rather than the String values of Script, Local Global, etc. The hardest part was figuring how to use Compare-Object correctly to create an array of variables not found within the Expand-String function.

Unfortunately Compare-Object doesn’t have something like -Diff Source for diffing. Compare-Object returns an array of objects with the SideInidicator property and the original object. We want objects that only have ‘=>’ as an indicator.

As a personal preference and because I often use JSON files for configuration, I opted to have a mustache-like syntax where or {{ variable }} or {{variable}} is replaced.

# TODO: pass in a PSObject, loop over properties and call set-variable on each
function Expand-String() {
    Param(
        [Parameter(Position = 1,ValueFromPipeline = $true)]
        [string] $Template
    )

    # get all the variables in the current function scope
    $local = gv -s 0

    # get all the variables from the previous scope that called this function.
    # Perform a diff, return missing variables from the calling script / function
    $removed = Compare-Object $local (gv -s 1) -Property Name -PassThru | Where {$_.SideIndicator -eq '=>' }

    # import all the missing variables
    foreach($var in $removed) {
        Set-Variable -Name ($var.Name) -Value ($var.Value)
    }

    # This could be improved. 
    $preparedTemplate = $Template.Replace("{{ ", "{{").Replace(" }}", "}}").Replace("{{", "`${").ReplacE("}}", "}")

    # nix the abilty to use $() withing the string.
    $preparedTemplate = $preparedTemplate.Replace("`$(", "(")

    return $ExecutionContext.InvokeCommand.ExpandString($preparedTemplate);
}

function Test-Theory() {

    $version = "1.11.3"
    return Expand-String "http://nginx.org/download/nginx-{{ version }}.zip"
}

Test-Theory
Expand-String "{{ Env:APPDATA }}\npm-cache"
Nerdy Mishka