Reputation: 37690
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
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.
$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
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
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
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