locked
Script Modules and Variable Scope (Preference Variables) RRS feed

  • General discussion

  • While working with PowerShell and writing various psm1 module files, I've noticed that for whatever reason, Advanced Functions in a Script Module (psm1 file) do not inherit the Preference variables (or any other variable) from the scope of the code that calls them (unless that happens to be the Global scope.)  As far as the functions in the Script Module are concerned, variables exist either in the function, in the module, or in the Global scope, and that's it. If the caller sets, for example, $ErrorActionPreference to 'Stop' in the Script scope and then calls your function, your function will still be acting with the value of the global $ErrorActionPreference, which is most likely still set to the default value of 'Continue'.

    You can see this behavior by creating a test PS1 and PSM1 file as follows:

    # Test.psm1:
    
    function Test-ModuleFunction
    {
        [CmdletBinding()]
        param ( )
    
        Write-Host "Module Function: ErrorActionPreference = $ErrorActionPreference"
    }
    
    # Test.ps1:
    
    Import-Module .\Test.psm1
    
    $ErrorActionPreference = 'Stop'
    
    Test-ModuleFunction
    

    I researched this, and at some point discovered that module functions can get at the variables in the caller's scope by using properties and methods of the $PSCmdlet object, for example:

    function Test-ModuleFunction
    {
        [CmdletBinding()]
        param ( )
        
        if (-not $PSBoundParameters.ContainsKey('ErrorAction'))
        {
            $ErrorActionPreference = $PSCmdlet.GetVariableValue('ErrorActionPreference')
        }
        
        Write-Host "Module Function: ErrorActionPreference = $ErrorActionPreference"
    }

    However, it gets rather annoying to have to define code like that in every single function that needs to be exported from a script module (assuming you want the module's functions to behave according to preference variables set in the caller's scope.)  I did some more digging earlier today, and discovered a way to put this code into a function, which can be called like this:

    function Test-ModuleFunction
    {
        [CmdletBinding()]
        param ( )
        
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        
        Write-Host "Module Function: ErrorActionPreference = $ErrorActionPreference"
    }

    Passing the $PSCmdlet object to the Get-CallerPreference function gives it the ability to read the BoundParameters collection and access your caller's variables, and passing in $ExecutionContext.SessionState gives it the ability to set those variables in your function's scope (Test-ModuleFunction, in this case.)  This works even if the Get-CallerPreference function is exported from a different module than Test-ModuleFunction.

    I'm pretty happy with how easy it is to copy and paste this one line into the Begin block of my script module functions, and figured I'd share it.  If you've run into this behavior and want to give the function a shot, it's on the Gallery at http://gallery.technet.microsoft.com/Inherit-Preference-82343b9d

    Tuesday, January 21, 2014 12:52 AM

All replies

  • I fixed a bug in the code today that was causing problems if Get-CallerPreference and the function calling it were in the same module. For the few people that have downloaded this already, you may want to pick up the latest version.
    Tuesday, January 21, 2014 3:53 PM
  • for "whatever reason": perhaps that reason is that script modules are considered outside the scope of the calling script. I often use functions defined in script modules myself instead of scripts because that makes the code in those functions as available as, well, cmdlets without having to know where the scripts are located.

    But one reason behind the script module concept seems to be to enable relatively seemless sharing/distribution of code that could be compiled or in script. Client users might not be up to debugging someone else's code, which, in any case, has most likely already been well tested.

    Alternately, there could be something inherent in the mechanisms that support script modules that causes this scope issue.

    Nice workaround, David. But it would be perhaps more manageable (in powershell 5?) to be able to declare which modules should have their own preference variables, and which should inherit from the caller.


    Al Dunbar -- remember to 'mark or propose as answer' or 'vote as helpful' as appropriate.

    Tuesday, January 21, 2014 7:22 PM
  • I'm not sure if that much thought went into how things work right now (or at least, not thought from this specific point of view). I've looked through some of the PowerShell engine code with dotPeek, and it's pretty complex.  In this case, it appears that each script module is given its own SessionState object (possibly because they wanted each module to have its own Script scope), and those SessionState objects are what keep track of the stack of scopes that are checked when you look up a variable value.

    Anyhow, this originally annoyed me because I could set $ErrorActionPreference or $VerbosePreference or whatever and call a compiled cmdlet, and the cmdlet would behave according to my preference variables.  Advanced Functions in a script module, on the other hand, would ignore them.

    I agree that it would be simpler of something in the engine took care of this for you, but I'm finally happy with the workaround.  Using $PSCmdlet.GetVariableValue() for each variable (with that code repeated in every exported function) worked, but was a lot of clutter.

    Tuesday, January 21, 2014 7:35 PM
  • I'm not sure if that much thought went into how things work right now (or at least, not thought from this specific point of view). I've looked through some of the PowerShell engine code with dotPeek, and it's pretty complex.  In this case, it appears that each script module is given its own SessionState object (possibly because they wanted each module to have its own Script scope), and those SessionState objects are what keep track of the stack of scopes that are checked when you look up a variable value.

    You seem to have confirmed my theory that, like other types of modules, script modules were built on a structure that isolated them from the scope of any user scripts that might call their functions.

    But maybe the clue came earlier in your original post where you said:

    
    While working with PowerShell and writing various psm1 
    module files, I've noticed that for whatever reason, 
    Advanced Functions in a Script Module (psm1 file) do 
    not inherit the Preference variables (or any other 
    variable) from the scope of the code that calls them 
    (unless that happens to be the Global scope.)
    

    Regardless of any intent to isolate module functions from the non-global preference values of the calling script, it seems to me that the preference values exist in exactly the same way that all powershell variables exist - in the variable: provider. The only difference is that they have special meaning to powershell, in that their values affect how powershell code functions.

    If they were handled in a completely different manner, perhaps being absolutely glopbal in nature, imagine the nuisance of the normal scoping rules not applying to them.


    Al Dunbar -- remember to 'mark or propose as answer' or 'vote as helpful' as appropriate.

    Thursday, January 23, 2014 5:36 PM
  • Could be.  I suppose it doesn't really matter what the reasoning was behind the way script modules / scopes are designed; I just like to be able to get consistent behavior between an advanced function and a cmdlet.  Ideally, the calling code should never have to know what type of command they're executing.

    This little snag with preference variables was one of the places where the functions and cmdlets don't behave the same way, by default.

    Thursday, January 23, 2014 5:46 PM