PowerShell Inside All articles
Cross-Platform Automation

Your Script Worked Fine on Windows. Then It Met Linux.

PowerShell Inside
Your Script Worked Fine on Windows. Then It Met Linux.

Photo: MoonlightDoubleE, CC BY-SA 4.0, via Wikimedia Commons

The Demo Went Great. Production Had Other Plans.

Every IT pro who's been around the block knows this story. You watch the PowerShell 7 announcement, you spin up a Ubuntu VM on your hypervisor of choice, you run Get-Process and a ForEach-Object pipeline, and it just works. Magic. You tell your boss the team can standardize on one scripting language across the whole estate.

Then you actually migrate a real script — one that's been quietly managing your Windows servers since the Obama administration — and things get interesting fast.

I've been through this on a couple of engagements now: mixed Windows Server and Linux shops, mostly in finance and healthcare here in the US, where the Linux side grew out of Docker deployments and the Windows side never left. Here's the honest catalog of what broke, so you can dodge it.

The Registry Problem Is the Obvious One

Let's get it out of the way: Get-ItemProperty HKLM:\... doesn't exist on Linux. Neither does Set-ItemProperty on a registry path, obviously, because there's no registry. The PowerShell team didn't remove the cmdlets so much as the concept itself.

The mistake teams make is wrapping every registry call in an if ($IsWindows) check and calling it a day. That works, but you've now got a script that's half configuration management for two operating systems. Sometimes that's the right call — but often the registry access was standing in for something portable. Was it reading a config value? That's a JSON file with ConvertFrom-Json. Was it checking an installed service? On Linux, that's systemctl is-active via Invoke-Expression or, better, shelling out cleanly.

WMI and CIM: The Silent Killer

Here's the one that gets people. Get-CimInstance exists on Linux. It just returns nothing useful for most classes, because WMI is a Windows technology. Your inventory script that pulls Win32_OperatingSystem won't throw a loud error — it'll return $null and then throw a cryptic error three lines later when you try to index into nothing.

Silent partial failure is worse than a hard crash. A script that logs "completed successfully" while collecting zero data is a resume-generating event waiting to happen.

The fix: detect the platform early and branch to genuinely different collection methods. On Linux, that's reading /proc, parsing lsb-release, or using the native package manager. On Windows, keep your CIM queries — they're excellent and you shouldn't sacrifice them. Test-Path /proc is a cheap, reliable platform check if you don't want to lean on $IsLinux.

Paths: Backslashes Bite

Windows-trained fingers type C:\Scripts\Logs on autopilot. On Linux, that's a single weird directory name at best. Even seasoned folks trip here because PowerShell on Windows happily accepts forward slashes in most .NET calls, so the bad habit never gets punished — until it does.

Three rules that have served me well:

Module Compatibility: Check Before You Ship

A huge chunk of real-world breakage lives in modules, not in the language. That ActiveDirectory module you've used for a decade? Windows-only. Same for most things that wrap proprietary tooling. Microsoft's own modules vary — Az and Microsoft.Graph are genuinely cross-platform, while WindowsUpdate or anything riding on .NET Framework assemblies will not load on Linux at all.

Before any migration, run your scripts through a compatibility pass. CompatibilityScore tooling and the PowerShell module compatibility analyzer can flag which modules use unsupported cmdlets. It's not perfect, but it turns a week of runtime surprises into an afternoon of planning.

The Stuff That Does Work (and Works Well)

To keep this from being a downer: the core language is rock solid cross-platform. Pipelines, functions, classes, Invoke-RestMethod, JSON handling, SSH-based remoting — all of it runs identically on Ubuntu in a container and Windows Server 2022. If your automation is mostly API calls, file processing, and orchestration, you may need to change almost nothing.

The pain is concentrated at the OS boundary. Which suggests a design principle: push platform-specific code to the edges. Keep your core logic in pure PowerShell, and isolate the registry pokes and CIM queries into small, well-named functions that have obvious platform branches. Six months from now, the person maintaining your script — probably you, tired, on a Friday — will be grateful.

A Quick Pre-Flight Checklist

Before you ship a script to a hybrid environment:

  1. Grep for HKLM, Win32_, and backslash-joined paths.
  2. Verify every imported module loads on the target OS.
  3. Add platform detection at the top, not scattered inline.
  4. Test with $ErrorActionPreference = 'Stop' so silent nulls surface early.
  5. Confirm line endings — some Linux tooling still chokes on CRLF.

Cross-platform PowerShell is real and increasingly good. It's just not free. Budget for the boundary work, and you get to keep the best parts of the language everywhere you run it.

All Articles