What is a PowerShell Module?

Put briefly, a PowerShell module — similar to a Python module — is a way to organize a collection of related code, variables, aliases, etc. in a contained workspace.
There are two types of PowerShell modules:
- Script modules (PowerShell scripts)
- Binary modules (compiled DLLs)
Module Loading Paths
If you inspect this PowerShell environment variable, you can see from which folders your PowerShell modules will be loaded from.
$env:PSModulePath
If you install a module from PowerShell Gallery, you can use the Scope
parameter to determine which folder in $env:PSModulePath
the module will be downloaded.
# Local User
# Install the module to C:\Users\user.name\Documents\WindowsPowerShell\Modules\ on Windows
# Install the module to /home/user.name/.local/share/powershell/Modules on Linux
Install-Module -Name <module-name> -Scope CurrentUser
# Global Module
# Install the module to C:\Program Files\WindowsPowerShell\Modules on Windows
# Install the module to /usr/local/share/powershell/Modules on Linux
Install-Module -Name <module-name>
Why mention this?
Because, you can place your module on the operating system using this same hierarchy. If you want the module to be available globally to all users on the system, use the appropriate directory; and vice-versa for local user scope only.
When to Create a Module
In past experiences — for me at least — modules have usually been created due to the following conditions:
- Create a script for a distinct purpose or project
- Additional scripts continue to be added
- Adding helper scripts
- Or, extending the scope of the project
- The project scope continues to grow, we need a place to keep things organized
Scaffolding the Module
Module Directory Structure
ModuleName
|___ModuleName.psd1
|___ModuleName.psm1
|___Private
| |___ps1
| |___Verb-Noun.ps1
| |___Verb-Noun.ps1
|
|___Public
|___ps1
|___Verb-Noun.ps1
|___Verb-Noun.ps1
ModuleName
— Name the module after the project or its intended purposeModuleName.psd1
— This is the module manifest, we'll cover this laterModuleName.psm1
— This is your script module, we'll cover this later as wellPrivate
— This is the directory to store your private functions and variables (eg. helper functions)ps1
— The PowerShell functions are stored here and are not directly referenced by the userVerb-Noun.ps1
— the script file containing the private PowerShell function.
Public
— This is directory to store your public functions and variablesps1
— The PowerShell function code is stored here and these are the cmdlets the user will be directly runningVerb-Noun.ps1
— the script file containing the public PowerShell function
Commands to Create the Module Structure
# Variables
$year = (Get-Date).Year
$moduleName = 'TestModule'
$templateString = 'Test Module'
$version = '1.0'
# Create the "TestModule" top-level directory
New-Item -ItemType Directory -Name $moduleName
# Create subdirectories
# TestModule
# |___ ...
# |___ ...
# |___Private
# | |___ps1
# |___ ...
New-Item -Path "$PWD\$moduleName\Private\ps1" -ItemType Directory -Force
# Create subdirectories
# TestModule
# |___ ...
# |___ ...
# |___ ...
# |___Public
# |___ps1
New-Item -Path "$PWD\$moduleName\Public\ps1" -ItemType Directory -Force
# Create the script module
# TestModule
# |___ ...
# |___ TestModule.psm1
New-Item -Path "$PWD\$moduleName\$moduleName.psm1" -ItemType File
# Create the module manifest
# TestModule
# |___TestModule.psd1
# |___ ...
$moduleManifestParameters = @{
Path = "$PWD\$moduleName\$moduleName.psd1"
Author = $templateString
CompanyName = $templateString
Copyright = "$year $templateString by Benjamin Heater"
ModuleVersion = $version
Description = $templateString
RootModule = "$moduleName.psm1"
}
New-ModuleManifest @moduleManifestParameters
Inspecting the Manifest
ModuleName
|___ModuleName.psd1
|___ ...
|___ ...
Get-Content "$PWD\TestModule\TestModule.psd1"`
Example Module Manifest
#
# Module manifest for module 'TestModule'
#
# Generated by: Test Module
#
# Generated on: 8/17/2023
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'TestModule.psm1'
# Version number of this module.
ModuleVersion = '1.0'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = '147cfba3-3c6d-4260-aeca-f99aa900cc0c'
# Author of this module
Author = 'Test Module'
# Company or vendor of this module
CompanyName = 'Test Module'
# Copyright statement for this module
Copyright = '2023 Test Module by Benjamin Heater'
# Description of the functionality provided by this module
Description = 'Test Module'
# Minimum version of the Windows PowerShell engine required by this module
# PowerShellVersion = ''
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = '*'
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = '*'
# Variables to export from this module
VariablesToExport = '*'
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = '*'
# DSC resources to export from this module
# DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
# Tags = @()
# A URL to the license for this module.
# LicenseUri = ''
# A URL to the main website for this project.
# ProjectUri = ''
# A URL to an icon representing this module.
# IconUri = ''
# ReleaseNotes of this module
# ReleaseNotes = ''
} # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''
}
When we ran the New-ModuleManifest
command before, PowerShell added our inputs to a .psd1
file with the above template.
The module manifest is responsible for declaring some important things about our module, including:
- The
RootModule
script (eg.TestModule.psm1
) - The
GUID
to uniquely identify the module FunctionsToExport
for function names that should be executable by the userCmdletsToExport
for compiled cmdlets that should be executable by the userVariablesToExport
for any module-specific variablesAliasesToExport
for any aliases defined in your functions or cmdlets- And, many more configurations

Inspecting the Script Module
ModuleName
|___ ...
|___ModuleName.psm1
|___ ...
Get-Content "$PWD\TestModule\TestModule.psm1"
Script Module I've Created for All My Modules
$directorySeparator = [System.IO.Path]::DirectorySeparatorChar
$moduleName = $PSScriptRoot.Split($directorySeparator)[-1]
$moduleManifest = $PSScriptRoot + $directorySeparator + $moduleName + '.psd1'
$publicFunctionsPath = $PSScriptRoot + $directorySeparator + 'Public' + $directorySeparator + 'ps1'
$privateFunctionsPath = $PSScriptRoot + $directorySeparator + 'Private' + $directorySeparator + 'ps1'
$currentManifest = Test-ModuleManifest $moduleManifest
$aliases = @()
$publicFunctions = Get-ChildItem -Path $publicFunctionsPath | Where-Object {$_.Extension -eq '.ps1'}
$privateFunctions = Get-ChildItem -Path $privateFunctionsPath | Where-Object {$_.Extension -eq '.ps1'}
$publicFunctions | ForEach-Object { . $_.FullName }
$privateFunctions | ForEach-Object { . $_.FullName }
$publicFunctions | ForEach-Object { # Export all of the public functions from this module
# The command has already been sourced in above. Query any defined aliases.
$alias = Get-Alias -Definition $_.BaseName -ErrorAction SilentlyContinue
if ($alias) {
$aliases += $alias
Export-ModuleMember -Function $_.BaseName -Alias $alias
}
else {
Export-ModuleMember -Function $_.BaseName
}
}
$functionsAdded = $publicFunctions | Where-Object {$_.BaseName -notin $currentManifest.ExportedFunctions.Keys}
$functionsRemoved = $currentManifest.ExportedFunctions.Keys | Where-Object {$_ -notin $publicFunctions.BaseName}
$aliasesAdded = $aliases | Where-Object {$_ -notin $currentManifest.ExportedAliases.Keys}
$aliasesRemoved = $currentManifest.ExportedAliases.Keys | Where-Object {$_ -notin $aliases}
if ($functionsAdded -or $functionsRemoved -or $aliasesAdded -or $aliasesRemoved) {
try {
$updateModuleManifestParams = @{}
$updateModuleManifestParams.Add('Path', $moduleManifest)
$updateModuleManifestParams.Add('ErrorAction', 'Stop')
if ($aliases.Count -gt 0) { $updateModuleManifestParams.Add('AliasesToExport', $aliases) }
if ($publicFunctions.Count -gt 0) { $updateModuleManifestParams.Add('FunctionsToExport', $publicFunctions.BaseName) }
Update-ModuleManifest @updateModuleManifestParams
}
catch {
$_ | Write-Error
}
}
I use the script module as an initialization script for when the module is first imported. Looking at the code from top to bottom, I'll briefly summarize what the script module is doing.
- Dynamically determine the module name using
$PSScriptRoot
- Assume the
.psd1
file shares the same name as the parent directory
(the.psd1
is invoking this.psm1
script module) - Pull the current module manifest using
Test-ModuleManifest
, in order to compare and update things as necessary - Discover all
.ps1
files in private and public directories and source them in - Loop over each public function and determine if any aliases are defined and export them as module members
- Do some comparisons against the current module manifest to see if any functions or aliases have been added or removed, and if so, update the module manifest
.psm1
script module if there are things you'd like your module to do when it's first imported.Adding Code to the Module
ModuleName
|___ ...
|___ ...
|___Private <--- May not be referenced by user
| |___ps1 <--- Script files
| |___Verb-Noun.ps1 <--- Private function 1
| |___Verb-Noun.ps1 <--- Private function 2
| |___etc.
|
|___Public <--- May be referenced by user
|___ps1 <--- Script files
|___Verb-Noun.ps1 <--- Public function 1
|___Verb-Noun.ps1 <--- Public function 2
|___etc.
The primary reason for breaking the code up into Private
and Public
folders is to keep specific things immutable to the user. We may have private variables and functions that aid in the overall execution of specific tasks, and should not be referenced, changed, or executed by the user.
Adding a Private Function
We'll add the private function first for clarity's sake. Keep in mind that private functions are generally just helpers, and not directly called by a user.
ModuleName
|___ ...
|___ ...
|___Private
| |___ps1
| |___Confirm-CoffeeTime.ps1
| |___ ...
|
|___ ...
function Confirm-CoffeeTime {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateSet('Regular', 'Decaf')]
[String]$CoffeeType
)
begin {
$currentDateTime = Get-Date
$regularCoffeeHours = 4..20
$decafCoffeeHours = 0..3 + 21..23
}
process {
if ($CoffeeType -eq 'Regular') {
if ($currentDateTime.Hour -in $regularCoffeeHours) {
$result = $true
}
else {
$result = $false
}
}
else {
if ($currentDateTime.Hour -in $decafCoffeeHours) {
$result = $true
}
else {
$result = $false
}
}
}
end {
if ($result -eq $true) {
return "Enjoy your cup of $CoffeeType coffee!"
}
else {
throw "You may not drink $CoffeeType right now!"
}
}
}
Confirm-CoffeeTime
function to the user. We simply want the user to run Get-Coffee
(see below) and let this function validate the results.Adding a Public Function
ModuleName
|___ ...
|___ ...
|___ ...
|
|___Public
|___ps1
|___Get-Coffee.ps1
|___ ...
function Get-Coffee {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet('Regular', 'Decaf')]
[String]$CoffeeType
)
begin {}
process {
try {
# Call the private function to confirm coffee time
$confirmation = Confirm-CoffeeTime -CoffeeType $CoffeeType
Write-Host $confirmation -ForegroundColor Green
}
catch {
throw $_
}
}
end {}
}
Importing and Using the Module
What we should expect to happen:
- Import the module
- Run
Get-Command -Module <module_name>
- Observe that we have only one user-executable command
Import-Module .\TestModule
Get-Command -Module TestModule


Perfect! Our public Get-Coffee
function is performing as expected — calling the private Confirm-CoffeeTime
function and returning stdout
or stderr
.
Applying this to Real Projects
As mentioned before, the examples above are contrived, and not great at demonstrating how a PowerShell module could be used for real projects. Some examples of how you might create a PowerShell module:
- API clients
- CTF tools
- Hacking tools
- Forensic tools
- Diagnostic tools
- Etc.
Creating a duplicate PowerShell module as a learning experience is absolutely fine. However, if there's a module on GitHub or the PS Gallery that meets your needs, use that and consider contributing to the source code if it's open source.
If you see an existing module, but aren't satisfied with it for any reason, that would absolutely be a perfect opportunity to create your own solution.
Some Modules I've Written
Wrapping Up
I want to emphasize that this post is a demonstration of the way that I create PowerShell modules. It is not the only way to create PowerShell modules.
That said, I do hope that I gave you some ideas on how to get started with creating your own modules, and how you can keep your related code organized.