In our previous article, we explored the fundamentals of PowerShell modules – what they are, why to use them, and how to create basic modules. Now, let’s dive deeper into advanced module development techniques, best practices for keeping your code clean and structured, and professional-grade module development workflows.
This continuation will help you transform from a module user to a module architect, capable of building robust, maintainable, and professional PowerShell modules.
What We’ll Cover
In this advanced guide, we’ll explore:
- Module Structure and Organization: Best practices for organizing complex modules
- Advanced Module Features: Parameter sets, advanced functions, and validation
- Documentation: Creating professional documentation
- Real-World Examples: Building a complete module from scratch
Module Structure and Organization
A well-structured module is the foundation of maintainable code. Let’s explore professional module organization patterns.
Recommended Folder Structure
MyModule/
├── MyModule.psd1 # Module manifest
├── MyModule.psm1 # Main module file
├── README.md # Documentation
├── CHANGELOG.md # Version history
├── Public/ # Public functions (exported)
│ ├── Get-MyData.ps1
│ └── Set-MyConfiguration.ps1
├── Private/ # Private helper functions
│ ├── ConvertTo-MyFormat.ps1
│ └── Test-MyConnection.ps1
└── docs/ # Additional documentation (optional)
└── examples.md
# Let's create a sample advanced module structure
$ModuleName = "AdvancedStarTrekUtils"
$ModulePath = "$HOME\Documents\PowerShell\Modules$ModuleName"
# Create the main module directory
New-Item -ItemType Directory -Path $ModulePath -Force
# Create subdirectories
$Directories = @('Public', 'Private')
foreach ($Dir in $Directories) {
New-Item -ItemType Directory -Path "$ModulePath$Dir" -Force
}
# Create optional docs directory
$DocsPath = "$ModulePath\docs"
if (-not (Test-Path $DocsPath)) {
New-Item -ItemType Directory -Path $DocsPath -Force
Write-Host "📁 Created optional docs directory" -ForegroundColor Yellow
}
Write-Host "Module structure created at: $ModulePath" -ForegroundColor Green
Get-ChildItem -Path $ModulePath -Recurse | Where-Object { $_.PSIsContainer } | ForEach-Object {
Write-Host "📁 $($_.FullName.Replace($ModulePath, '.'))" -ForegroundColor Cyan
}
Creating the Module Manifest (Advanced)
The module manifest is the heart of your module. Let’s create a comprehensive manifest with advanced features.
# Create an advanced module manifest
$ManifestParams = @{
Path = "$ModulePath$ModuleName.psd1"
RootModule = "$ModuleName.psm1"
ModuleVersion = '1.0.0'
GUID = [System.Guid]::NewGuid().ToString()
Author = 'Casper Stekelenburg'
CompanyName = 'Starfleet Command'
Copyright = '(c) 2025 Starfleet. All rights reserved.'
Description = 'Advanced utilities for Star Trek operations and fleet management'
PowerShellVersion = '5.1'
# Functions to export - we'll populate this dynamically
FunctionsToExport = @()
# Cmdlets and variables (if any)
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @()
# Tags for PowerShell Gallery
Tags = @('StarTrek', 'Utilities', 'Fleet', 'Management')
# Links
ProjectUri = 'https://github.com/starfleet/AdvancedStarTrekUtils'
LicenseUri = 'https://github.com/starfleet/AdvancedStarTrekUtils/blob/main/LICENSE'
IconUri = 'https://github.com/starfleet/AdvancedStarTrekUtils/blob/main/icon.png'
# Release notes
ReleaseNotes = @'
## Version 1.0.0
- Initial release
- Fleet management functions
- Ship status monitoring
- Crew management utilities
'@
# Dependencies
RequiredModules = @()
# Minimum .NET version
DotNetFrameworkVersion = '4.7.2'
}
New-ModuleManifest @ManifestParams
Write-Host "Advanced module manifest created!" -ForegroundColor Green
Advanced Function Design Patterns
Modern PowerShell modules benefit from well-designed functions with advanced parameter sets, validation, and error handling.
# Create an advanced function with parameter sets
$AdvancedFunctionContent = @'
function Get-StarTrekShipInfo {
<#
.SYNOPSIS
Gets information about Star Trek ships with multiple parameter sets.
.DESCRIPTION
This function demonstrates advanced parameter sets, allowing users to search
for ships by different criteria while maintaining a clean interface.
.PARAMETER Name
The name of the ship to search for.
.PARAMETER Registry
The registry number to search for.
.PARAMETER Class
The ship class to filter by.
.PARAMETER Status
The operational status to filter by.
.PARAMETER All
Returns all ships in the database.
.EXAMPLE
Get-StarTrekShipInfo -Name "Enterprise"
Gets information about ships named Enterprise.
.EXAMPLE
Get-StarTrekShipInfo -Registry "NCC-1701"
Gets information about the ship with registry NCC-1701.
.EXAMPLE
Get-StarTrekShipInfo -Class "Constitution" -Status "Active"
Gets all active Constitution-class ships.
#>
[CmdletBinding(DefaultParameterSetName = 'All')]
[OutputType([PSCustomObject])]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'ByName', Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'ByRegistry')]
[ValidatePattern('^NCC-\d+$')]
[string]$Registry,
[Parameter(ParameterSetName = 'ByClass')]
[Parameter(ParameterSetName = 'ByStatus')]
[ValidateSet('Constitution', 'Galaxy', 'Sovereign', 'Intrepid', 'Defiant', 'Miranda', 'Excelsior')]
[string]$Class,
[Parameter(ParameterSetName = 'ByStatus')]
[Parameter(ParameterSetName = 'ByClass')]
[ValidateSet('Active', 'Inactive', 'Destroyed', 'Missing', 'Under Construction')]
[string]$Status,
[Parameter(ParameterSetName = 'All')]
[switch]$All
)
begin {
Write-Verbose "Starting ship information retrieval using parameter set: $($PSCmdlet.ParameterSetName)"
# Sample ship database (in a real module, this might come from a file or API)
$script:ShipDatabase = @(
@{ Name = 'Enterprise'; Registry = 'NCC-1701'; Class = 'Constitution'; Status = 'Active'; Captain = 'James T. Kirk'; CrewCount = 430 }
@{ Name = 'Enterprise'; Registry = 'NCC-1701-A'; Class = 'Constitution'; Status = 'Active'; Captain = 'James T. Kirk'; CrewCount = 430 }
@{ Name = 'Enterprise'; Registry = 'NCC-1701-D'; Class = 'Galaxy'; Status = 'Destroyed'; Captain = 'Jean-Luc Picard'; CrewCount = 1014 }
@{ Name = 'Enterprise'; Registry = 'NCC-1701-E'; Class = 'Sovereign'; Status = 'Active'; Captain = 'Jean-Luc Picard'; CrewCount = 855 }
@{ Name = 'Voyager'; Registry = 'NCC-74656'; Class = 'Intrepid'; Status = 'Active'; Captain = 'Kathryn Janeway'; CrewCount = 150 }
@{ Name = 'Defiant'; Registry = 'NCC-75633'; Class = 'Defiant'; Status = 'Active'; Captain = 'Benjamin Sisko'; CrewCount = 50 }
)
}
process {
$results = switch ($PSCmdlet.ParameterSetName) {
'ByName' {
Write-Verbose "Searching for ships named: $Name"
$script:ShipDatabase | Where-Object { $_.Name -like "*$Name*" }
}
'ByRegistry' {
Write-Verbose "Searching for ship with registry: $Registry"
$script:ShipDatabase | Where-Object { $_.Registry -eq $Registry }
}
'ByClass' {
Write-Verbose "Filtering by class: $Class$(if ($Status) { " and status: $Status" })"
$filtered = $script:ShipDatabase | Where-Object { $_.Class -eq $Class }
if ($Status) {
$filtered | Where-Object { $_.Status -eq $Status }
} else {
$filtered
}
}
'ByStatus' {
Write-Verbose "Filtering by status: $Status"
$script:ShipDatabase | Where-Object { $_.Status -eq $Status }
}
'All' {
Write-Verbose "Returning all ships"
$script:ShipDatabase
}
}
# Convert hashtables to PSCustomObjects for better output
foreach ($ship in $results) {
[PSCustomObject]@{
Name = $ship.Name
Registry = $ship.Registry
Class = $ship.Class
Status = $ship.Status
Captain = $ship.Captain
CrewCount = $ship.CrewCount
FullDesignation = "USS $($ship.Name) ($($ship.Registry))"
}
}
}
end {
Write-Verbose "Ship information retrieval completed"
}
}
'@
# Save the advanced function
$AdvancedFunctionContent | Out-File -FilePath "$ModulePath\Public\Get-StarTrekShipInfo.ps1" -Encoding UTF8
Write-Host "Advanced function with parameter sets created!" -ForegroundColor Green
Public Functions: The Module’s Interface
Public functions are what users interact with. They should be well-documented, have proper parameter validation, and handle errors gracefully.
# Create a comprehensive public function
$PublicFunctionContent = @'
function New-StarTrekShip {
<#
.SYNOPSIS
Creates a new StarTrek ship object.
.DESCRIPTION
This function creates a new StarTrek ship object with the specified parameters.
It supports various ship classes and validates input parameters.
.PARAMETER Name
The name of the ship (without USS prefix).
.PARAMETER Registry
The registry number (e.g., NCC-1701).
.PARAMETER Class
The ship class. Must be a valid Starfleet ship class.
.PARAMETER Captain
The name of the ship's captain.
.PARAMETER CrewCount
The number of crew members aboard the ship.
.EXAMPLE
New-StarTrekShip -Name "Enterprise" -Registry "NCC-1701" -Class "Constitution"
Creates a new Constitution-class ship named Enterprise.
.EXAMPLE
$ship = New-StarTrekShip -Name "Voyager" -Registry "NCC-74656" -Class "Intrepid" -Captain "Kathryn Janeway" -CrewCount 150
Creates a new Intrepid-class ship with full details.
.OUTPUTS
PSCustomObject
Returns a custom object representing the ship.
.NOTES
Author: Casper Stekelenburg
Version: 1.0.0
.LINK
https://github.com/starfleet/AdvancedStarTrekUtils
#>
[CmdletBinding(SupportsShouldProcess)]
[OutputType([PSCustomObject])]
param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter(Mandatory = $true, Position = 1)]
[ValidatePattern('^NCC-\d+$')]
[string]$Registry,
[Parameter(Mandatory = $true, Position = 2)]
[ValidateSet('Constitution', 'Galaxy', 'Sovereign', 'Intrepid', 'Defiant', 'Miranda', 'Excelsior', 'Ambassador', 'Akira', 'Norway')]
[string]$Class,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$Captain,
[Parameter()]
[ValidateRange(1, 10000)]
[int]$CrewCount
)
begin {
Write-Verbose "Starting ship creation process"
}
process {
try {
if ($PSCmdlet.ShouldProcess("$Name ($Registry)", "Create new StarTrek ship")) {
Write-Verbose "Creating ship: $Name"
# Create the ship object using PSCustomObject
$ship = [PSCustomObject]@{
Name = $Name
Registry = $Registry
Class = $Class
Captain = $Captain
CrewCount = $CrewCount
Status = 'Active'
CommissionDate = Get-Date
PSTypeName = 'StarTrek.Ship'
}
# Add a method-like script property for getting ship info
$ship | Add-Member -MemberType ScriptMethod -Name 'GetShipInfo' -Value {
return "USS $($this.Name) ($($this.Registry)) - $($this.Class) class"
}
Write-Information "Successfully created USS $Name" -InformationAction Continue
return $ship
}
}
catch {
Write-Error "Failed to create ship '$Name': $($_.Exception.Message)"
throw
}
}
end {
Write-Verbose "Ship creation process completed"
}
}
'@
# Save the public function
$PublicFunctionContent | Out-File -FilePath "$ModulePath\Public\New-StarTrekShip.ps1" -Encoding UTF8
Write-Host "Public function New-StarTrekShip created!" -ForegroundColor Green
Private Helper Functions
Private functions support your public functions but aren’t exposed to users. They should be focused on specific tasks and be easily testable.
# Create a private helper function
$PrivateFunctionContent = @'
function Test-RegistryFormat {
<#
.SYNOPSIS
Validates StarTrek ship registry format.
.DESCRIPTION
This private function validates that a ship registry follows the correct format.
.PARAMETER Registry
The registry to validate.
.OUTPUTS
Boolean
#>
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string]$Registry
)
Write-Verbose "Validating registry format: $Registry"
# Check if registry matches NCC-#### pattern
if ($Registry -match '^NCC-\d+$') {
Write-Verbose "Registry format is valid"
return $true
}
else {
Write-Warning "Invalid registry format: $Registry. Expected format: NCC-####"
return $false
}
}
function Get-ShipClassDefaults {
<#
.SYNOPSIS
Gets default values for different ship classes.
.DESCRIPTION
This private function returns default crew counts and other defaults for ship classes.
.PARAMETER Class
The ship class to get defaults for.
.OUTPUTS
Hashtable
#>
[CmdletBinding()]
[OutputType([hashtable])]
param (
[Parameter(Mandatory = $true)]
[ValidateSet('Constitution', 'Galaxy', 'Sovereign', 'Intrepid', 'Defiant', 'Miranda', 'Excelsior', 'Ambassador', 'Akira', 'Norway')]
[string]$Class
)
$defaults = @{
Constitution = @{ CrewCount = 430; MaxWarp = 8 }
Galaxy = @{ CrewCount = 1014; MaxWarp = 9.6 }
Sovereign = @{ CrewCount = 855; MaxWarp = 9.7 }
Intrepid = @{ CrewCount = 150; MaxWarp = 9.975 }
Defiant = @{ CrewCount = 50; MaxWarp = 9.5 }
Miranda = @{ CrewCount = 220; MaxWarp = 8 }
Excelsior = @{ CrewCount = 750; MaxWarp = 9 }
Ambassador = @{ CrewCount = 700; MaxWarp = 9.2 }
Akira = @{ CrewCount = 500; MaxWarp = 9.3 }
Norway = @{ CrewCount = 190; MaxWarp = 9.1 }
}
return $defaults[$Class]
}
'@
# Save the private function
$PrivateFunctionContent | Out-File -FilePath "$ModulePath\Private\ShipHelpers.ps1" -Encoding UTF8
Write-Host "Private helper functions created!" -ForegroundColor Green
Additional Public Functions
Let’s create some additional public functions to demonstrate advanced pipeline processing techniques.
Advanced Module Loading (PSM1 File)
The main module file (.psm1) is responsible for loading all components of your module. Let’s create an advanced loader that handles classes, enums, and functions properly.
# Create the main module file with advanced loading
$ModuleContent = @'
#Requires -Version 5.1
# Get the module root path
$ModuleRoot = $PSScriptRoot
# Import private functions (not exported)
Write-Verbose "Loading private functions..."
$privateFiles = Get-ChildItem -Path "$ModuleRoot\Private\*.ps1" -Recurse -ErrorAction SilentlyContinue
foreach ($privateFile in $privateFiles) {
try {
Write-Verbose "Importing private function: $($privateFile.Name)"
. $privateFile.FullName
}
catch {
Write-Error "Failed to import private function '$($privateFile.Name)': $($_.Exception.Message)"
}
}
# Import public functions (these will be exported)
Write-Verbose "Loading public functions..."
$publicFiles = Get-ChildItem -Path "$ModuleRoot\Public\*.ps1" -Recurse -ErrorAction SilentlyContinue
$publicFunctions = @()
foreach ($publicFile in $publicFiles) {
try {
Write-Verbose "Importing public function: $($publicFile.Name)"
. $publicFile.FullName
# Add the function name to the export list
$functionName = [System.IO.Path]::GetFileNameWithoutExtension($publicFile.Name)
$publicFunctions += $functionName
}
catch {
Write-Error "Failed to import public function '$($publicFile.Name)': $($_.Exception.Message)"
}
}
# Export public functions
if ($publicFunctions.Count -gt 0) {
Write-Verbose "Exporting functions: $($publicFunctions -join ', ')"
Export-ModuleMember -Function $publicFunctions
}
# Module initialization
Write-Verbose "AdvancedStarTrekUtils module loaded successfully"
Write-Information "🖖 Live long and prosper! AdvancedStarTrekUtils is ready." -InformationAction Continue
# Optional: Run any module initialization code here
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$ModuleRoot\AdvancedStarTrekUtils.psd1").ModuleVersion
Write-Verbose "Module version: $script:ModuleVersion"
'@
# Save the main module file
$ModuleContent | Out-File -FilePath "$ModulePath$ModuleName.psm1" -Encoding UTF8
Write-Host "Main module file created!" -ForegroundColor Green
# Update the module manifest to export the functions we created
$FunctionsToExport = @('New-StarTrekShip', 'Get-StarTrekShipInfo', 'Get-FleetStatus', 'Test-ShipRegistry')
$ManifestPath = "$ModulePath$ModuleName.psd1"
# Read the current manifest and update it
$manifestContent = Get-Content -Path $ManifestPath -Raw
$manifestContent = $manifestContent -replace "FunctionsToExport = @\(\)", "FunctionsToExport = @('$($FunctionsToExport -join "', '")')"
$manifestContent | Set-Content -Path $ManifestPath -Encoding UTF8
Write-Host "Module manifest updated with function exports!" -ForegroundColor Green
Documentation Best Practices
Good documentation is crucial for module adoption. Let’s create comprehensive documentation.
# Create a comprehensive README
$ReadmeContent = @'
# AdvancedStarTrekUtils PowerShell Module
[](https://www.powershellgallery.com/packages/AdvancedStarTrekUtils)
[](https://www.powershellgallery.com/packages/AdvancedStarTrekUtils)
Advanced utilities for Star Trek operations and fleet management. This module provides a comprehensive set of tools for managing Starfleet vessels, crew, and operations.
## Features
- **Ship Management**: Create and manage Starfleet vessels
- **Advanced Parameter Sets**: Multiple ways to search and filter data
- **Professional Documentation**: Complete help system with examples
- **Modern PowerShell**: Leverages PowerShell 5.1+ features
## Installation
### From PowerShell Gallery (Recommended)
```powershell
Install-Module -Name AdvancedStarTrekUtils -Scope CurrentUser
Manual Installation
- Download the module files
- Copy to your PowerShell modules directory:
- Windows:
$env:USERPROFILE\Documents\PowerShell\Modules\AdvancedStarTrekUtils
- Linux/macOS:
~/.local/share/powershell/Modules/AdvancedStarTrekUtils
- Windows:
Quick Start
# Import the module
Import-Module AdvancedStarTrekUtils
# Create a new starship
$enterprise = New-StarTrekShip -Name "Enterprise" -Registry "NCC-1701" -Class "Constitution" -Captain "James T. Kirk"
# View ship information
$enterprise.GetShipInfo()
# Search for ships using different criteria
Get-StarTrekShipInfo -Name "Enterprise"
Get-StarTrekShipInfo -Class "Constitution" -Status "Active"
Get-StarTrekShipInfo -All
Functions
New-StarTrekShip
Creates a new StarTrek ship object with validation and error handling.
New-StarTrekShip -Name "Defiant" -Registry "NCC-75633" -Class "Defiant" -Captain "Benjamin Sisko" -CrewCount 50
Get-StarTrekShipInfo
Searches for ships using multiple parameter sets for flexible querying.
# Find ships by name
Get-StarTrekShipInfo -Name "Enterprise"
# Find ships by registry
Get-StarTrekShipInfo -Registry "NCC-1701"
# Find ships by class and status
Get-StarTrekShipInfo -Class "Constitution" -Status "Active"
Contributing
- Fork the repository
- Create a feature branch
- Submit a pull request
License
This project is licensed under the MIT License – see the LICENSE file for details.
🖖 Live long and prosper!
Save the README
$ReadmeContent | Out-File -FilePath “$ModulePath\README.md” -Encoding UTF8 Write-Host “README.md created!” -ForegroundColor Green
## Module Versioning and Release Management
Proper versioning is crucial for module maintenance and user trust.
### Semantic Versioning (SemVer)
- **Major** (X.0.0): Breaking changes
- **Minor** (1.X.0): New features, backward compatible
- **Patch** (1.1.X): Bug fixes, backward compatible
### PowerShell Gallery Publishing
When you're ready to publish your module:
1. Test thoroughly across different PowerShell versions
2. Update version in the manifest
3. Create release notes
4. Publish to PowerShell Gallery
```powershell
# Create a release script
$ReleaseScriptContent = @'
param (
[Parameter(Mandatory = $true)]
[string]$Version,
[Parameter()]
[string]$ReleaseNotes = "Bug fixes and improvements",
[Parameter()]
[string]$ApiKey = $env:NUGET_API_KEY
)
$ModulePath = $PSScriptRoot
# Validate version format
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
throw "Version must be in format X.Y.Z (e.g., 1.2.3)"
}
# Update module manifest version
$ManifestPath = "$ModulePath\AdvancedStarTrekUtils.psd1"
$manifest = Import-PowerShellDataFile -Path $ManifestPath
# Update the manifest file
$manifestContent = Get-Content -Path $ManifestPath -Raw
$manifestContent = $manifestContent -replace "ModuleVersion\s*=\s*'[\d\.]+'", "ModuleVersion = '$Version'"
$manifestContent = $manifestContent -replace "ReleaseNotes\s*=\s*@'.*?'@", "ReleaseNotes = @'`n$ReleaseNotes`n'@"
$manifestContent | Set-Content -Path $ManifestPath -Encoding UTF8
Write-Host "Updated module version to $Version" -ForegroundColor Green
# Publish to PowerShell Gallery
if ($ApiKey) {
Write-Host "Publishing to PowerShell Gallery..." -ForegroundColor Yellow
Publish-Module -Path $ModulePath -NuGetApiKey $ApiKey -Verbose
Write-Host "Module published successfully!" -ForegroundColor Green
} else {
Write-Warning "No API key provided. Module not published."
Write-Host "To publish manually, run:" -ForegroundColor Cyan
Write-Host "Publish-Module -Path '$ModulePath' -NuGetApiKey '<your-api-key>'" -ForegroundColor Cyan
}
'@
# Save the release script
$ReleaseScriptContent | Out-File -FilePath "$ModulePath\Release.ps1" -Encoding UTF8
Write-Host "Release script created!" -ForegroundColor Green
Using Our Complete Module
Let’s use our advanced module to ensure everything works together.
Summary and Best Practices
Congratulations! You’ve built a professional-grade PowerShell module. Here are the key takeaways:
✅ What We Accomplished
- Structured Organization: Clean folder structure separating public, private, and docs
- Modern PowerShell: Used advanced parameter sets and validation techniques
- Professional Documentation: README, inline help, and examples
- Publishing Ready: Release script and proper versioning
🎯 Key Best Practices
- Separation of Concerns: Keep public and private functions separate
- Parameter Design: Use parameter sets and validation for better user experience
- Comprehensive Help: Every public function should have complete help documentation
- Semantic Versioning: Follow SemVer for predictable upgrades
- User Experience: Design intuitive APIs with good error messages
🚀 Next Steps
- Extend Functionality: Add more ship management features
- Documentation Site: Create a documentation website with examples
- Community: Open source your module and build a community
- Advanced Features: Explore PowerShell classes and CI/CD in future articles
Your module is now ready for professional use and distribution!
Conclusion
We’ve now learned how to build modules with an advanced structure and while there is a lot more to learn I hope this is a good start on what will be an interesting yourney into the land of PowerShell Modules. As always: Happy Scripting!
Leave a Reply