Using Module Scope
One of the most perplexing topics in PowerShell, especially for beginner scripters is scope. Referencing items like variables and functions that may or may not be available can feel spinning a roulette wheel. You probably know about things like the global and script scopes. But there's another scope that you may not think about often: the module scope. There's a reason we use a manifest or the Export-ModuleMember
cmdlet. It's all about scope. Let's take a look at how module scope works and how you can use it to your advantage.
A PowerShell module typically consists of a set of functions and perhaps variables. When you import a module, PowerShell sets up a module scope. All commands from the module run in that scope. Where it gets tricky is that you'll see commands from the global scope. Here's a test module.
$fooVar = 100
Function Get-Secret {
Param([string]$string)
$n = $string.ToCharArray()
for ($i = 0; $i -lt $fooVar; $i++) {
$n = $n | Get-Random -Count $n.count
}
$n -join ''
}
Function Get-PSFoo {
[cmdletBinding()]
[alias('psf')]
Param(
[string]$Text
)
$space = ';',':','<','?','-','_' | Get-Random -count 1
#replace all whitespace with a random character
$Text = $Text -replace '\s',$space
Write-Verbose "Running PSFoo on $Text using a value of $fooVar"
Get-Secret -string $Text
}
I'll import his module.
Import-Module c:\demo\PSFoo-A.psm1
PowerShell loads all the modules into the global scope.
PS C:\> Get-Command -module PSFoo-A
CommandType Name Version Source
----------- ---- ------- ------
Function Get-PSFoo 0.0 PSFoo-A
Function Get-Secret 0.0 PSFoo-A
But I don't see the variable $fooVar
or the psf
alias.
PS C:\> Get-Alias psf
Get-Alias: This command cannot find a matching alias because an alias with the name 'psf' does not exist.
PS C:\> Get-Variable foovar
Get-Variable: Cannot find a variable with the name 'foovar'.
But the variable, which has a default value of 100 is available to the function.
PS C:\> Get-PSFoo "I am the walrus" -Verbose
VERBOSE: Running PSFoo on I:am:the:walrus using a value of 100
rl:s:hw:amaeIut
PowerShell found the variable in the module scope.
Export Module Member
Knowing that a module scope exists means we can take advantage of it. Your module will have many functions, but not every function need to be exposed or made visible to the user. You only need to let the user know what commands they can run from your module. You can have as many internal or helper functions as you need.
Internal functions help you modularize code across your module. It can also help with Pester testing. You can't mock a .NET method, but you can wrap the method in an internal function and mock that.
In the sample module I started with, the Get-Secret
function is intended to be used internally. It is not a command I expect the user to run. I want to keep that function private. It will still be available in the module scope so I can call it. Here's a revised version of the simple module.
#this is a private module scoped variable
$fooVar = 100
#this is a private function
Function _getSecret {
Param([string]$string)
$n = $string.ToCharArray()
for ($i = 0; $i -lt $fooVar; $i++) {
$n = $n | Get-Random -Count $n.count
}
$n -join ''
}
#this is a public function
Function Get-PSFoo {
[cmdletBinding()]
[alias('psf')]
Param(
[string]$Text
)
$space = ';',':','<','?','-','_' | Get-Random -count 1
#replace all whitespace with a random character
$Text = $Text -replace '\s',$space
Write-Verbose "Running PSFoo on $Text using a value of $fooVar"
_getSecret -string $Text
}
#Export-ModuleMember -Function Get-PSFoo -Alias psf
One big change is that I gave the private function a non-standard name. Since this function will never be called by a user, it does not need a standard Verb-Noun name. In fact, I want it to have a non-standard name so that if I see the command in my PowerShell session, I know I've done something wrong.
PS C:\> Get-Command -module PSFoo | Select Name
Name
----
_getSecret
Get-PSFoo
Importing this module also defines the alias, but not the variable.
PS C:\> Get-Alias psf
CommandType Name Version Source
----------- ---- ------- ------
Alias psf -> Get-PSFoo 0.0 PSFoo
PS C:\> PS C:\> $fooVar
PS C:\>
The way we solve this is by exporting the parts of the module using Export-ModuleMember
.
Edit the module file to uncomment the line and re-import the module. If you've previously imported the module in the same session, be sure to include the -Force
parameter.
I still get the alias, but I only see the single, and expected, public command.
PS C:\> Get-Command -module PSFoo | Select Name
Name
----
Get-PSFoo
PS C:\> Get-PSFoo "Secret Monkey"
ynMrkceoet-Se
If using a module manifest, you would specify what aliases and functions to export.
New-ModuleManifest -Path .\psfoo.psd1 -FunctionsToExport Get-PSFoo -AliasesToExport psf -RootModule PSFoo.psm1
As you add functions, you can manually update this entry.
FunctionsToExport = 'Get-PSFoo'
The recommended best practice is to explicitly list functions. Do not rely on wildcards.
Variable Bug
There is a long-standing bug with module scope that still exists in PowerShell 7.4. Let's say you had a variable in the module scope that you want to be publicly available.
#this is a public variable
$PSFooName = "banana"
You add it in the manifest.
New-ModuleManifest -Path .\psfoo-b.psd1 -FunctionsToExport Get-PSFoo -AliasesToExport psf -RootModule PSFoo-B.psm1 -VariablesToExport PSFooName
Typically, when you use a manifest, there is no need to use Export-ModuleMember
in the root module. However, when you import the module, you will have every exported featured except the variable.
PS C:\> $psfooname
PS C:\>
The variable does not get exported in the manifest. You need to re-enable the Export-ModuleMember
command in the root module.
Export-ModuleMember -Function Get-PSFoo -Alias psf -Variable PSFooName
Now, the variable is exported from the module scope.
PS C:\> $psfooname
banana
There's nothing wrong with exporting module members in the manifest and and Export-ModuleMember
The variable shows that it belongs to the module.
PS C:"\> Get-Variable psfooname | Format-List
Name : PSFooName
Description :
Value : banana
Visibility : Public
Module : PSFoo-B
ModuleName : PSFoo-B
Options : None
Attributes : {}
Defining a Global Variable
Remember, by default all variables will be visible within the module scope. I'm addressing a situation where you want to expose a variable to the user that they might want to change. You could define the variable to be in the global scope in the root module, or I suppose any script file you dot-source in your module.
$global:PSFooName = "banana"
If you go this route, you don't need to worry about exporting anything. The variable exists. But it no longer belongs to the module.
PS C:\> Get-Variable psfooname | Format-List
Name : PSFooName
Description :
Value : banana
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
This means that when you remove the module from your PowerShell session, this variable will remain. A exported variable will be removed.
If defining variables from your module this way, you might want to include a description.
Set-Variable -Name PSFooName -Value "banana" -Scope Global -Description "Defined in module $($MyInvocation.MyCommand.Name)"
You can't set the module property, but at the last the variable is documented.
PS C:\> Get-Variable psfooname | Format-List
Name : PSFooName
Description : Defined in module PSFoo-B.psm1
Value : banana
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
A Module Example
To give you a practical example, look at my PSClockmodule. In the root module I define a private variable
$SavePath = Join-Path -Path $home -ChildPath PSClockSettings.xml
In Start-PSClock I use the internal variable:
#Test if there is a saved settings file and no other parameters have been called
# $SavePath is a module-scoped variable set in the psm1 file
if ((Test-Path $SavePath)-AND (-not $Force)) {
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Using saved settings"
$import = Import-Clixml -Path $SavePath
foreach ($prop in $import.PSObject.properties) {
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Using imported value for $($prop.name)"
Set-Variable -name $prop.name -value $prop.Value
}
}
I also use it in Save-PSClock
Get-PSClock | Select-Object -property $props | Export-Clixml -Path $SavePath
The user never directly interacts with the variable, but I can use it in the module scope.
Summary
Technically, module scope doesn't exist in the same way that we have global and script scopes. What I'm referring to as module scope is really a special type of script scope. You don't have to worry about modifying anything. Just know that the scope exists and you can make it work to your advantage. Next time, I'll show you something else very interesting about this scope.