Understanding PowerShell Modules

Almost all PowerShell commands live in modules, and by adding new modules to PowerShell, you can easily add more commands. Here is all you need to know about PowerShell modules.

PowerShell is a scripting engine. Commands are strictly separated from this engine and reside in modules. That’s why you can easily extend the PowerShell command set. Even the specialized PowerShell consoles shipping with Exchange or SQL Server are plain regular PowerShell consoles that simply load more commands from external modules.

You can use whatever PowerShell host you like (console, ISE editor, VSCode editor, etc.) to manage Active Directory, Exchange, Sharepoint, or whatever it may be. Just make sure the PowerShell modules are loaded that provide the commands you require.

This is the first part of an article series deeply looking into PowerShell modules. It applies to both Windows PowerShell and the new PowerShell 7 aka PowerShell Core. The first part explains how commands are loaded from modules, and how PowerShell automatically loads modules on-demand.

As an experienced PowerShell user, you may safely skip this article. It provides you with a basic overview of the module architecture, and I’ll reference this material in other articles where I explain how to author your own modules.

Before PowerShell modules were invented in PowerShell 2, commands were added using Snap-Ins. The snap-in technology is obsolete and was removed from PowerShell 7. In Windows PowerShell, you can still use Add-PSSnapin to load snap-ins. Snap-ins are not covered in this article, and you should try and avoid them in favor of modules.

Commands On Demand

When you launch a new PowerShell it comes with only very few commands loaded into memory:

(Get-Command -ListImported).Count
213

Still, there are thousands of commands potentially available:

(Get-Command).Count
3716

The reported numbers can vary greatly, too, so they may be different for you. That’s because PowerShell loads commands on demand which saves resources and speeds up the launch process, and commands can be extended by adding PowerShell modules. The more modules you have, the more commands are available.

Automatic Module Loading

When you launch a fresh PowerShell, only a few Modules are pre-loaded in memory:

# load a new fresh PowerShell instance w/o running any profile scripts:
powershell.exe -NoProfile
# dump all modules loaded in memory:
Get-Module
ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type, Clear-Variable, Compare-O...
Script     2.0.0      PSReadline                          {Get-PSReadLineKeyHandler, Get-PSReadLineOption,...

These pre-loaded Modules provide a basic set of PowerShell commands:

(Get-Module).ExportedCommands | Format-Wide -Column 6
Add-Computer       Add-Content        Checkpoint-Com... Clear-Content     Clear-EventLog    Clear-Item
Clear-ItemProperty Clear-RecycleBin   Complete-Trans... Convert-Path      Copy-Item         Copy-ItemProperty
Debug-Process      Disable-Compute... Enable-Compute... Get-ChildItem     Get-Clipboard     Get-ComputerInfo
...

With these pre-loaded commands you can already do a lot of sophisticated things, for example download information from websites:

# dump source code from url:
Invoke-RestMethod -Uri powershell.fun -UseBasicParsing

You are not restricted to the pre-loaded commands, though. PowerShell automatically loads modules on demand when you use a command that hasn’t been loaded into memory yet.

Implicitly Importing New Modules

There is no need for you to manually import modules or even know about them:

  • Cache: Details of all commands from all modules are kept in a cache file. Because of this cache, PowerShell always suggests all available commands to you even if their modules haven’t been loaded into memory yet.
  • Auto-Loading: Once you actually use a command, PowerShell determines whether the module that defines the command has been loaded into memory. If it wasn’t loaded yet, PowerShell automatically calls Import-Module for you. Imported modules typically stay in memory until you close PowerShell.

Discovering Commands and Modules

This command lists all PowerShell commands with the verb Get from all modules, including the ones that haven’t been loaded into memory yet:

Get-Command -Verb Get

As a beginner, it’s a good idea to focus on PowerShell commands with verb Get because these commands only read information and are safe to play with. There is no risk of accidentally changing settings or deleting things.

The column Source reveals the name of the Module that defines the command:

CommandType Name                         Version   Source
----------- ----                         -------   ------
...
Cmdlet      Get-Event                    3.1.0.0   Microsoft.PowerShell.Utility
Cmdlet      Get-EventLog                 3.1.0.0   Microsoft.PowerShell.Management
Cmdlet      Get-EventSubscriber          3.1.0.0   Microsoft.PowerShell.Utility
Cmdlet      Get-ExecutionPolicy          3.0.0.0   Microsoft.PowerShell.Security
Cmdlet      Get-FormatData               3.1.0.0   Microsoft.PowerShell.Utility
Cmdlet      Get-GPInheritance            1.0.0.0   GroupPolicy
Cmdlet      Get-GPO                      1.0.0.0   GroupPolicy
Cmdlet      Get-GPOReport                1.0.0.0   GroupPolicy
Cmdlet      Get-GPPermission             1.0.0.0   GroupPolicy
Cmdlet      Get-GPPrefRegistryValue      1.0.0.0   GroupPolicy
Cmdlet      Get-GPRegistryValue          1.0.0.0   GroupPolicy
Cmdlet      Get-GPResultantSetOfPolicy   1.0.0.0   GroupPolicy
Cmdlet      Get-GPStarterGPO             1.0.0.0   GroupPolicy
Cmdlet      Get-Help                     3.0.0.0   Microsoft.PowerShell.Core
...

If a module name starts with Microsoft.PowerShell. then it is shipping as part of PowerShell, and you can safely assume the module (and its commands) are available to anyone using the same PowerShell version that you use.

Any module with a different name is not part of PowerShell. It may be part of the operating system, a software installation, or installed from the PowerShell Gallery. If your scripts use commands from these modules, it is your job to identify these dependencies and make sure the modules (and commands inside of it) are available wherever you want to run your script.

When you want to use any one of the listed commands, there is no need to manually import modules. Just use them. PowerShell does all the rest for you.

Let’s dump the local Administrator accounts and show them in a dialog: Get-LocalGroupMember can read the members of a local user group, and Out-GridView shows data in a dialog, so here is a one-liner that shows all accounts in the local Administrators group:

Get-LocalGroupMember -SID S-1-5-32-544 | Out-GridView

The command immediately runs. No need for you to care much about modules.

Modules Ship Commands

To better understand the mechanisms behind the scenes, let’s manually identify the modules that ship these two commands. Start a new PowerShell, then run this code:

# get all commands (do NOT use parameter -Name)
Get-Command | 
  # pick the commands we want to investigate
  Where-Object { 'Get-LocalGroupMember', 'Out-GridView' -contains $_.Name } | 
  # add a calculated property "Loaded" which indicates whether the module was loaded into memory:
  Select-Object -Property Name, Source, @{N='Loaded';E={(Get-Module $_.Source) -ne $null}}

I am using Get-Command without any parameters and dump all commands, then use Where-Object to pick only the commands I am planning to use. Why not use the parameter -Name to pick the two commands immediately?

When you use the parameter -Name, Get-Command automatically loads the module for these commands to provide you with original information about the commands. When you avoid the parameter -Name, Get-Command will not load modules and instead takes command information from its internal cache.

As it turns out, Out-GridView is part of the module Microsoft.PowerShell.Utility which is preloaded. Get-LocalGroupMember ships with Microsoft.PowerShell.Management which is initially not loaded:

Name                 Source                             Loaded
----                 ------                             ------
Get-LocalGroupMember Microsoft.PowerShell.LocalAccounts  False
Out-GridView         Microsoft.PowerShell.Utility         True

Still, you can immediately run Get-LocalGroupMember.

Get-LocalGroupMember -SID S-1-5-32-544 | Out-GridView

PowerShell has automatically imported the module Microsoft.PowerShell.LocalAccounts for you:

Get-Module
ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Binary     1.0.0.0    Microsoft.PowerShell.LocalAccounts  {Add-LocalGroupMember, Disable-LocalUser, Enable...
Manifest   3.1.0.0    Microsoft.PowerShell.Management     {Add-Computer, Add-Content, Checkpoint-Computer,...
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type, Clear-Variable, Compare-O...
Script     2.0.0      PSReadline                          {Get-PSReadLineKeyHandler, Get-PSReadLineOption,...

Since the module Microsoft.PowerShell.LocalAccounts starts with Microsoft.PowerShell., you know that it ships as part of PowerShell, and you can safely use it in your scripts without having to worry about dependencies.

The module Microsoft.PowerShell.LocalAccounts was added in PowerShell 5. As long as your customers use at least PowerShell 5, you can trust that this module is available.

Module Auto-Import: Behind The Scenes

Let’s take a look at what PowerShell does behind the scenes when you run a command like Get-LocalGroupMember that wasn’t loaded before.

I am just illustrating the steps with individual PowerShell commands. Of course, in reality this is all done internally in one step.

Command Lookup

PowerShell looks up the command Get-LocalGroupMember in its internal cache:

Get-Command -Name Get-LocalGroupMember

It discovers that the command is shipped as part of the module Microsoft.PowerShell.LocalAccounts (see column Source):

CommandType Name                 Version Source
----------- ----                 ------- ------
Cmdlet      Get-LocalGroupMember 1.0.0.0 Microsoft.PowerShell.LocalAccounts

In classic shells and programming languages, you are responsible for importing libraries and extensions before you can use them. In PowerShell, this is done automatically.

This feature is driven by an internal command cache that lists all available commands, and the location of the modules providing these commands.

This cache can get corrupted or become stale, especially when you add new modules. If PowerShell fails to suggest commands correctly, you can force a cache rebuild and diagnose auto-discovery issues. Run this command:

Get-Module -ListAvailable -Refresh

This command rebuilds the command cache and re-examines all modules in all PowerShell default locations. The result is a folder listing including all default module folders. These folders are defined in the environment variable $env:PSModulePath.

$env:PSModulePath -split ';'

Make sure the listing contains the module you intended to use, and lists the command in the column ExportedCommands.

If your module isn’t included in the list, make sure it exists and is present in one of the default folders. If the module is listed but the command isn’t showing in ExportedCommands, make sure the ExecutionPolicy allows scripts to run, and the module isn’t corrupted.

If the module is listed, and the command shows in ExportedCommands, make sure there is no other module exporting a conflicting command with the same name as the one you intended to run.

Module Auto-Import

It then checks whether the module is already loaded into memory:

$loaded = (Get-Module -Name Microsoft.PowerShell.LocalAccounts) -ne $null
$loaded

If the module isn’t yet loaded, it figures out whether the module is available:

# find the module and its file location:
Get-Module -Name Microsoft.PowerShell.LocalAccounts -ListAvailable | Select-Object -ExpandProperty ModuleBase

If it is available, it then imports it explicitly into memory:

Import-Module -Name Microsoft.PowerShell.LocalAccounts

Adjusting And Disabling Module Auto-Loading

You can turn off module auto-loading by setting the variable $PSModuleAutoLoadingPreference to None:

# turn module autoloading off:
$PSModuleAutoLoadingPreference = 'None'
# command will not run (unless its module was loaded previously)
Get-LocalGroup
# turn module autoloading on again (default):
$PSModuleAutoLoadingPreference = 'All'
# command runs (PowerShell identifies and imports required module)
Get-LocalGroup

This preference variable was added for security reasons. Auto-loading PowerShell modules can be a security risk. After all, when a module is imported it can execute arbitrary PowerShell code.

With module auto-loading disabled, it is your responsibility to import all required modules via Import-Module. PowerShell will no longer import any module automatically.

You can also set the variable to ModuleQualified: modules are auto-loaded only when you run a module-qualified command (prepend the command name with the module name that defines the command):

# launch a fresh powershell (replace with pwsh.exe for PowerShell 7)
powershell.exe -noprofile
# enable module-qualified auto-import only:
$PSModuleAutoLoadingPreference = 'ModuleQualified'
# plain commands will not auto-import modules:
Get-LocalGroup
# module-qualified commands will auto-import:
Microsoft.PowerShell.LocalAccounts\Get-LocalGroup

Explicitly Importing Modules

Most of the time, PowerShell is importing modules automatically. There are five scenarios, though, where running Import-Module manually can be helpful or even required:

  • Refresh Module: If you are authoring your own PowerShell modules, you may want to freshly re-import an already imported module, i.e. when you changed code inside of it. Use the parameter -Force to force a fresh import:

    # freshly re-import an already imported module because you changed its code:
    Import-Module -Name MyLittleModule -Force
    
  • Load From Custom Location: PowerShell auto-imports modules only from known module folders. If the module isn’t located in one of the folders listed in $env:PSModulePath, use Import-Module and provide the path to the module instead of its name:

    # import a module from a non-default file location, i.e. from a USB stick:
    Import-Module -Name 'd:\mytools\MyLittleModule'
    
  • Ensure Loaded: Module auto-import is triggered by command usage. PowerShell modules can contain more than commands, though. Some PowerShell modules include PowerShell providers. A provider implements drives. For example, the optional Microsoft module ActiveDirectory ships with a provider that implements the new PowerShell drive AD: to traverse the Active Directory structure. Before you can use the drive, you need to import the module. Either use one of its commands to trigger implicit loading, or use Import-Module:

    # import module to load contained providers:
    Import-Module -Name ActiveDirectory # (make sure you installed the module, it is optional)
    # drive AD: now available:
    Get-ChildItem -Path 'AD:\'
    
  • Diagnose Correct Module Loading: If you suspect there is something wrong with a module, import it manually and enable verbose and debug output:

    # manually load module and enable verbose and debug output:
    # (this invokes the command in a scriptblock so $DebugPreference won't change globally)
    & { $DebugPreference = 'Continue'; Import-Module -Name Microsoft.PowerShell.Utility -Force -Verbose }
    

    The output lists all commands imported by the module, plus potentially other information that may be useful to investigate issues:

    VERBOSE: Loading module from path 'C:\Windows\system32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utili
    ty.psd1'.
    VERBOSE: Importing cmdlet 'New-Object'.
    VERBOSE: Importing cmdlet 'Measure-Object'.
    ...
    
  • Auto-Import Disabled: If you have disabled module auto-import for security reasons (by setting the variable $PSModuleAutoLoadingPreference to None) you must use Import-Module and import all modules manually that you intend to use.

Module Location

Let’s take a look at all PowerShell commands available for your PowerShell environment, and add the information about the module that defines these commands:

# calculated property:
# get the base path for a module
$basePath = @{
  Name = 'Path'
  Expression = { $_.Module.ModuleBase }
}

# get all PowerShell commands...
Get-Command -CommandType Function, Cmdlet |
  # ...that are defined by a module...
  Where-Object ModuleName |
  # ...and output command name, type, module name, and module location:
  Select-Object -Property Name, CommandType, ModuleName, $basePath

The output reports the commands, command types, and the module from which they will be loaded when you use the commands:

Name                                       CommandType ModuleName                             Path
----                                       ----------- ----------                             ----
...
Revoke-SPOTenantServicePrincipalPermission      Cmdlet Microsoft.Online.SharePoint.PowerShell C:\Users\tobia\OneDrive\Dokumente\WindowsPo...
Revoke-SPOUserSession                           Cmdlet Microsoft.Online.SharePoint.PowerShell C:\Users\tobia\OneDrive\Dokumente\WindowsPo...
Save-CauDebugTrace                              Cmdlet ClusterAwareUpdating                   C:\Windows\system32\WindowsPowerShell\v1.0\...
Save-Help                                       Cmdlet Microsoft.PowerShell.Core
Save-Package                                    Cmdlet PackageManagement                      C:\Program Files\WindowsPowerShell\Modules\...
Save-ShieldedVMRecoveryKey                      Cmdlet ShieldedVMDataFile                     C:\Windows\system32\WindowsPowerShell\v1.0\...
Save-VolumeSignatureCatalog                     Cmdlet ShieldedVMDataFile                     C:\Windows\system32\WindowsPowerShell\v1.0\...
Save-WindowsImage                               Cmdlet Dism                                   C:\Windows\system32\WindowsPowerShell\v1.0\...
Search-ADAccount                                Cmdlet ActiveDirectory                        C:\Windows\system32\WindowsPowerShell\v1.0\...
Select-Object                                   Cmdlet Microsoft.PowerShell.Utility           C:\Windows\System32\WindowsPowerShell\v1.0
Select-String                                   Cmdlet Microsoft.PowerShell.Utility           C:\Windows\System32\WindowsPowerShell\v1.0
Select-Xml                                      Cmdlet Microsoft.PowerShell.Utility           C:\Windows\System32\WindowsPowerShell\v1.0
Send-AppvClientReport                           Cmdlet AppvClient                             C:\Windows\system32\WindowsPowerShell\v1.0\...
...

The module Microsoft.PowerShell.Core is the only module in this list that does not report a file location. This module is hardcoded into PowerShell and always loaded when PowerShell launches. The commands in this module are vital to PowerShell:

Get-Command -Module Microsoft.Powershell.Core

Default Module Locations

PowerShell is monitoring all folders listed in this environment variable:

$env:PSModulePath -split ';'

By default, this variable contains three folders:

C:\Users\tobia\OneDrive\Dokumente\WindowsPowerShell\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\Windows\system32\WindowsPowerShell\v1.0\Modules

The first path holds all modules installed per user and is located in your user profile. The second path stores all modules installed for all users and is located inside the default location for installed 3rd party software. The last folder keeps all internal Microsoft modules and is off limits for anyone else.

Custom Module Locations

Even though you can edit the folder list in $PSModulePath and add more locations, this is strongly discouraged:

  • Per User, All User: Sometimes, 3rd party software decide to install PowerShell modules in completely separate custom locations. Only why? Typically, PowerShell modules are installed either per user or for all users, just like any other type of software. The default module folders enable you to do just that. There is really no reason for adding additional module locations.

  • Damages To Default Folders: Occasionally, software publishers damage the default locations while trying to add their custom folders. Typically, this occurs when PowerShell modules are shipped as MSI packages. For example, when an MSI package accidentally replaced the path to your user profile with a custom location (rather than adding that custom location), any module installed per user becomes suddenly unavailable.

  • Slow Network, Slow PowerShell: Network shares and UNC paths are frequently abused as a convenient central module store. While technically possible, this slows down PowerShell command discovery and command completion, and can add hefty network traffic:

    PowerShell visits the folders listed in $env:PSModulePath very frequently, i.e. whenever you request command completion, and unless you have a very fast network and file server, you start to see strange progress bars while waiting for command completion results.

    PowerShell modules should strictly be stored in local folders: push them to one of the local default module folders in $env:PSModulePath using classic software distribution, or pull them from central repositories using Install-Module.

If you see more or less (or different) folders than the default folders in $env:PSModulePath, you should rethink and try and make sure all PowerShell modules are stored in the default folders.

If you must import modules from non-default places, use Import-Module and submit the path to the module folder. This call can then be added to your profile script in $profile to automatically load such modules.

What’s Next

Now that you know the basics about PowerShell modules, let’s take a look at some of the best sources for new free PowerShell modules, and how to install, update, and remove additional PowerShell modules.