20 minute read

This is the technical post. Twelve modules, all the patterns, all the gotchas. If you want the meta-story about AI-assisted development and why I am doing this at all, that is part 7. This post is about what we built and what we learned building it.

The modules, current as of this writing:

Module Version Implemented Stubs GitHub
Storage.Linux 0.5.0 4 157 repo
PowerShell.Management.Linux 0.2.0 8 7 repo
NetTCPIP.Linux 0.4.0 4 30 repo
DnsClient.Linux 0.1.0 4 17 repo
Update.Linux 0.2.0 3 19 repo
PowerShell.Security.Linux 0.1.0 2 4 repo
PowerShell.LocalAccounts.Linux 0.1.0 15 0 repo
ScheduledTasks.Linux 0.1.0 13 2 repo
PKI.Linux 0.3.1 8 9 repo
PrintManagement.Linux 0.1.1 7 15 repo
PowerShell.Utility.Linux 0.4.0 4 0 repo
NetAdapter.Linux 0.1.0 4 74 repo

All repositories are public under peppekerstens. All modules have been tested on WSL2 Ubuntu 24.04. Each README has a detailed “How we built this” section if you want the full story on a specific module.


Coverage

Before getting into the per-module detail: a brief note on scope. The baseline for this project is Evgenij Smirnov’s gap analysis from the 2025 European PowerShell Summit — a list of cmdlets that exist in Windows PowerShell but are missing from PS7.5 on Linux. The list has 209 cmdlets across seven regions.

91 of those are genuinely Windows-specific with no useful Linux equivalent: the Windows VPN stack, IPsec policy management, Teredo, the Windows Defender Firewall model, the Windows NDIS hardware offload driver API. Stripping those leaves 118 cmdlets worth pursuing.

Current status across the 118 in-scope cmdlets:

Status Count %
✅ Implemented 63 53 %
🔶 Stubbed (warns on call) 51 43 %
❌ Not covered 4 3 %

The 4 uncovered cmdlets are all NFS/SMB client cmdlets (Get-NfsSession, Get-NfsClientConfiguration, Disconnect-NfsSession, Block-SmbClientAccessToServer) — deprioritized. The networking ❌ gap (NetAdapter) is now fully covered. Security is 100% covered. Networking is 100% covered. That feels solid.


Cross-cutting patterns

There are patterns that apply to every module. Establishing them early and keeping them consistent was probably the most valuable decision in the project.

Linux-only guard in .psm1

Every module throws immediately if loaded on Windows:

#Requires -Version 7.2
if (-not $IsLinux) {
    throw "Storage.Linux cannot be loaded on Windows. On Windows, use the built-in Storage module."
}

This is at the very top of .psm1, before dot-sourcing anything. The alternative — transparently delegating to the real Windows cmdlets — sounds appealing but creates a maintenance burden. You end up maintaining two execution paths. The Windows path drifts. Tests stop covering Linux behaviour because the module loads fine on Windows and the Windows cmdlets pass. Clean failure is better than silent drift.

Where-Object instead of -Filter -Exclude

Early modules used -Filter and -Exclude parameters on Get-ChildItem inside .psm1. On Linux, the FileSystem provider silently ignores -Filter and -Exclude — it uses the provider-native filtering which on Linux does nothing for those parameters. Replaced everywhere with explicit Where-Object { $_.Name -notlike '*.Tests.ps1' } expressions.

Stub strategy

Windows modules export large surfaces. Storage has 161 cmdlets. NetTCPIP covers 34 and DnsClient adds 21 more. We cannot implement all of them — and do not need to. The pattern: export every cmdlet name, implement the ones that cover real-world usage, emit Write-Warning "not yet implemented on Linux" for the rest.

This keeps Get-Command -Module Storage.Linux consistent with the Windows version. Scripts that call a stub get a warning rather than a “command not found” error. If someone needs one of the stubs, the warning message is the signal.

BeforeDiscovery in test files

Every test file has:

#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.2.0' }

BeforeDiscovery {
    $script:OnLinux = $IsLinux
}

BeforeAll {
    if ($IsLinux) {
        Import-Module $PSScriptRoot/ModuleName.psd1 -Force
    }
}

BeforeDiscovery sets the platform flag used by -Skip:(-not $script:OnLinux) on every Describe block. Import-Module is inside BeforeAll rather than BeforeDiscovery because $PSScriptRoot is reliable at runtime but can be $null during Pester’s discovery phase depending on how the test run was invoked.

ShouldProcess on mutating cmdlets

Every cmdlet that modifies system state supports -WhatIf and -Confirm. This is done via [CmdletBinding(SupportsShouldProcess)] and a $PSCmdlet.ShouldProcess(...) guard before any system call. It costs almost nothing to add and makes every module safe to explore with -WhatIf before committing.

Structured ErrorRecord everywhere

No bare throw "some string" and no Write-Error "some string". Every error path constructs a typed exception, wraps it in an ErrorRecord with a unique error ID and an ErrorCategory enum value, and calls $PSCmdlet.WriteError(...) or $PSCmdlet.ThrowTerminatingError(...). This makes errors catchable by type and filterable in logs.

$ex = [System.InvalidOperationException]::new("CUPS not found. Install: apt install cups")
$er = [System.Management.Automation.ErrorRecord]::new(
    $ex, 'PrintManagement.Linux.CupsNotFound',
    [System.Management.Automation.ErrorCategory]::NotInstalled, $null)
$PSCmdlet.ThrowTerminatingError($er)

IDisposable in try/finally

Any .NET object implementing IDisposable (X509Store, X509Chain, RSA key, stream) is opened inside a try block and disposed in the finally block — unconditionally, whether the body threw or not. This was audited in session 17 (part 10) and applied consistently from PKI.Linux onwards.


Storage.Linux

Disk, volume, and partition management. Wraps lsblk, df, mount, and Crescendo-powered block device helpers. 4 implemented, 157 stubs. README

lsblk --bytes

lsblk returns sizes like 465.8G and 512M by default — human-readable strings. Without --bytes, Size ends up as a string and anything that compares or sorts by size breaks silently. The --bytes flag was missing from the first version; tests caught it because $disk.Size -gt 0 was always $false against a string.

[SWAP] in output

lsblk lists swap partitions with [SWAP] as the mount point. The volume parser skips any entry whose mount point does not start with / — this excludes swap and unmounted devices cleanly.

Crescendo as a private helper

Microsoft.PowerShell.Crescendo wraps lsblk JSON output internally. It is an implementation detail — not listed as a required module, not exposed in the public surface.


PowerShell.Management.Linux

Service management and computer information. 8 implemented: Get-Service, Start-Service, Stop-Service, Restart-Service, Set-Service, Get-ComputerInfo, Rename-Computer, Restart-Computer. README

Joining two systemctl outputs

Get-Service needs both service status (running/stopped) and the service description. systemctl list-units --type=service --all gives the first. systemctl list-unit-files --type=service gives the second. Neither gives both. Two calls, joined on service name, merged into one object.

Get-ComputerInfo from /proc

On Linux, the data that Windows returns from a single WMI call comes from six different files: /proc/version, /proc/cpuinfo, /proc/meminfo, /etc/os-release, /proc/uptime, and hostname. The result assembles into a PSCustomObject covering the properties scripts actually use.


NetTCPIP.Linux

IP addresses, routing, and TCP connections. 4 implemented: Get-NetIPAddress, Get-NetIPConfiguration, Get-NetRoute, Get-NetTCPConnection. 30 stubs. DNS cmdlets live in the separate DnsClient.Linux module. README

ip -json for structured output

ip -json addr show and ip -json route show return proper JSON. No text parsing. This is the advantage of iproute2 since version 4.12 (2017). Get-NetIPAddress, Get-NetRoute, and Get-NetIPConfiguration all use this.

LISTEN sockets and the * port

ss -tnap shows listening sockets with 0.0.0.0:* or [::]:* as the remote peer. The * is not a valid port number. A regex that extracts digits from the port field silently returns nothing for all LISTEN sockets — they are just dropped from output. Detect :\*$ explicitly and return 0 for the remote port.

Loop variable shadowing

Inside Get-NetTCPConnection, the loop variable was named $localPort — same as the -LocalPort parameter. Inside the loop, $localPort resolved to the current iteration value. The filter always matched everything or nothing. Renamed to $_localPort. PowerShell parameter names and pipeline variables share the same scope.

Get-NetRoute needs two calls

ip -json route show returns IPv4 only. IPv6 requires ip -6 -json route show. The default route has no destination prefix — mapped to 0.0.0.0/0 (IPv4) or ::/0 (IPv6) to match Windows behavior.


DnsClient.Linux

DNS resolution and client configuration. Mirrors the Windows DnsClient module — kept separate from NetTCPIP.Linux to match the Windows module boundary. 4 implemented: Resolve-DnsName, Clear-DnsClientCache, Get-DnsClientServerAddress, Get-DnsClientGlobalSetting. 17 stubs for NRPT, DoH, and remaining configuration cmdlets. README

Why a separate module

On Windows, DnsClient and NetTCPIP are separate modules — you load them independently and they have distinct scopes. An earlier version merged the DnsClient cmdlets into NetTCPIP.Linux v0.3.0 for convenience. That was wrong. Scripts that do Import-Module DnsClient on Windows cannot replicate that with Import-Module NetTCPIP.Linux — the names do not match and the intent does not match. v0.4.0 of NetTCPIP.Linux removed all DnsClient cmdlets; DnsClient.Linux v0.1.0 took them as its initial release.

Resolve-DnsName via dig

dig +noall +answer +authority +additional +ttlid +comments gives structured section output. The module parses the section markers and maps record types to typed PSCustomObject entries with Name, Type, TTL, and IPAddress (or NameHost for CNAME/PTR, etc.). If dig is not installed, the function throws a terminating error with the package install command in the message — unlike most stubs, DNS resolution with a missing tool is not a graceful degradation situation.

Clear-DnsClientCache with fallback

resolvectl flush-caches works on systemd-resolved systems. On systems without systemd-resolved, the fallback is nscd --invalidate=hosts. If neither is available, a warning is emitted. The function is fully ShouldProcess-aware — -WhatIf reports what would be flushed without running anything.

Get-DnsClientServerAddress from two sources

/etc/resolv.conf gives the global nameserver list. resolvectl status per interface gives per-interface DNS server assignments. The function merges both, deduplicating where the global and per-interface entries agree. Interface-specific overrides are returned with the interface name populated; the global fallback uses InterfaceAlias = 'Global'.


Update.Linux

Package update management as a PSWindowsUpdate peer. 3 implemented: Get-LinuxUpdate / Get-WindowsUpdate, Install-LinuxUpdate / Install-WindowsUpdate, Get-LinuxUpdateHistory / Get-WUHistory. README

The Listing... header

apt list --upgradable 2>/dev/null always emits a Listing... line before the package data. One Where-Object { $_ -notmatch '^Listing' } removes it.

dpkg.log action verbs

dpkg.log records install, upgrade, remove, purge, and configure actions. configure is a post-install step that appears for every package — include it and every install shows up twice in history. Filter to install and upgrade only.


PowerShell.Security.Linux

File ACL management. Get-LinuxAcl / Get-Acl, Set-LinuxAcl / Set-Acl. Stubs for Authenticode and catalog functions. README

stat vs getfacl

stat --format='%a|%A|%U|%G|%F|%n' is universally available. It gives octal mode, symbolic mode, owner, group, file type, and filename. getfacl adds named-user and named-group ACL entries but requires the acl package. The module uses stat as baseline and enriches with getfacl if present.

PSPath provider prefix

Get-ChildItem produces objects with PSPath set to Microsoft.PowerShell.Core\FileSystem::/etc/hosts. Passing this to stat fails — strip the provider prefix first. This only surfaces during pipeline input testing (Get-ChildItem /etc | Get-LinuxAcl), not when passing literal paths.


PowerShell.LocalAccounts.Linux

All 15 LocalAccounts cmdlets implemented. No stubs needed — Linux has tools for everything: useradd, usermod, userdel, groupadd, groupmod, groupdel, gpasswd, getent, passwd -S, chage. README

Three sources per user

Get-LocalUser calls getent passwd for basic user data, passwd -S for lock status, and chage -l for expiry dates. Three commands, one object per user. On systems where passwd -S requires root, the module catches the permission error and defaults Enabled to $true with a warning.

Primary group membership gap

/etc/group only lists supplementary memberships — not primary group membership. Get-LocalGroupMember also scans /etc/passwd for users whose primary GID matches the requested group. Without this, querying a user’s primary group returns empty results.


ScheduledTasks.Linux

13 of 15 ScheduledTasks cmdlets via systemd timers. Get-ScheduledTask, Register-ScheduledTask, Unregister-ScheduledTask, Start-ScheduledTask, Stop-ScheduledTask, Enable-ScheduledTask, Disable-ScheduledTask, Get-ScheduledTaskInfo, plus the four New-ScheduledTask* factory functions. Stubs for Set-ScheduledTask and Export-ScheduledTask. README

Why systemd, not cron

Cron was the obvious answer. systemd timers were chosen because they integrate with systemctlStart-ScheduledTask, Stop-ScheduledTask, Enable-ScheduledTask, and Disable-ScheduledTask all map directly to systemctl start/stop/enable/disable. Cron has no lifecycle management equivalent. Systemd also provides journald log integration and systemctl list-timers for structured status.

Timer + service unit pair

Each task creates two files: a .timer unit with the schedule (OnCalendar= or OnBootSec=) and a .service unit with the command to run. They share the same base name. Register-ScheduledTask writes both and runs systemctl daemon-reload.

User vs system scope

Tasks go to /etc/systemd/system/ (system scope, requires root) or ~/.config/systemd/user/ (user scope). Scope is inferred from id -u when not explicitly provided.


PKI.Linux

Certificate management via .NET cryptography APIs — no openssl CLI dependency. 7 cmdlets fully implemented: New-SelfSignedCertificate, Export-Certificate, Export-PfxCertificate, Get-PfxData, Import-Certificate, Import-PfxCertificate, Test-Certificate. Get-Certificate partially implemented (LocalStore parameter set). 9 stubs for Windows-only enrollment/AD cmdlets. README

Pure .NET — no openssl

System.Security.Cryptography.X509Certificates is fully cross-platform in .NET 6+. Every operation — generating keys, signing, encoding DER/PEM/PFX — is done through the .NET API. This means no dependency on openssl being installed, no parsing of openssl text output, and consistent behaviour across distributions. It also means the module works on distros where openssl is not in PATH or has a non-standard version.

IDisposable audit

After the initial implementation, a review against the PowerShell SDK development guidelines found that X509Chain, X509Store, RSA, and ECDsa objects were being created but never disposed. These implement IDisposable and hold cryptographic memory that should be released deterministically. All were wrapped in try/finally { .Dispose() }. The detailed write-up is in part 10.

Partial implementation model

Some cmdlets have multiple parameter sets — some implementable on Linux, some not. Get-Certificate has LocalStore (reads from X509Store, fully Linux-compatible), SubmitRequest (sends to a CA via RPC — Windows-specific), and PendingRetrieval (retrieves a pending cert via DCOM — Windows-specific). The Linux module implements LocalStore fully and throws PlatformNotSupportedException for the others, with an error message that names LocalStore as the supported alternative. This is the partial implementation model: between a full implementation and a stub, documented per-parameter-set.


PrintManagement.Linux

Print queue management via CUPS. 7 cmdlets implemented: Get-Printer, Get-PrintJob, Add-Printer, Remove-Printer, Remove-PrintJob, Suspend-PrintJob, Resume-PrintJob. 15 stubs for driver and port management. README

CUPS as the backend

Every print operation on Linux goes through CUPS. The module wraps lpstat (query), lpadmin (configure), and cancel (job control). These are the standard CUPS command-line tools available on any system with CUPS installed.

Get-Printer parses three separate lpstat calls — lpstat -p for status, lpstat -v for device URIs, lpstat -a for accepting state — and joins them into one object per printer. There is no single lpstat flag that returns all three in one call.

CUPS not installed

If CUPS is not installed, lpstat is not available. The module checks with Get-Command lpstat -ErrorAction SilentlyContinue before any CUPS operation and throws a structured ErrorRecord with ErrorCategory.NotInstalled and the package install command in the message.

Suspend is cancel -H hold

CUPS does not have a dedicated “pause job” command. Suspending a job is done via cancel -H hold <jobid>. Resuming is cancel -H resume <jobid>. The Windows cmdlet names Suspend-PrintJob / Resume-PrintJob map to these CUPS hold/release operations.


PowerShell.Utility.Linux

UI cmdlets. Out-GridView and Show-Command via Microsoft.PowerShell.ConsoleGuiTools. Out-Printer via CUPS lp. ConvertFrom-SddlString as a Linux-aware stub. README

ConsoleGuiTools, not PSTuiTools

Two terminal UI frameworks exist: Microsoft.PowerShell.ConsoleGuiTools (Terminal.Gui v1.16, read-only grid) and PSTuiTools (Terminal.Gui v1.19, editable forms). They cannot be loaded in the same PS session — assembly version conflict. We chose ConsoleGuiTools because it is the Microsoft-supported one and Out-GridView only needs read/select capability. Show-Command is a functional approximation — it lists available commands in the grid rather than rendering a true GUI form.

Out-Printer via lp

lp -d <printer> - takes stdin. Out-Printer formats the input object as text (| Out-String) and pipes it to lp. The -PrinterName parameter maps to -d. If no printer is specified, lp uses the system default.


NetAdapter.Linux

Network adapter management. 4 implemented: Get-NetAdapter, Get-NetAdapterStatistics, Enable-NetAdapter, Disable-NetAdapter. 74 stubs for NDIS hardware offload and other Windows-specific cmdlets. This module closes the last ❌ gap in the networking region. README

iproute2 has supported --json since kernel 4.12 (2017). Get-NetAdapter uses ip -json link show — no text parsing. Each link object has ifindex, ifname, address (MAC), operstate, flags, mtu, and link_type.

ip link show does not include link speed. Speed is read from /sys/class/net/<name>/speed. Virtual adapters (loopback, bridges, tunnels) return -1 or an error — these are reported as Unknown. Physical adapters on a live link return the speed in Mbps.

The -s flag adds a stats64 subtree to each link object with rx and tx sub-objects containing bytes, packets, errors, and dropped. Get-NetAdapterStatistics maps these directly to ReceivedBytes, SentBytes, etc.

NDIS offload cmdlets — out of scope

The majority of the Windows NetAdapter surface covers Windows NDIS hardware offload: checksum offload, LSO, RSS, VMQ, SR-IOV, etc. Linux has equivalent features but they are controlled via ethtool, sysfs, and driver-specific interfaces — not a unified API. All 74 non-implemented cmdlets are stubbed with Write-Warning.


Test coverage

All 12 modules were run through their full Pester test suites on both Windows (PS 7.5.1, Pester 5.3.3) and WSL2 Ubuntu 24.04 (PS 7.5.1, Pester 5.7.1). 1513 tests total. Zero failures on either platform.

Module Tests Win Pass Win Skip WSL Pass WSL Skip
Storage.Linux 534 15 519 533 1
PowerShell.Management.Linux 89 15 74 89 0
NetTCPIP.Linux 167 15 152 167 0
DnsClient.Linux 103 12 91 97 6
Update.Linux 134 63 71 134 0
PowerShell.Security.Linux 78 37 41 78 0
PowerShell.LocalAccounts.Linux 71 10 61 70 1
ScheduledTasks.Linux 56 17 39 39 17
PKI.Linux 37 1 36 36 1
PrintManagement.Linux 45 1 44 21 24
PowerShell.Utility.Linux 28 18 10 26 2
NetAdapter.Linux 171 0 171 171 0
TOTAL 1513 204 1309 1461 52

Windows skips are all Linux-only tests. WSL skips are tool-conditional: CUPS not installed (PrintManagement, 24), dig not installed (DnsClient, 6), systemd task tests require non-root elevation (ScheduledTasks, 17), and a handful of single-test edge cases across other modules.


Coverage analysis

After ten modules, going back to Evgenij Smirnov’s gap list from the 2025 European PowerShell Summit. The list has 209 cmdlets. 91 are genuinely Windows-specific with no useful Linux equivalent (VPN stack, IPsec policy engine, Teredo, Windows Firewall model, NDIS hardware offload API). The remaining 118 are the target.

Legend: ✅ Implemented · 🔶 Stubbed (warns, returns nothing) · ❌ No module · ⛔ Windows-specific

Summary

Region Total 🔶
Disk / Storage 26 3 23 0 0
Printing 18 6 12 0 0
Networking 105 11 9 0 85
Services / Tasks 22 16 6 0 0
Client (SMB/NFS) 4 0 0 4 0
Security 17 17 0 0 0
Sundry 17 10 1 0 6
Total 209 63 51 4 91

Of the 118 in-scope cmdlets: 53% implemented, 43% stubbed, 3% not yet covered (SMB/NFS client).


Disk / Storage (26 cmdlets)

Cmdlet Module Status
Get-Disk Storage.Linux
Get-Partition Storage.Linux
Get-PhysicalDisk Storage.Linux
Add-PartitionAccessPath Storage.Linux 🔶
Add-PhysicalDisk Storage.Linux 🔶
Clear-Disk Storage.Linux 🔶
Connect-IscsiTarget Storage.Linux 🔶
Connect-VirtualDisk Storage.Linux 🔶
Convert-PhysicalDisk Storage.Linux 🔶
Disconnect-IscsiTarget Storage.Linux 🔶
Dismount-DiskImage Storage.Linux 🔶
Format-Volume Storage.Linux 🔶
Get-DiskImage Storage.Linux 🔶
Get-PartitionSupportedSize Storage.Linux 🔶
New-Partition Storage.Linux 🔶
New-Volume Storage.Linux 🔶
Remove-Partition Storage.Linux 🔶
Remove-PartitionAccessPath Storage.Linux 🔶
Remove-PhysicalDisk Storage.Linux 🔶
Repair-VirtualDisk Storage.Linux 🔶
Repair-Volume Storage.Linux 🔶
Reset-PhysicalDisk Storage.Linux 🔶
Resize-Partition Storage.Linux 🔶
Set-Partition Storage.Linux 🔶
Set-PhysicalDisk Storage.Linux 🔶
Update-Disk Storage.Linux 🔶

Printing (18 cmdlets)

Cmdlet Module Status
Add-Printer PrintManagement.Linux
Get-Printer PrintManagement.Linux
Get-PrintJob PrintManagement.Linux
Remove-Printer PrintManagement.Linux
Remove-PrintJob PrintManagement.Linux
Resume-PrintJob PrintManagement.Linux
Add-PrinterDriver PrintManagement.Linux 🔶
Add-PrinterPort PrintManagement.Linux 🔶
Get-PrintConfiguration PrintManagement.Linux 🔶
Get-PrinterDriver PrintManagement.Linux 🔶
Get-PrinterPort PrintManagement.Linux 🔶
Get-PrinterProperty PrintManagement.Linux 🔶
Rename-Printer PrintManagement.Linux 🔶
Remove-PrinterDriver PrintManagement.Linux 🔶
Remove-PrinterPort PrintManagement.Linux 🔶
Set-PrintConfiguration PrintManagement.Linux 🔶
Set-Printer PrintManagement.Linux 🔶
Set-PrinterProperty PrintManagement.Linux 🔶

Networking (105 cmdlets)

Implemented (11)

Cmdlet Module Linux tool
Resolve-DnsName DnsClient.Linux dig
Clear-DnsClientCache DnsClient.Linux resolvectl / nscd
Get-DnsClientServerAddress DnsClient.Linux /etc/resolv.conf + resolvectl
Get-NetIPAddress NetTCPIP.Linux ip -json addr
Get-NetIPConfiguration NetTCPIP.Linux ip -json addr + ip -json route
Get-NetRoute NetTCPIP.Linux ip -json route
Get-NetTCPConnection NetTCPIP.Linux ss -tnap
Get-NetAdapter NetAdapter.Linux ip -json link show
Get-NetAdapterStatistics NetAdapter.Linux ip -s -json link show
Enable-NetAdapter NetAdapter.Linux ip link set up
Disable-NetAdapter NetAdapter.Linux ip link set down

Stubbed — in-module, potentially Linux-implementable (9)

Cmdlet Module Future tool
Find-NetRoute NetTCPIP.Linux ip route get
Get-DnsClient DnsClient.Linux resolvectl
Get-DnsClientCache DnsClient.Linux resolvectl statistics
Get-NetIPInterface NetTCPIP.Linux ip link
Get-NetIPv4Protocol NetTCPIP.Linux sysctl
Get-NetIPv6Protocol NetTCPIP.Linux sysctl
Get-NetNeighbor NetTCPIP.Linux ip neigh
Get-NetTCPSetting NetTCPIP.Linux sysctl net.ipv4.tcp_*
Test-NetConnection NetTCPIP.Linux ping / nc

Not covered — no module yet, implementable (0)

All networking cmdlets from Evgenij’s list are now covered (implemented or stubbed in a module).

Out of scope — Windows-specific (85)

Group Count Reason
VPN (Add-VpnConnection*) 5 Windows VPN stack
NetIPsec (*-NetIPsec*, Copy-NetIPsec*, Find-NetIPsecRule) 20 Windows IPsec policy engine
Teredo (Get-NetTeredo*) 2 Windows IPv6 tunneling
NetFirewall (*-NetFirewall*) 14 Windows Defender Firewall model
NetAdapter hardware offload (Disable/Enable-NetAdapter{Checksum,Encap,IPsec,LSO,PacketDirect,PowerMgmt,QoS,RDMA,RSC,RSS,SRIOV,URO,USO,VMQ}) 29 Windows NDIS driver model
NetAdapter advanced Get-* (Get-NetAdapter{AdvancedProperty,Binding,HardwareInfo,IPsecOffload,LSO,PacketDirect,PowerMgmt,QoS,RDMA,RSC,RSS,SRIOV,SriovVf,URO,USO}) 15 Windows NDIS driver model

Services / Tasks (22 cmdlets)

Cmdlet Module Status
Get-Service PowerShell.Management.Linux
Restart-Service PowerShell.Management.Linux
Set-Service PowerShell.Management.Linux
Start-Service PowerShell.Management.Linux
Stop-Service PowerShell.Management.Linux
Disable-ScheduledTask ScheduledTasks.Linux
Enable-ScheduledTask ScheduledTasks.Linux
Get-ScheduledTask ScheduledTasks.Linux
Get-ScheduledTaskInfo ScheduledTasks.Linux
New-ScheduledTask ScheduledTasks.Linux
New-ScheduledTaskAction ScheduledTasks.Linux
New-ScheduledTaskPrincipal ScheduledTasks.Linux
New-ScheduledTaskSettingsSet ScheduledTasks.Linux
New-ScheduledTaskTrigger ScheduledTasks.Linux
Register-ScheduledTask ScheduledTasks.Linux
Start-ScheduledTask ScheduledTasks.Linux
Export-ScheduledTask ScheduledTasks.Linux 🔶
New-ScheduledJobOption ScheduledTasks.Linux 🔶
New-Service PowerShell.Management.Linux 🔶
Remove-Service PowerShell.Management.Linux 🔶
Resume-Service PowerShell.Management.Linux 🔶
Suspend-Service PowerShell.Management.Linux 🔶

Client functionality — SMB / NFS (4 cmdlets)

Cmdlet Status Notes
Block-SmbClientAccessToServer SmbShare.Linux deprioritized
Disconnect-NfsSession No module
Get-NfsClientConfiguration No module — nfsstat available
Get-NfsSession No module — showmount available

Security (17 cmdlets)

Cmdlet Module Status
Get-Acl PowerShell.Security.Linux
Set-Acl PowerShell.Security.Linux
Add-LocalGroupMember PowerShell.LocalAccounts.Linux
Disable-LocalUser PowerShell.LocalAccounts.Linux
Enable-LocalUser PowerShell.LocalAccounts.Linux
Get-LocalGroup PowerShell.LocalAccounts.Linux
Get-LocalGroupMember PowerShell.LocalAccounts.Linux
Get-LocalUser PowerShell.LocalAccounts.Linux
New-LocalGroup PowerShell.LocalAccounts.Linux
New-LocalUser PowerShell.LocalAccounts.Linux
Remove-LocalGroup PowerShell.LocalAccounts.Linux
Remove-LocalGroupMember PowerShell.LocalAccounts.Linux
Remove-LocalUser PowerShell.LocalAccounts.Linux
Rename-LocalGroup PowerShell.LocalAccounts.Linux
Rename-LocalUser PowerShell.LocalAccounts.Linux
Set-LocalGroup PowerShell.LocalAccounts.Linux
Set-LocalUser PowerShell.LocalAccounts.Linux

Sundry (17 cmdlets)

Cmdlet Module Status Notes
Export-Certificate PKI.Linux  
Export-PfxCertificate PKI.Linux  
Get-ComputerInfo PowerShell.Management.Linux  
Get-PfxData PKI.Linux  
New-SelfSignedCertificate PKI.Linux  
Out-GridView PowerShell.Utility.Linux  
Out-Printer PowerShell.Utility.Linux  
Rename-Computer PowerShell.Management.Linux  
Show-Command PowerShell.Utility.Linux  
Test-Certificate PKI.Linux  
Set-TimeZone PowerShell.Management.Linux 🔶 Not available in PS7.5 on Linux natively
Get-DisplayResolution WinPS only (noted by Evgenij)
Get-PlatformIdentifier No standard PS cmdlet
Get-PnpDevice Windows PnP subsystem
Get-PnpDeviceProperty Windows PnP subsystem
Get-SystemDriver Windows driver model
Set-SystemPreferredUILanguage Windows UI language stack

The remaining ❌ cmdlets are SMB/NFS client cmdlets — deprioritized given the complexity of SMB server-side infrastructure and the low value of NFS client wrappers at this stage.


All twelve module repositories are at github.com/peppekerstens. The links in the table at the top of this post go to specific commits, so they will show the code as it was when this post was written. Pull requests welcome.


PSScriptAnalyzer — Getting to Zero

After all twelve modules were test-complete, we ran PSScriptAnalyzer across every module to systematically clean up code quality issues.

Baseline: Error=6, Warning=399 across 12 modules.

Real bugs fixed

The errors and actionable warnings turned out to be genuine defects worth fixing:

Automatic variable collisions:

  • Get-LocalUser.ps1: variable $home overwrites PowerShell’s read-only automatic variable ($HOME). Renamed to $homeDir.
  • New-LocalUser.ps1, Rename-LocalUser.ps1: variable $args overwrites the automatic $args array. Renamed to $cmdArgs.

Empty catch blocks: Seven empty catch { } blocks across Get-LocalUser.ps1, Get-ComputerInfo.ps1, and Get-ScheduledTaskInfo.ps1 swallowed exceptions silently. Changed to catch { Write-Debug $_.Exception.Message } — still non-fatal, but now debuggable.

Pipeline functions without process {}: Eleven functions declared ValueFromPipeline or ValueFromPipelineByPropertyName on parameters but had no process {} block, meaning only the last piped value would be processed. Fixed by wrapping the entire function body (including the Windows delegation path) in process {}. The affected functions were spread across Storage.Linux, PowerShell.Management.Linux, ScheduledTasks.Linux, PKI.Linux, and PowerShell.Utility.Linux.

One subtle trap: putting code before process {} in a function body causes PowerShell (and PSSA) to interpret the process keyword as the Get-Process cmdlet alias. The process {} named block must be the first statement in the function body.

Manifest issues: PowerShell.LocalAccounts.Linux.psd1 had CmdletsToExport = '*' — changed to CmdletsToExport = @() since the module exports functions, not compiled cmdlets.

Credential parameter type: Get-Certificate.ps1 had [object] $Credential. Changed to [PSCredential] with the [System.Management.Automation.Credential()] attribute, matching the correct PowerShell credential pattern.

Unused variables: Stop-Service.ps1: $params = @{} built but then bypassed. Get-Service.ps1: $loadState captured but never used. Resolve-DnsName.ps1: a first $lines capture was immediately overwritten by a better $rawLines call. All removed.

Intentional patterns — suppressed, not fixed

A large chunk of the warnings were correct rule violations but intentional by design:

Rule Count Reason
PSAvoidOverwritingBuiltInCmdlets 18 That’s literally what the modules do
PSUseShouldProcessForStateChangingFunctions 110 Stub functions don’t change state — they emit a warning
PSShouldProcess 75 Same stubs: SupportsShouldProcess without calling $PSCmdlet.ShouldProcess()
PSReviewUnusedParameter 79 Stub parameters exist for interface parity with Windows cmdlets
PSUseBOMForUnicodeEncodedFile 66 Cross-platform UTF-8 without BOM is intentional

These were suppressed by adding a PSScriptAnalyzerSettings.psd1 at the root of each of the twelve module repos with an ExcludeRules list. This is cleaner than sprinkling [SuppressMessageAttribute] on every stub function.

The run-pssa.ps1 runner was updated to pass -Settings per module, and to filter results from Helpers\ and Crescendo\ directories (scratch/generated files that are not part of the module surface).

Final result: Error=0, Warning=0 across all 12 modules. 0 test regressions (204 pass, 1309 skip on Windows; 1513 total unchanged).

Updated: