Steve B
Steve B

Reputation: 37690

Avoid variable in module to be overriden by caller and vice-versa

I'm building a custom module in Powershell to factorize some code.

In the functions in the module, I use variables. However, if the caller use the same variable names, it can interfer with my module.

For example, here a small module (MyModule.psm1) :

function Get-Foo{
    param(
        [int]$x,
        [int]$y
    )

    try{
        $result = $x/$y

    } catch{
        Write-Warning "Something get wrong"
    }
    if($result -ne 0){
        Write-Host "x/y = $result"
    }
}

Export-ModuleMember -Function "Get-Foo"

And a sample script that use the module:

Import-Module "$PSScriptRoot\MyModule\MyModule.psm1" -Force

$result = 3 # some other computation

Get-Foo -x 42 -Y 0

The output is :

x/y = 3

As you can see, the caller declared a variable name that conflicts with the one in my module.

What is the best practice to avoid this behavior ?

As a requirement, I have to assume that the module's developer won't be the main script developer. Thus, the internal on the module is not supposed to be known (kinda black box)

Upvotes: 3

Views: 234

Answers (4)

mklement0
mklement0

Reputation: 438398

Ivan Mirchev's helpful answer, robdy's helpful answer and AdminOfThings's comments on the question provide the crucial pointers; let me summarize and complement them:

  • Inside your function, a local $result variable is never created if parameter variable $y contains 0, because the division by zero causes a statement-terminating error (which triggers the catch block).

  • In the absence of a local $result variable, one defined in an ancestral scope may be visible (parent scope, grandparent scope, ...), thanks to PowerShell's dynamic scoping.

    • In your case, $result was defined in the global scope, which modules see as well, so your module function saw that value.

      • However, note that assigning to $result implicitly creates a local variable by that name rather than modifying the ancestral one. Once created locally, the variable shadows the ancestral one by the same name; that is, it hides it, unless you explicitly reference it in the scope in which it was defined.
    • Also note that modules by design do not see variables from a module-external caller other than the global scope, such as if your module is called from a script.

    • See this answer for more information about scopes in PowerShell.


Solutions:

  • Initialize local variable $result at the start of your function to guarantee its existence - see below.

  • Alternatively, refer to a local variable explicitly - I mention these options primarily for the sake of completeness and to illustrate fundamental concepts, I don't think they're practical:

    • You can use scope specifier $local:, which in the case of $y being 0 will cause $local:result to refer to a non-existent variable (unless you've initialized it before the failing division), which PowerShell defaults to $null:

      • if ($null -ne $local:result) { Write-Host "x/y = $result" }

      • Caveat: If Set-StrictMode -Version 1 or higher is in effect, a reference to a non-existent variable causes a statement-terminating error (which means that the function / script as a whole will by default continue to execute at the next statement).

    • A strict-mode-independent, but verbose and slower alternative is to use the Get-Variable cmdlet to explicitly test for the existence of your local variable:

      • if (Get-Variable -ErrorAction Ignore -Scope Local result) { Write-Host "x/y = $result" }

Solution with initialization == creation of the local variable up front:

function Get-Foo{
    param(
        [int]$x,
        [int]$y
    )

    # Initialize and thereby implicitly create
    # $result as a local variable.
    $result = 0

    try{
        $result = $x/$y
    } catch{
        Write-Warning "Something get wrong"
    }
    # If the division failed, $result still has its initial value, 0
    if($result -ne 0){
        Write-Host "x/y = $result"
    }
}

Upvotes: 3

Robert Dyjas
Robert Dyjas

Reputation: 5227

I'm not sure if this is actually best practice, but my way to avoid this is to always declare the variables I use (unless I specifically need to use variable from parent scope, which sometimes happen). That way you make sure you never reach the value from parent scope in your module:

# Declare
$result = $null
# Do something
$result = $x/$y

Of course in your example if seems like overkill, but in real life might be reasonable.

Another way I can think of is to change the scope.

$result => $private:result

Or to $script:result like Mike suggested.

Upvotes: 2

Mike
Mike

Reputation: 131

For this you need to have a good understanding of Powershell Scope Types. There are four different types of scopes: Global Scope, Script Scope, Private Scope, Local Scope

I think you need to make use of the script scope, because these scopes are created when you run/execute a PS1 script/module. This means that you have to define the variable like:

$script:x
$script:y

Upvotes: 1

Ivan Mirchev
Ivan Mirchev

Reputation: 839

The reason for this is that you are not assigning a value to the variable result, when dividing by zero, as it generates an error. You have a variable $result in the global scope (PowerShell console), which is being inherited into the function scope (Child scope and inheritance is parent to child, not vice versa)! If you have a value, that is being assigned to the variable $result in the chatch block, for example, it could solve the issue. Something like:

function Get-Foo{
    param(
        [int]$x,
        [int]$y
    )

    try{
        $result = $x/$y

    } catch{
        Write-Warning "Something get wrong"
        $result = $_.exception.message 
    }
    if($result -ne 0){
        Write-Host "x/y = $result"
    }
}

Note: $_.exception.message = $error.Exception.message in this case

Another way would be to use the scope modifier for the variable result at the begining of the function: $global:result = $null. This way you will null the global variable (or provide other value), but then the result would be:

WARNING: Something get wrong
x/y =

Which is not really meaningful.

more details: get-help about_Scopes -ShowWindow or: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-6

If you have more questions, I would be happy to address.

Upvotes: 1

Related Questions