Table of Contents
Introduction
Currently, I am not using PowerShell so often anymore. Before my knowledge is sinking too deep, I try to document some things I did in the past and try to remember what I learned back then. At the time, I had a certain approach to writing solutions for different problems and used certain patterns and methods. I am not sure what the quality of this approach is, but it helped me to avoid naming collisions and easily print out the modules and functions from a particular custom group of modules. So, this post helps organizating PowerShell modules with commandprefixes.
In short, this post explores a way to setup a method of organizing a collection of PowerShell modules. It attempts to prevent naming collisions and therefore uses CommandPrefixes. What I wanted to achieve is to distinguish my own Scripts from the great pile of possible invocations, and to automate my workflow a little when creating new Scripts. I wanted to keep a clear overview and structured workflow.
Considerations
As I was writing this post, I noticed I had to refresh my knowledge on how PowerShell operates. I try to sum up what I found here:
Auto-loading
- Modules that are stored in a path defined in the $env:PSModulePath variable qualify to be auto-loaded.
- Auto-loading only works when a naming convention is followed: The directory name and the module name need to be equal.
- When a module is auto-loaded, modules in sub-directories of the auto-loaded script, although with the correct naming convention will not be auto-loaded. When the parent naming convention is disbanded, the auto-loading for the sub-directory seems to work again.
- When a module is auto-loaded, the exported function names become “visible”, but the module is only actually loaded until a function is invoked/called. This can be seen as lazy loading.
- When a Profile is used, it could have loaded modules which become available in your PowerShell session.
- When a file is auto-loaded and you try to import a module with the same name outside a $env:PSModulePath, it will not override the existing auto-loaded file. This is also a form of a naming collision.
- Under other circumstances the latest loaded module with a specific name will be the one that is loaded into the session.
CommandPrefixes
For example, let’s say we have Manifest file, which loads a Root module. The Root module then loads ModuleA.psm1 from a directory ModuleA and is therefore auto-loaded. The Root module also loads ModuleB.psm1 which does not follow the naming convention for auto-loading and is therefore not placed in a folder with the same name.
Both are loaded with CommandPrefixes, which is set in the manifest file. The manifest and root file are both in a $env:PSModulePath directory.
The content of the ModuleA and ModuleB file are as follows:
# .\ModuleA\ModuleA.psm1
function Get-ModuleA {
[CmdletBinding()]
param( )
Write-Host "ModuleA"
}
Export-ModuleMember -Function Get-ModuleA
PowerShell# .\B\ModuleB.psm1
function Get-ModuleB {
[CmdletBinding()]
param( )
Write-Host "ModuleB"
}
Export-ModuleMember -Function Get-ModuleB
PowerShell- Invoking “Get-ModuleA” works out of the box in this case, for it is auto-loaded.
- Invoking “Get-ModuleB” does not work, it has not been auto-loaded and needs to be imported first.
I gave the Root file a function which loads other modules, called Initialize-Modules. Since the manifest was loaded, we have to call the function with the CommandPrefix included (which prevents naming collisions).
# Now we import and initialize
Import-Module .\Manifest.psd1
Initialize-CGModules -Verbose # where CG is the prefix
PowerShell- Invoking Get-ModuleA, Get-CGModuleA, Get-CGModuleB all work.
- Invoking Get-ModuleA from ModuleB works, because ModuleA is auto-loaded.
- Invoking Get-ModuleB from ModuleA does not work, because ModuleB was not loaded.
- Invoking Get-CGModuleB from ModuleA works. (because, ModuleB is loaded with a prefix)
When you are not certain what the prefix will be (in the future), I would not recommend to hard-code the prefix with function calls. The solution to this, could be to let ModuleA import ModuleB itself so it can use the original function name. Or you could create a convention where certain modules will always be loaded without prefixes. If you choose to make modules load each other, it would be good to create another convention where for example importing goes a certain direction (like up or down the directory structure), to prevent circular dependencies.
A method of organization
This following is a custom method of organization.
In support of the image above, the Manifest file will import the Root module with a CommandPrefix. The Root.psm1 file is programmed to load underlying modules with or without the prefix provided in the Manifest file.
The structure has three directories:
- Global: Add modules here that could be auto-loaded when in a $env:PSModulePath. Module functions will not get prefixed. Both modules from Helpersand Modules directory can use these modules.
- Modules: Will be prefixed and should not auto-load to prevent naming collisions.
- Helpers (was Dependencies before): Modules can import helper function, not vice versa. Should also not auto-load.
If two modules from the Modules directory need functionality from each other, it should be placed into a helper directory instead. Helpers should only load when used.
Naming conventions
To build and arrange things in a nice and clear way, there are naming guidelines to consider. Microsoft encourages naming conventions for naming modules. The list of verbs can also be requested from within PowerShell:
Get-Verb
PowerShellBecause one of the main goals was to avoid naming collisions, we can import modules from Helpers with suppressed feedback from conventions, e.g.:
# Example
Import-Module -Name MyModule.psm1 -DisableNameChecking
PowerShellYou could now choose to write function names in the Helpers that do not follow the naming conventions without getting PowerShell pointing it out to you.
Create a manifest file to set the CommandPrefix
A manifest file stores metadata about your files. To do so, a PS terminal window can be opened and navigate to the path where you want to store your PowerShell files:
New-ModuleManifest -Path .\Manifest.psd1 -RootModule .\Root.psm1
PowerShellThis creates a manifest file where you can set (at least) the following variables:
# Script module or binary module file associated with this manifest. RootModule = '.\Root.psm1' # ... # Default prefix for commands exported from this module. Override the default # prefix using Import-Module -Prefix. DefaultCommandPrefix = 'YourPrefix' # example prefix
What this manifest file does is using the prefix you provided in every function call in the Root.psm1 file. This will be more clear further down.
Create a Root module
In this example the name of the root module the manifest refers to is called Root.psm1.
<#
.SYNOPSIS
Root module (Root.psm1)
.DESCRIPTION
.INPUTS
.OUTPUTS
.EXAMPLE
.LINK
.NOTES
#>
function Initialize-Modules {
BEGIN {
Write-Host "Initialize-Modules"
}
PROCESS {
# Manual module loading code here, it should manually add the prefix set in the Manifest
}
END {}
}
PowerShellWhat the Command Prefix does
What the command prefix does is alter the function names in a module to avoid naming collisions.
# A function named:
Initialize-Modules
# Will now be invokeable with: (<PREFIX> is the DefaultCommandPrefix provided in the Manifest)
Initialize-<PREFIX>Modules
PowerShellModules which are imported with a prefix, must be invoked with the prefix. (Unless they’re also auto-loaded, then they work with and without prefix)
A few Examples
Lets try the following, to import the module use:
Import-Module .\Root.psm1
PowerShellNow:
# this will work:
Initialize-Modules
# this will not work: (the manifest is not used/imported)
Initialize-CGModules
PowerShellNow close your PowerShell terminal to unload the module (or use Remove-Module). Start the PowerShell terminal again and navigate to your directory where the files are at. Test that the module was unloaded by trying to invoke Initialize-Modules again, this should not work. This time we’re going to invoke the manifest file:
Import-Module .\Manifest.psd1
PowerShellNow the reverse will work:
# this will not work:
Initialize-Modules
# this will work: (where DefaultCommandPrefix = 'CG')
Initialize-CGModules
PowerShellThe manifest file has prefixed the function with CG, this is the DefaultCommandPrefix I set in the Manifest.
Enforcing the Command Prefix
To make sure a module is loaded with a prefix, you can check if it has been set, and use the return value in an if statement:
function Get-CommandPrefix {
$mod = Get-Module -Name $MyInvocation.MyCommand.Module.Name
if ($mod.Prefix -eq $null -or $mod.Prefix -eq '') {
return $False
}
return $mod.Prefix
}
PowerShellConsiderations with this setup
Using the prefix hard-coded
If you are creating a new module and you use this methodology of loading modules with a prefix, you could then use prefixed function calls everywhere instead of the original function call without the prefix.
For example, if the Root module (loaded by the Manifest file) loads ModuleB with a prefix set in the Manifest, then ModuleB could use the prefixed versions of the Root module, without importing the Root module first. The problem with this, is that if you ever want to change the name of the DefaultCommandPrefix, none of the function calls will work.
Example, pseudo code:
# Root.psm1: prefix in manifest is: Tools
function Initialize-Modules {
Import-Module ModuleB -Prefix "Tools"
}
function Write-Success {} # does something
Export-ModuleMember -Function Initialize-Modules
Export-ModuleMember -Function Write-Success
PowerShellIn ModuleB, we can hard-code the Tools prefix in a function call:
# ModuleB.psm1:
function Start-ModuleBFunction {
# This (a hard-coded prefix) is now possible, but breaks if the prefix changes
Write-ToolsSuccess "Task completed"
}
PowerShellWhile this is possible, it will only work if you make sure that your prefix does not change. Generally, importing modules within modules is not considered a good practice.
Auto-loading
The $env:PSModulePath variable displays the paths on your system where PowerShell will automatically load modules with exported module members (functions). Auto-loading will only happen if the module name has the exact same name as the directory it is in. When a module is auto-loaded, there is no need to use Import-Module explicitly.
In this case, the Root.psm1 file is not in a directory called Root, thus it does not auto-load. Auto-loading does not seem to work to sub-directories of modules that are already auto-loaded.
Overview
- Inside a $env:PSModulePath, modules with the same name as their parent directory auto are auto-loaded when a function is also being exported with Export-ModuleMember.
- Auto-loading does not apply to sub-directories of where an auto-loaded file is present. Adding these sub-directories explicitly to the $env:PSModulePath can fix this.
- Function names that have the same name as an existing function will be preferred over the existing one (for this session). When adding the prefix, the official function name from the overridden module is accessible again.
Command (pseudo) | In A $env:PSModulePath | Outside a $env:PSModulePath |
---|---|---|
Import-Module Manifest.psd1 followed by: Root -> Initialize-<Prefix>Modules | – Auto-loaded modules work. – Explicitly imported modules will work with the prefix provided. | – Only explicitly imported modules will work with the prefix provided. – Global modules will import without prefixes. |
Import-Module Root.psm1 followed by: Root -> Initialize-Modules | – Auto-loaded modules work. – Explicitly imported modules will work, without prefix per default. | – Explicitly imported modules will work, without prefix per default. |
(No command) | – Only auto-loaded modules will work. (Provided that they have been setup by the naming convention, as usual) – No prefixes are used. | – Nothing works out of the box. |
Conclusion
While I am not sure how useful this approach is or to what extend it is a good practice, it served me in the past to keep overview and have a clear structure. Maybe it is best used for smaller, personal projects.
An example of the implementation can be found on GitHub.
# Test script, use before anything else
Invoke-pester .\Root.Tests.ps1
# Import the manifest (the prefix helps to prevent naming collisions)
Import-Module .\Manifest.psd1
# Initialize all modules
Initialize-CGModules
# Print out modules and functions (helps to keep an overview)
Get-CGModuleList
PowerShell# Output of Get-CGModuleList
Module Manifest has functions:
- Get-CGCommandPrefix
- Get-CGHelperList
- Get-CGGlobalList
- Get-CGModuleList
- Initialize-CGModules
- Remove-CGModules
- Restart-CGInitialization
Module Write has functions:
- Write-Alert
- Write-Error
- Write-Info
- Write-Neutral
- Write-Status
- Write-Success
Module Modules has functions:
Module SearchInMSWord has functions:
- Search-CGInWordDoc
Module Unzip has functions:
- Expand-CGZipFiles
Module SystemWakeUp has functions:
- Disable-CGSystemWake
Module Winget has functions:
- Initialize-CGWinget
- Install-CGWithWinget
In the post about preventing Windows 11 from waking up from hibernation on its own, I added the script to prevent this:
# Use is at your own responsibility
Disable-CGSystemWake
PowerShell