Tutorial: PowerShell Modules in FSharp
F# is a amazing functional language bridging C#/.NET ecosystem with OCaml and interactive programming. It has recently became by favorite language to create personal projects in. It both has additional security of strong typing, unlike Python or Ruby while keeping the functional and interactive properties of weak-typed languages. F# is truly a engineering marvel.
PowerShell 7 is the Open-Source implementation of the old Windows PowerShell that is also cross-platform and can be used for scripting and automation. It’s Object-orientation makes an amazing extension capability compared to Bash.
You can use the F# language to create PowerShell modules. Normally PS Modules are written in C# but since the interop between .NET languages is insanely fluent one can just swap C# for F#.
Creating new project
1 2 |
mkdir -p ~/source/temporary/powershell-modules/fs-example cd ~/source/temporary/powershell-modules/fs-example |
Dotnet ClassLib
Init new F# Class project:
1 |
dotnet new classlib --language "F#" |
Dependencies
Add dependency on Powershell interaction library:
1 |
dotnet add package PowerShellStandard.Library --version 5.1.1 |
F# interaction with PowerShell
Let’s rewrite Library.fs to contain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
namespace FsExample.PowerShell open System.Management.Automation [<Cmdlet(VerbsCommon.Format, "FsExample")>] type FsExample() = inherit PSCmdlet() [<Parameter>] [<ValidateNotNullOrEmpty>] member val Name: string = "Me" with get, set override this.BeginProcessing() = printfn "Hello %s" this.Name |
Then, change RootNamespace
to the main module name, that is FsExample.PowerShell
.
We need to copy all assemblies to output. Add <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net9.0</TargetFramework> <RootNamespace>FsExample.PowerShell</RootNamespace> <GenerateDocumentationFile>true</GenerateDocumentationFile> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <!-- Snip ... --> </Project> |
PowerShell library
Create PSModuleAssets
directory:
1 2 |
mkdir -p PSModuleAssets cd PSModuleAssets |
The main module file will in turn load the DLL compiled form above F# code.
Create main module file - fs-example.psm1
:
1 2 3 |
#!/usr/bin/env -S pwsh -NoLogo -NoProfile -NonInteractive Import-Module "$PSScriptRoot/fs-example.dll" |
We then need to create the main manifest file fs-example.psd1
.
Path
- relative path to output the manifest,RootModule
- relative module path to load on module import,ModuleVersion
,Description
,Author
andCopyright
are some of standard metadata fields,AliasesToExport
,CmdletsToExport
,DscResourcesToExport
,FunctionsToExport
andVariablesToExport
are either globs or lists that tell what functions will be available on module load, for simplicity we will just specify a glob expression'*'
.
1 2 3 4 5 6 |
New-ModuleManifest -Path ./fs-example.psd1 -RootModule ./fs-example.psm1 ` -ModuleVersion "1.0.0" ` -Description "FSharp example module" ` -Author "Me" -Copyright "Copyright (c) 2025, Me" ` -AliasesToExport '*' -CmdletsToExport '*' ` -DscResourcesToExport '*' -FunctionsToExport '*' -VariablesToExport '*' |
Copying PowerShell assets
Following ItemGroup
will copy all files from PSModuleAssets
to the output directory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<Project Sdk="Microsoft.NET.Sdk"> <!-- Snip ... --> <ItemGroup> <None Include="PSModuleAssets/*.*"> <Link>%(Filename)%(Extension)</Link> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup> <!-- Snip ... --> </Project> |
Building the F# module
Clean up the old builds:
1 2 |
rm -fr ./psrepository rm -fr ./out-module/fs-example |
Dotnet-Build
And then build the module:
1 2 |
dotnet restore dotnet build --configuration Release --output ./out-module/fs-example |
Language locality bug
You have to use the en-US
locale when publishing PowerShell modules.
1 2 3 |
$env:LANG = "en_US.UTF-8" $env:LANGUAGE = "en_US.UTF-8" $env:LC_MESSAGES = "en_US.utf8" |
Installation
Remove old registered PowerShell repository:
1 |
try { Unregister-PSRepository -Name fs-example } catch { } |
Register-PSRepository and Publish-Module
This quite complicated script will:
- set up a PowerShell repository in current location,
- register the PowerShell repository,
- publish the module into the PowerShell repository,
- install that published module from the PowerShell repository.
1 2 3 4 5 6 7 8 |
$repo = "fs-example" $repoPath = "$pwd/psrepository/fs-example" New-Item -ItemType Directory -Path $repoPath | Out-Null Register-PSRepository -InstallationPolicy Trusted -Name $repo -SourceLocation $repoPath Publish-Module -Verbose -Path $pwd/out-module/fs-example -Repository $repo Install-Module -Scope CurrentUser -Force -Verbose -Name fs-example -Repository $repo |
Removal
Uninstall-Module
Just call Uninstall-Module
to remove any PowerShell module.
1 |
Uninstall-Module -Name fs-example |
Usage in the REPL
Import-Module
Import it with Import-Module
.
1 |
Import-Module -Verbose fs-example -Force |
Finally - use Format-FsExample
Remember the above F# function that was defined in the class?
1 2 |
[<Cmdlet(VerbsCommon.Format, "FsExample")>] type FsExample() |
It will be now available under the name composed of the “common verb” and a secondary part, so Format-FsExample
.
Try it:
1 |
Format-FsExample -Name Maciej |