Is there any way for a powershell module to get at its caller's scope?

倖福魔咒の 提交于 2019-12-01 06:06:22

问题


I have a collection of utility functions and other code that I dot-source into every powershell file I write. Having gotten bit by caller scope variables influencing it, I started looking into changing it into a powershell module.

I ran into problems with a couple of the special things I do in it where I actually do want some interaction between the scopes. I'm wondering if there is anyway to "get at" the scope of a module's caller to keep this functionality while moving to a powershell module?

If not, is my best path forward to keep these more specialized things in a dot-sourced file and move the more traditional utility functions into a module? Here are the things that don't easily move to a module:

  • Setting strict mode and error action preferences to keep sane, e.g.:

    Set-StrictMode -Version Latest
    $ErrorActionPreference = "Stop"
    $PSDefaultParameterValues['*:ErrorAction']='Stop'
    

    This (as expected) has no effect on the caller's environment when the code is run from a .psm1 powershell module. Is there any way to cross from the psm1 scope to the caller scope to make these changes?

  • Printing out information about the top-level script call, e.g.:

    $immediateCallerPath = Get-Variable -scope 1 -name PSCommandPath -ValueOnly
    Write-Host "Starting script at $immediateCallerPath"
    $boundParameters = Get-Variable -scope 1 -name PSBoundParameters -ValueOnly
    Write-Host "Bound parameters are:"
    foreach($psbp in $boundParameters.GetEnumerator())
    {
            "({0},{1})" -f $psbp.Key,$psbp.Value | Write-Host
    }
    

    Likewise, these commands can no longer see the top-most calling scope once placed in a .psm1 file


回答1:


$PSCmdlet.SessionState seems to provide a function inside a script module access to the call site's variables provided the call site is outside the module. (If the call site is inside the module, you can just use Get- and Set-Variable -Scope.) Here is an example using SessionState:

New-Module {
    function Get-CallerVariable {
        param([Parameter(Position=1)][string]$Name)
        $PSCmdlet.SessionState.PSVariable.GetValue($Name)
    }
    function Set-CallerVariable {
        param(
            [Parameter(ValueFromPipeline)][string]$Value,
            [Parameter(Position=1)]$Name
        )
        process { $PSCmdlet.SessionState.PSVariable.Set($Name,$Value)}
    }
} | Import-Module

$l = 'original value'
Get-CallerVariable l
'new value' | Set-CallerVariable l
$l

which outputs

original value
new value

I'm not sure whether SessionState was intended to be used in this manner. For what it's worth, this is the same technique used in Get-CallerPreference.ps1. There are also some test cases here which pass on PowerShell versions 2 through 5.1.




回答2:


Not sure if I understand completely what you are after. I believe you would like to know where is the code implemented that invokes the cmdlets of you module. Maybe even further up.

If I'm correct then you can use Get-PSCallStack to retrieve the stack trace. For example from an unsaved script it looks like this

PS C:\Users\asarafian> Get-PSCallStack

Command       Arguments Location 
-------       --------- -------- 
<ScriptBlock> {}        <No file>

If the file was saved then it would look like this

PS C:\Users\asarafian> Get-PSCallStack

Command       Arguments Location 
-------       --------- -------- 
File1.ps1           <No file>

Depending on what you want to achieve, which is not clear to me, you need to walk the list from [0] which is the code that executed Get-PSCallStack to [x].

When building the XWrite I wanted also to figure out if an entry from the stack was a script file, a cmdlet part of a module or unknown like <ScriptBlock>.

My implementation is in Get-XCommandSource.ps1 and it follows the following logic for the command value from the stacktrace

  1. if it ends with .ps1 then it's a script file.
  2. if it is <ScriptBlock> then it's a script block.
  3. if the command can be loaded with Get-Command then
    1. if it has a module then it's a cmdlet in a module.
    2. if not then it's a cmdlet/function imported with .\cmdlet.ps1 pattern.

Here is the implementation:

function Get-XCommandSource
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$Command
    )
    begin {

    }

    process {
        if(-not $Command)
        {
            "Unknown"
        }
        elseif($Command.EndsWith(".ps1"))
        {
            "Script"
        }
        elseif($Command -eq "<scriptblock>")
        {
            "Unknown"
        }
        else
        {
            $cmdlet=Get-Command -Name $command -ErrorAction SilentlyContinue
            if($cmdlet)
            {
                $moduleName=$cmdlet|Select-Object -ExpandProperty ModuleName

                if($moduleName)
                {
                    $moduleName
                }
                else
                {
                    "Function"
                }
            }
            else
            {
                "Unknown"
            }
        }
    }

    end {

    }
}


来源:https://stackoverflow.com/questions/46528262/is-there-any-way-for-a-powershell-module-to-get-at-its-callers-scope

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!