Powershell: $ExecutionContext.InvokeCommand.ExpandString does not work as expected in Modules

Created on 4 Oct 2018  路  6Comments  路  Source: PowerShell/PowerShell

When used inside a module function $ExecutionContext.InvokeCommand.ExpandString only works for global variables. However using $PSCmdLet.GetVariableValue also works for Script Scope variables. I think they both should work the same way, or there should be some other way to make a module function inherit the execution context of the caller.
The issue came up when I tried to wrap ExpandString and [System.Environment]::ExpandEnvironmentVariables in a neat Cmdlet inside our inhouse helper module. Spent the last 5 hours trying to figure out a way to make it work, found some solutions but all are sort of ugly.
I thought of two approaches to cleanly solve this issue:

  • a new option for CmdletBinding, like [CmdletBinding(InheritExecutionContext)]
  • some way to pass the $ExecutionContext, or the $Executioncontext.InvokeCommand property to a function/cmdlet.

I know that dotsourcing the script works, but that is not what I would like, since it can't be in a module then.
What also kind of works is to add the script to the ScriptsToProcess section in the modules .psd1 file, but that's also not very tidy, since it prevents the user from using Remove-Module to fully remove the module. The script will remain as an extra module.

A simple sample to demonstrate:

Create a new Module, ExpandStringIssue and add a file ExpandStringIssue.psm1 with the following content:

function ExpandStringIssue {
[CmdletBinding()]
Param( 
   [Parameter(Mandatory=$True,ValueFromRemainingArguments=$True,ValueFromPipeline=$True)]$Value
)
PROCESS {
    [System.Environment]::ExpandEnvironmentVariables( $ExecutionContext.InvokeCommand.ExpandString( $Value ) )
    $PSCmdlet.GetVariableValue( $Value.Trim( '$' ) )
}
}

Create a script file named ShowExpandStringIssue.ps1 with the following content

import-module ExpandStringIssue

New-Variable -Scope Script -Name MyVar -Value 'MyVarValue'

$StringToExpand = '$MyVar'
Write-Host "ExpandStringIssue returns:"
ExpandStringIssue -Value $StringToExpand
Write-Host "-"
Write-Host "ExpandString returns:"
$ExecutionContext.InvokeCommand.ExpandString( $StringToExpand )
Write-Host "-"

When invoked, the module ExpandStringIssue function returns an empty line for the $MyVar expansion, and a line containing the actual value of $MyVar to demonstrate that the module actually is able to query $MyVar, just not through ExpandString.
The bottom ExpandString output was just added to show the actual output of ExpandString.

Issue-Discussion WG-Engine

Most helpful comment

@mklement0 I saw that issue too, I guess it's related but I think there should be some general way to make the parent context available to a module function. So I thought it best to open a new issue, instead of somewhat hijacking that issue :)
Regarding the $PSCmdlet.SessionState.InvokeCommand.ExpandString(), I tried that too, during my research on the matter, among many other things. From what I found the InvokeCommand reference is actually the same in the caller and in the module function. By glancing at the Powershell source it seems that calling a function adds another scope to the ExecutionContext, and that causes the variables to be "hidden", so there's no way to get around that without changing the function invocation code in the Powershell source. As a side note, I even tried to get to the parent's scope trough calling Callstackframe.GetFrameVariables for the parent stack frame obtained using Get-PSCallstack but that also didn't work.

In the meantime I also thought a little about the implications a change to the context availability would have on module functions, and I definitely think the module function needs to request the parent context in some way, so the module programmer can make sure the function is aware of the security issues that might arise (like function overwrites, aliases or variables with malicious content, etc.).

All 6 comments

Somewhat related (in that not seeing the caller's variables is problematic with respect to preference variables): #4568

My first thought was that calling $PSCmdlet.SessionState.InvokeCommand.ExpandString() might work, but it doesn't (I don't know enough to tell you why).

@mklement0 I saw that issue too, I guess it's related but I think there should be some general way to make the parent context available to a module function. So I thought it best to open a new issue, instead of somewhat hijacking that issue :)
Regarding the $PSCmdlet.SessionState.InvokeCommand.ExpandString(), I tried that too, during my research on the matter, among many other things. From what I found the InvokeCommand reference is actually the same in the caller and in the module function. By glancing at the Powershell source it seems that calling a function adds another scope to the ExecutionContext, and that causes the variables to be "hidden", so there's no way to get around that without changing the function invocation code in the Powershell source. As a side note, I even tried to get to the parent's scope trough calling Callstackframe.GetFrameVariables for the parent stack frame obtained using Get-PSCallstack but that also didn't work.

In the meantime I also thought a little about the implications a change to the context availability would have on module functions, and I definitely think the module function needs to request the parent context in some way, so the module programmer can make sure the function is aware of the security issues that might arise (like function overwrites, aliases or variables with malicious content, etc.).

Thanks, @Line40; good sleuthing, and I agree on all counts.

& {
    $a = 42
    New-Module {
        function f {
            [CmdletBinding()]
            param()
            $PSCmdlet.InvokeCommand.InvokeScript(
                $PSCmdlet.SessionState,
                [ScriptBlock]::Create{
                    $ExecutionContext.SessionState.InvokeCommand.ExpandString('"$a"')
                }
            )
        }
    } | Out-Null
    f
}

Great stuff, @PetSerAl, thanks for sharing.

If I understand this correctly, the [scriptblock]::Create() call could be simplified to simply output a regular expandable string, as its expansion will be delayed anyway:

# Using a string argument, which is what [scriptblock]::Create() requires anyway
[scriptblock]::Create(' "`"$a`"" ') 

or, perhaps more typically, if embedded quoting isn't needed:

[scriptblock]::Create(' "The answer is $a." ') 

Bummer. Just spent 4 hours trying to figure out what I am doing wrong, and then found this. :-( Well, at least it explains it. Thanks for recording the problem.

Was this page helpful?
0 / 5 - 0 ratings