Posts tagged fsharp


Tutorial: PowerShell Modules in FSharp

:: dotnet, fsharp, powershell, programming language, tutorial

By: Maciej Barć

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 and Copyright are some of standard metadata fields,
  • AliasesToExport, CmdletsToExport, DscResourcesToExport, FunctionsToExport and VariablesToExport 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