PowerShell · lesson 8 of 12
Modules, Export-ModuleMember, and -WhatIf
Every function in a .psm1 is public by default — a helper meant to stay internal leaks out silently until Export-ModuleMember says otherwise. Then: CmdletBinding, -Verbose, and SupportsShouldProcess.
By SkillByExample · Published
A module file with no Export-ModuleMember at all still works — every function in it is
public. That’s rarely what was intended.
The bug: an internal helper leaks out
# BookTools.psm1
function Format-BookTitle {
param([string]$Title)
"$Title (formatted)"
}
function Get-InternalHelper {
"this was meant to stay private"
}
Import-Module .\BookTools.psm1 -Force
Format-BookTitle -Title "Dune"
Get-InternalHelper
Dune (formatted)
this was meant to stay private
Get-InternalHelper was never meant to be called from outside the module — its name doesn’t
even follow a convention a caller would expect to use directly — and yet it works exactly
like Format-BookTitle does. With no Export-ModuleMember call anywhere in the file,
every function defined in a .psm1 is public by default.
The fix: Export-ModuleMember
# BookTools.psm1
function Format-BookTitle {
param([string]$Title)
"$Title (formatted)"
}
function Get-InternalHelper {
"this was meant to stay private"
}
Export-ModuleMember -Function Format-BookTitle
Import-Module .\BookTools.psm1 -Force
Format-BookTitle -Title "Dune"
Get-InternalHelper
Dune (formatted)
error: The term 'Get-InternalHelper' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
Naming only Format-BookTitle in Export-ModuleMember makes it the module’s entire public
surface — Get-InternalHelper still exists and still runs inside the module, but nothing
outside it can see or call it anymore.
Get-Command -Module BookTools
CommandType Name Version Source
----------- ---- ------- ------
Function Format-BookTitle 0.0 BookTools
Get-Command -Module <name> is the direct way to check a module’s actual public surface — no
guessing from reading the source required.
CmdletBinding and -Verbose
# Backup-Report.ps1
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[int]$CopyCount = 1
)
Write-Verbose "Backing up $Name with $CopyCount copies"
"Backed up $Name ($CopyCount copies)"
.\Backup-Report.ps1 -Name "Dune" -Verbose
VERBOSE: Backing up Dune with 1 copies
Backed up Dune (1 copies)
.\Backup-Report.ps1 -Name "Dune"
Backed up Dune (1 copies)
[CmdletBinding()] at the top of a script or function turns on common parameters like
-Verbose for free — Write-Verbose messages are silently dropped unless the caller actually
asks for them, so they’re safe to leave in a script permanently rather than commenting them
out for a “real” run.
SupportsShouldProcess and -WhatIf
# Remove-Report.ps1
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[Parameter(Mandatory=$true)]
[string]$Path
)
if ($PSCmdlet.ShouldProcess($Path, "Delete report")) {
"actually deleting $Path"
} else {
"skipped (WhatIf)"
}
.\Remove-Report.ps1 -Path "C:\temp\report.csv" -WhatIf
What if: Performing the operation "Delete report" on target "C:\temp\report.csv".
skipped (WhatIf)
.\Remove-Report.ps1 -Path "C:\temp\report.csv"
actually deleting C:\temp\report.csv
One if ($PSCmdlet.ShouldProcess(...)) check gives every caller a way to preview a
potentially destructive script without changing anything — the same idea as terraform plan
or any other dry-run flag, built into the parameter system instead of hand-rolled.
Practice
1. List a module's exported commands without opening the source file.
Import-Module .\BookTools.psm1 -Force
Get-Command -Module BookTools
CommandType Name Version Source
----------- ---- ------- ------
Function Format-BookTitle 0.0 BookTools
The same command from the lesson body — worth remembering as the fast way to answer “what does this module actually expose” for any module, including ones you didn’t write.
2. Call the backup script without -Name and see how a mandatory parameter is enforced.
.\Backup-Report.ps1
Backup-Report.ps1: Cannot process command because of one or more missing mandatory parameters: Name.
In an interactive session, a missing mandatory parameter prompts for the value instead of
failing outright — but in a non-interactive context like this one (a scheduled task, a CI
job), there’s no one to answer the prompt, so it fails immediately with a clear message
instead. Either way, the script body never runs without Name.
3. Add a second exported function to the module and confirm both are visible.
function Format-BookTitle {
param([string]$Title)
"$Title (formatted)"
}
function Get-BookSummary {
param([string]$Title, [int]$Year)
"$Title ($Year)"
}
Export-ModuleMember -Function Format-BookTitle, Get-BookSummary
Import-Module .\BookTools.psm1 -Force
Get-Command -Module BookTools | Select-Object Name
Name
----
Format-BookTitle
Get-BookSummary
-Function on Export-ModuleMember takes a comma-separated list — nothing about restricting
the export surface means restricting it to exactly one function.
Next: background jobs and running work in parallel.