User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

Applies to: Windows PowerShell 5.0.10586, 5.1.14385.0, PowerShellGet 1.0.0.1

The Import-Module cmdlet, the #Requires directive, and module auto-loading (PS 3.0+) all import modules into a PowerShell session. But, which modules do they import by default? It’s an important question when you are writing shared code that uses commands from other modules. And, the answer has become more complex with the advent of side-by-side versioning in PowerShell 5.0.

NOTE: A change in PowerShell 5.1 removes the automatic preference that module auto-loading provides to the Modules directory in $home ($env:UserProfile). However, the behavior described in this article is remains the same when the first path in $env:PSModulePath is the Modules directory in $home.

——–

Beginning in Windows PowerShell 5.0, you can have multiple versions of the same module on the same machine. You can even have multiple versions of the same module imported into the same session.

Here are the versions of Pester on my test machine. (I have many because I’ve been experimenting with Pester for a while.)

PS C:\ps-test> Get-Module -List Pester

    Directory: C:\Users\JuneBlender\Documents\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.4.0      Pester                              {Describe, Context, It, Should...}
Script     3.3.11     Pester                              {Describe, Context, It, Should...}
Script     3.3.9      Pester                              {Describe, Context, It, Should...}

    Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.3.10     Pester                              {Describe, Context, It, Should...}
Script     3.3.8      Pester                              {Describe, Context, It, Should...}
Script     3.3.5      Pester                              {Describe, Context, It, Should...}


#Import all installed versions of Pester module
PS C:\ps-test> Get-Module -List Pester | Import-Module -PassThru

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.4.0      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}
Script     3.3.11     Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}
Script     3.3.9      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}
Script     3.3.10     Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}
Script     3.3.8      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}
Script     3.3.5      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}

Of course, you wouldn’t import multiple versions of a module into your session intentionally, but you need to expect that it might happen when scripts and modules that you share are run in environments that you cannot predict and control. In fact, even if you don’t plan to install multiple versions of a module on the same machine, Update-Module does it by default.

So, when you have multiple versions of a module installed on a machine, which version of the module does the #Requires directive, module auto-loading. or Import-Module import?

Which Versions are Imported By Default?

When you use Import-Module, run a command that triggers module auto-loading (PowerShell 3.0+), or run a script or module with a #Requires -Module directive, Windows PowerShell does not import the newest version of the module. Instead, it imports the newest version of the module that it finds in the first directory in which it finds the module. The order in which it looks for the module is determined by the value of the PSModulePath environment variable ($env:PSModulePath).

For example, when the first path in PSModulePath is $home\Documents\WindowsPowerShell\Modules and any version of the module is installed in that directory, #Requires, auto-loading and Import-Module import the newest version in that directory.

In this case, because the newest version of Pester, 3.4.0, is installed in my $home directory, Import-Module installs it.

PS C:\ps-test> Import-Module Pester -PassThru

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.4.0      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}

But, the newest installed version is not always installed by default. Look what happens when I import the PSScriptAnalyzer module. The newest version, 1.5.0, is installed in the Program Files directory, not the $home directory.

PS C:\ps-test> Get-Module -ListAvailable PSScriptAnalyzer

    Directory: C:\Users\JuneBlender\Documents\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Binary     1.2.0      PSScriptAnalyzer                    {Get-ScriptAnalyzerRule, Invoke-ScriptAnalyzer}
Binary     1.1.1      PSScriptAnalyzer                    {Get-ScriptAnalyzerRule, Invoke-ScriptAnalyzer}
Binary     1.1.0.1    PSScriptAnalyzer                    {Get-ScriptAnalyzerRule, Invoke-ScriptAnalyzer}
Binary     1.1.0      PSScriptAnalyzer                    {Get-ScriptAnalyzerRule, Invoke-ScriptAnalyzer}

    Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     1.5.0      PSScriptAnalyzer                    {Get-ScriptAnalyzerRule, Invoke-ScriptAnalyzer}

When I run Import-Module, it imports the newest version in the first directory that PowerShell encounters ($home\…\Modules), that is, version 1.2.0.

PS C:\ps-test> Import-Module -Name PSScriptAnalyzer -PassThru

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Binary     1.2.0      PSScriptAnalyzer                    {Get-ScriptAnalyzerRule, Invoke-ScriptAnalyzer}

The #Requires directive uses the same underlying logic. When I use #Requires with the Pester and PSScriptAnalyzer modules, it imports Pester 3.4.0 and PSScriptAnalyzer 1.2.0.

#Requires with the Pester and PSScriptAnalyzer modules imports Pester 3.4.0 and PSScriptAnalyzer 1.2.0

And, module auto-loading works the same way. In this sequence, I run Get-Module, then run commands that trigger auto-loading of the PSScriptAnalyzer and Pester modules. The result is the same: Pester 3.4.0 and PSScriptAnalyzer 1.2.0.

run Get-Module, then run commands that trigger auto-loading of the PSScriptAnalyzer and Pester modules

How to Import a Different Version?

The previous examples show how Import-Module, #Requires -Module, and module auto-loading work by default. But, you can specify the version of the module that you want to import.

The Import-Module command has MinimumVersion, MaximumVersion, and RequiredVersion parameters. It also has a FullyQualifiedName parameter that takes a ModuleSpecification object that accepts ModuleVersion (minimum), MaximumVersion, and RequiredVersion keys.

So, to import (exactly) the 1.5.0 version of PSScriptAnalyzer, you can use the following commands:

Import-Module -RequiredVersion 1.5.0

-or-

Import-Module -FullyQualifiedName @{ModuleName = 'PSScriptAnalyzer'; RequiredVersion = '1.5.0'}

TIP: For more information about the FullyQualifiedName or FullyQualifiedModule parameters, or the ModuleSpecification object (hash table), see Using a ModuleSpecification Object.

The #Requires -Module directive doesn’t have version parameters, but it can take a ModuleSpecification object, too. For example:

#Requires -Module @{ModuleName = 'PSScriptAnalyzer'; RequiredVersion = '1.5.0'}

Of course, module auto-loading is automatic, so you cannot control it. So, when you need a particular version of a module, use #Requires or Import-Module to import it explicitly.

What do MinimumVersion and MaximumVersion Do?

You can also use the MinimumVersion and MaximumVersion parameters. But, they are very literal. These parameters import the first version of the module that they find that meets the minimum criteria, even if the exact version exists on the computer, even in the same directory.

One more peek at the versions of Pester on this machine.

PS C:\ps-test> Get-Module -List Pester

    Directory: C:\Users\JuneBlender\Documents\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.4.0      Pester                              {Describe, Context, It, Should...}
Script     3.3.11     Pester                              {Describe, Context, It, Should...}
Script     3.3.9      Pester                              {Describe, Context, It, Should...}

    Directory: C:\Program Files\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.3.10     Pester                              {Describe, Context, It, Should...}
Script     3.3.8      Pester                              {Describe, Context, It, Should...}
Script     3.3.5      Pester                              {Describe, Context, It, Should...}

When I use the MinimumVersion parameter, it visits the directories in $env:PSModulePath (in order of appearance), and when it finds an instance of the module, it sorts the versions in that directory in descending order and imports the first one it finds that is greater than or equal to the specified MinimumVersion.

So, even though version 3.3.11 is present in the first encountered ($home) directory, Import-Module imports version 3.4.0. It makes sense when you think about it, but it wasn’t what I expected.

PS C:\ps-test> Import-Module Pester -MinimumVersion 3.3.11 -PassThru

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.4.0      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-

The MaximumVersion parameter works the same way using less than or equal.

PS C:\ps-test> Import-Module Pester -MaximumVersion 3.3.10 -PassThru

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.3.9      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}

What About Other Cmdlets?

Get-Module and Get-Command are very useful for getting the precise version of a module or command. Get-Module uses the same default rules as Import-Module. Get-Command uses the precedence rules (alias -> function -> cmdlet -> executable file), unless a command with that name exists in the session. Then, it gets the command that’s in the session. Good strategy.

In code that runs on other people’s computers (outside of my control), to make sure that I’m running the command I intend, and not a command with the same name in a different module, I use module-qualified commands.

For example, in a quick review of just a 100 or so of the 800+ modules in PSGallery, I found 4 commands named Get-Type. And, even if you explicitly import the module that contains the Get-Type command you want to use, module auto-loading often imports another one when you run the command. So, I always use module-qualified commands.

<ModuleName>\Get-Type ...
\Get-Type ...

TIP: To find commands with the same name in a session, use Group-Object.

Get-Module and Get-Command don’t have version parameters, but Get-Module has a FullyQualifiedName parameter and Get-Command has a FullyQualifiedModule command that both take ModuleSpecification objects.

For example, this command gets the modules that the PowerShellGet module, version 1.0.0.1 or greater, requires.

C:\ps-test> (Get-Module -FullyQualifiedName = @{ModuleName = 'PowerShellGet'; ModuleVersion = '1.0.0.1'}).RequiredModules
PackageManagement

This command gets the syntax of the Expand-Archive command in the default version (on that machine) of the PSCX module.

Get-Command -Name PSCX\Expand-Archive -Syntax

This command gets the Expand-Archive command from PSCX 3.2.1.0.

$theCommandIWant = Get-Command -Name Expand-Archive -FullyQualifiedModule @{ModuleName = 'PSCX'; RequiredVersion = '3.2.1.0'}

And, runs it.

& $theCommandIWant -Path .\MyZip.zip -OutputPath .\MyZip\Output

Excellent! So, what can go wrong?

Even when your command syntax is perfect and the module and command are installed on the system, Get-Command cannot get a command in the non-default version of the module, unless that version is already imported into the session.

For example, I can try to get the Invoke-Pester command in Pester 3.3.11, but it fails.

PS C:\ps-test> Get-Command -Name Invoke-Pester -FullyQualifiedModule @{ModuleName = 'Pester'; RequiredVersion = '3.3.11'}
Get-Command : The term 'Invoke-Pester' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ Get-Command -Name Invoke-Pester -FullyQualifiedModule @{ModuleName =  ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Invoke-Pester:String) [Get-Command], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand

Even though it fails, this Get-Command command auto-loads the default version of the Pester module. I didn’t expect that.

PS C:\ps-test> Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Manifest   3.1.0.0    Microsoft.PowerShell.Management     {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Content...}
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Script     3.4.0      Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}

I can use Get-Module -ListAvailable to verify that Pester 3.3.11 is installed on the system.

PS C:\ps-test> Get-Module -FullyQualifiedName @{ModuleName = 'Pester'; RequiredVersion = '3.3.11'} -ListAvailable

    Directory: C:\Users\JuneBlender\Documents\WindowsPowerShell\Modules

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.3.11     Pester                              {Describe, Context, It, Should...}

And, I can explicitly import Pester 3.3.11.

PS C:\ps-test> Import-Module Pester -RequiredVersion 3.3.11 -PassThru

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     3.3.11     Pester                              {AfterAll, AfterEach, Assert-MockCalled, Assert-VerifiableMocks...}

After it’s imported, I can run the original Get-Command for Invoke-Pester in Pester 3.3.11. Now that the required version of the module is imported into the session, the command succeeds.

 Get-Command -Name Invoke-Pester -FullyQualifiedModule @{ModuleName = 'Pester'; RequiredVersion = '3.3.11'}

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        Invoke-Pester                                      3.3.11     Pester

The auto-loading behavior creates a problem for modules that fail when you import more than one version of the module into the session. For example, PSScriptAnalyzer includes a Types.ps1xml file. When you try to import a second version of the same module into the session, it generates errors explaining that the extended types are already defined in the session.

# Look for a cmdlet in the newest version of the PSScriptAnalyzer module
PS C:\ps-test> Get-Command Get-ScriptAnalyzerRule -FullyQualifiedModule @{ModuleName = 'PSScriptAnalyzer'; ModuleVersion = '1.5.0'}
Get-Command : The term 'Get-ScriptAnalyzerRule' is not recognized as the name of a cmdlet, function, script file, or operable
program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ Get-Command Get-ScriptAnalyzerRule -FullyQualifiedModule @{ModuleName ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Get-ScriptAnalyzerRule:String) [Get-Command], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand

Even though the Get-Command command failed, PowerShell auto-loaded the default version of the module.

PS C:\ps-test> Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Manifest   3.1.0.0    Microsoft.PowerShell.Management     {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Content...}
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Binary     1.2.0      PSScriptAnalyzer                    {Get-ScriptAnalyzerRule, Invoke-ScriptAnalyzer}

Importing the 15.0 version of the module should fix the problem, but this command fails, because the Types.ps1xml added the extended types when PSScriptAnalyzer 1.2.0 was auto-loaded.

PS C:\ps-test> Import-Module PSScriptAnalyzer -MinimumVersion 1.5.0 -PassThru
Import-Module : The following error occurred while loading the extended type data file:
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord": The member Line is already present.
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord": The member Column is already present.
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord": The member DefaultDisplayPropertySet
is already present.
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.SuppressedRecord": The member Line is already present.
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.SuppressedRecord": The member Column is already present.
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.SuppressedRecord": The member Justification is already
present.
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.SuppressedRecord": The member DefaultDisplayPropertySet
is already present.
Error in TypeData "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.RuleInfo": The member DefaultDisplayPropertySet is
already present.
At line:1 char:1
+ Import-Module PSScriptAnalyzer -MinimumVersion 1.5.0 -Passthru
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Import-Module], RuntimeException
    + FullyQualifiedErrorId : FormatXmlUpdateException,Microsoft.PowerShell.Commands.ImportModuleCommand

Therefore, when you write code that is running on a system that you can’t predict, you need to be sure to remove all other versions of the module from the session before you try to import it. (In Pester tests for a module, before importing Pester or the module under test, I always run Remove-Module.)

So, to use the commands in PSScriptAnalyzer 1.5.0 or later, I write a sequence like this.

# Doesn't error out if there's no PSScriptAnalyzer in the session
Get-Module PSScriptAnalyzer | Remove-Module 
if ( (Import-Module -Name PSScriptAnalyzer -MinimumVersion 1.5.0 -PassThru).Version -ge '1.5.0') {
Get-ScriptAnalyzerRule ...
}

-or-

Get-Module PSScriptAnalyzer | Remove-Module 
if ( (Import-Module -Name PSScriptAnalyzer -MinimumVersion 1.5.0 -PassThru).Version -ge '1.5.0' ) {
 
$getRule = Get-Command Get-ScriptAnalyzerRule -FullyQualifiedModule @{ModuleName = 'PSScriptAnalyzer; ModuleVersion = '1.5.0'}
 
@ $GetRule
}

Conclusion

When writing code that might run on systems with different versions of different modules, be sure to verify that the versions of the modules that you need are the ones that are imported into the session.

June Blender is a technology evangelist at SAPIEN Technologies, Inc. You can reach her at This email address is being protected from spambots. You need JavaScript enabled to view it. or follow her on Twitter at @juneb_get_help.

If you have questions about our products, please post in our support forum.
For licensed customers, use the forum associated with your product in our Product Support Forums for Registered Customers.
For users of trial versions, please post in our Former and Future Customers - Questions forum.
Copyright © 2024 SAPIEN Technologies, Inc.