Your Own Module

A module is a folder of functions that PowerShell loads by name: the functions go in a .psm1 file, Export-ModuleMember chooses which ones others can call, New-ModuleManifest writes a .psd1 that gives the module a version and a description, and a folder on $env:PSModulePath lets PowerShell find it from anywhere.

Journeyman · PowerShell · Updated

After this lesson you can

  • Turn a file of functions into a module with a .psm1 file, Import-Module and Export-ModuleMember
  • Give a module a manifest with New-ModuleManifest, and put it where PowerShell finds it by name through $env:PSModulePath

Key takeaways

  • A module is a folder with a .psm1 file of functions in it, both named after the module. Import-Module loads it.
  • Export-ModuleMember -Function lists the commands others can call. Helpers that aren't listed stay inside the module.
  • New-ModuleManifest writes a .psd1 file, the module's label: its version, author, description and the commands it shares.
  • $env:PSModulePath lists the folders where PowerShell looks for modules. A module there loads by itself the first time you use one of its commands.
  • After you change a module, Import-Module -Force loads the new version.

Your playlist functions work, and now they live in a script you dot-source, or pasted at the bottom of your profile, which grows longer every week. A friend asks for "your playlist commands", and you send a file with a note: "dot-source this, and ignore the helper functions, they're not for you." PowerShell's own commands don't arrive like that. You type Compress-Archive and it's there, with a version number, and nobody tells you about its helpers. They come in modules, and yours can too. IT teams hand out their tools as modules, and so does everyone who publishes to the PowerShell Gallery.

Make a practice folder with a playlist to work on:

New-Item -ItemType Directory -Force -Path "$HOME\terminal-practice\modules\PlaylistTools", "$HOME\terminal-practice\playlists" | Out-Null
Set-Content -Path "$HOME\terminal-practice\playlists\road-trip.csv" -Value 'Title,Artist,Length', 'Golden Hour Drive,The Night Owls,3:45', 'Rain on the Roof,Clara Moss,4:15', 'Long Way Home,Harbor Lights,5:30'
Set-Location "$HOME\terminal-practice\modules"

From a script to a module: a .psm1 file

A module is a toolbox with a label on it. In its simplest form it's a folder, and inside it a file of functions with the extension .psm1 (PowerShell script module), both with the module's name. Open the folder in VS Code (code PlaylistTools) and save this as PlaylistTools.psm1. It's a short set of playlist functions, cousins of the ones you wrote in Functions: Naming a Job: this Get-Song has the same name, but it lives in a new file, for a playlist that keeps each song's length as minutes and seconds. Get-Song reads the file into objects, Measure-Playlist adds up the minutes, and ConvertTo-Second is a helper that turns 3:45 into 225 seconds. Your own PlaylistTools.ps1 becomes a module the same way.

# PlaylistTools.psm1: tools for playlists saved as CSV files.

function ConvertTo-Second {
    param([string] $Length)
    $parts = $Length -split ':'
    [int] $parts[0] * 60 + [int] $parts[1]
}

function Get-Song {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path
    )
    foreach ($row in Import-Csv -Path $Path) {
        [PSCustomObject]@{
            Title   = $row.Title
            Artist  = $row.Artist
            Seconds = ConvertTo-Second -Length $row.Length
        }
    }
}

function Measure-Playlist {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSCustomObject] $Song
    )
    begin   { $total = 0; $count = 0 }
    process { $total += $Song.Seconds; $count++ }
    end {
        [PSCustomObject]@{
            Songs   = $count
            Minutes = [math]::Round($total / 60, 1)
        }
    }
}

Import-Module loads it, from the folder's path, and Get-Command -Module lists what it brought:

Import-Module .\PlaylistTools
Get-Command -Module PlaylistTools
Get-Song -Path ..\playlists\road-trip.csv | Measure-Playlist
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        ConvertTo-Second                                   0.0        PlaylistTools
Function        Get-Song                                           0.0        PlaylistTools
Function        Measure-Playlist                                   0.0        PlaylistTools

Songs Minutes
----- -------
    3   13.50

Share only what others need: `Export-ModuleMember`

ConvertTo-Second is in the list, so anyone who loads the module can call it, and may start to depend on it. A helper is a tool you keep inside the box. Add this line at the end of PlaylistTools.psm1, and save:

Export-ModuleMember -Function Get-Song, Measure-Playlist

To export a command is to hand it out. A module that never says which commands to export hands out all its functions; once it does, only those listed leave the box. PowerShell keeps the old version loaded until you ask for the new one, so load it again with -Force:

Import-Module .\PlaylistTools -Force
Get-Command -Module PlaylistTools
ConvertTo-Second -Length 3:45
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        Get-Song                                           0.0        PlaylistTools
Function        Measure-Playlist                                   0.0        PlaylistTools

ConvertTo-Second: The term 'ConvertTo-Second' 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.

Get-Song still calls ConvertTo-Second from inside the module. Only the prompt can't see it.

A label for the box: the manifest

The Version column says 0.0: the module has no label yet. A module manifest is a .psd1 file beside the .psm1 that says what the module is: its version, who wrote it, what it does, and which file holds its commands. New-ModuleManifest writes one, and Test-ModuleManifest checks it:

New-ModuleManifest -Path .\PlaylistTools\PlaylistTools.psd1 -RootModule PlaylistTools.psm1 -ModuleVersion 1.0.0 -Author 'Sam' -Description 'Tools for playlists saved as CSV files' -FunctionsToExport Get-Song, Measure-Playlist
Test-ModuleManifest -Path .\PlaylistTools\PlaylistTools.psd1
ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     1.0.0                 PlaylistTools                       {Get-Song, Measure-Playlist}

Open the .psd1: it's a long list, mostly comments for settings you don't need yet. The lines that matter now:

RootModule = 'PlaylistTools.psm1'
ModuleVersion = '1.0.0'
Author = 'Sam'
Description = 'Tools for playlists saved as CSV files'
FunctionsToExport = 'Get-Song', 'Measure-Playlist'

RootModule names the file with the commands; without it, the module loads nothing. FunctionsToExport is the manifest's list of what to hand out. A command leaves the box only when it's on both lists, this one and Export-ModuleMember's, so a new function has to be added to both. When you change the module, raise ModuleVersion (1.0.1 for a fix, 1.1.0 for a new command), so people can tell which copy they have.

The PlaylistTools module drawn as a toolbox: the box is the PlaylistTools folder, the tag on its handle is PlaylistTools.psd1 with version 1.0.0, and inside is PlaylistTools.psm1 with three tools; Get-Song and Measure-Playlist are handed out to the prompt, while the helper ConvertTo-Second stays inside the box

Your recipe box from Exercise: A Recipe Box Module becomes a module the same way: a RecipeBox folder, RecipeBox.psm1 and a manifest.

Where PowerShell looks: `$env:PSModulePath`

You never typed Import-Module for Compress-Archive. $env:PSModulePath is the list of folders where PowerShell looks for modules, the way PATH lists the folders for programs:

$env:PSModulePath -split ';'
C:\Users\Sam\Documents\PowerShell\Modules
C:\Program Files\PowerShell\Modules
c:\program files\powershell\7\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules

The first folder is yours, for modules you install for your account only. (If your Documents folder is kept in OneDrive, that path is inside OneDrive.) A module in one of these folders loads by itself the first time you use one of its commands. Try it with the practice folder, added to the front of the list for this window only:

$env:PSModulePath = "$HOME\terminal-practice\modules;" + $env:PSModulePath
Remove-Module PlaylistTools
Set-Location $HOME
Get-Song -Path .\terminal-practice\playlists\road-trip.csv | Measure-Playlist
Songs Minutes
----- -------
    3   13.50

No Import-Module, from your home folder: PowerShell found Get-Song in PlaylistTools on its own. To keep the module for good, copy its folder into your own modules folder, the first one in the list; a new window then finds it too. These lines make that folder if it isn't there yet, find your Documents folder wherever Windows keeps it, and are safe to run again after each change:

$myModules = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell\Modules'
New-Item -ItemType Directory -Force -Path $myModules | Out-Null
Copy-Item -Path "$HOME\terminal-practice\modules\PlaylistTools" -Destination $myModules -Recurse -Force

On a Mac or Linux, your modules folder is ~/.local/share/powershell/Modules, and $env:PSModulePath separates its folders with : instead of ;. Windows PowerShell 5.1 looks in Documents\WindowsPowerShell\Modules instead.

Check yourself

2 questions to try on the lesson page.

So what

A module turns a pile of functions into a tool with a name, a version and a clear front door, one that loads itself wherever you are and that you can hand to anyone.

Key terms

Cheat sheets

Related