Skip to main content
PowerShell · lesson 8 of 12
PowerShell intermediate 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

powershell modules scripts cmdletbinding whatif

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.

Frequently Asked Questions

Do I need Export-ModuleMember for a module to work at all?
No — without it, every function defined in a .psm1 is exported and callable from outside, which is exactly the problem this lesson opens with. Export-ModuleMember exists to restrict that list, not to enable it; a module with no functions meant to stay private doesn't need it at all.
What does [CmdletBinding()] actually add to a function or script?
It turns on the common parameters every real cmdlet has — -Verbose, -Debug, -ErrorAction, -WarningAction — without writing any handling for them yourself. Write-Verbose messages are silently dropped unless the caller passes -Verbose, which is why they're safe to leave in permanently rather than commenting them out.
What does SupportsShouldProcess actually protect against?
It gives the caller -WhatIf (preview what would happen without doing it) and -Confirm (ask before each action) for free, driven by a single if ($PSCmdlet.ShouldProcess(...)) check around the code that does something real. It's the standard way to make a destructive script safely previewable, the same idea behind Terraform's plan step or a dry-run flag.
Is Mandatory=$true the same as just checking if a parameter is $null inside the function?
No — a mandatory parameter stops the command before the function body ever runs if the value is missing, and in an interactive session it prompts for the value instead of failing outright. A manual null check only runs after the function has already started, and requires writing the check yourself in every function that needs it.
How is a PowerShell module different from just dot-sourcing a .ps1 file?
Dot-sourcing (. . ile.ps1) runs the script in the current scope and everything it defines becomes visible, with no way to keep anything private. A module gives you Export-ModuleMember to control exactly what's public, plus Import-Module features like versioning and Get-Module to see what's currently loaded — the difference matters most once a script grows past a handful of functions.