Cross-Platform PowerShell Scripting Tips
I was at the PowerShell+DevOps Global Summit last week, helping to run the OnRamp program. I was demonstrating a PowerShell example in PowerShell 7 on my Windows 11 desktop. My partner, kindly reminded me that what I was showing would only work on Windows. He was right, of course. In my haste to prepare the demo, I didn't take cross-platform compatibility into account. And I should. You should. Even though you may not be specifically writing code that will run cross-platform, it is possible that code you write today may end up on non-Windows machines. Or maybe you are developing code on a Mac that might be run on Windows. I thought it would be a good idea to review items you should keep in mind when writing PowerShell code that might run cross-platform.
#Requires
I would recommend you at least take advantage of the #requires
feature of PowerShell scripts. If you are writing a script that will only work on Windows PowerShell, specify the PSEdition
.
#requires -PSEdition Desktop
If the script requires PowerShell 7, set the value to Core
. It would also be a good idea to specify the version.
#requires -version 7.4
Unfortunately, there is no requirement statement that will say "Windows only". But there are ways around this limitation with a little code at the beginning of your script.
What IS this?
PowerShell 7 includes several variables that you can use to identify the platform.
if ($IsCoreCLR) {
Get-Variable -Name Is*
}


These variables do not exist on Windows PowerShell.
You could also test for the operating system using $PSVersionTable
.
PS C:\> $PSVersionTable.OS
Microsoft Windows 10.0.22635
PS /home/jeff> $PSVersionTable.OS
Ubuntu 20.04.6 LTS
Again, Windows PowerShell lacks this entry, but you have other ways to test what platform you are on.
I often use code like this:
If ($IsMacOS -OR $IsLinux) {
Write-Warning "This script requires a Windows platform."
#bail out
Return
}
Case Matters
Even though PowerShell itself is mostly case-insensitive, on non-Windows systems, casing can cause headaches.
PS /home/jeff> Get-Item env:user
Get-Item: Cannot find path 'Env:/user' because it does not exist.
On Linux, and presumably a Mac which I don't have to test with, the items in the environment PSDrive are all upper case.
PS /home/jeff> Get-Item env:USER
Name Value
---- -----
USER jeff
PS /home/jeff> $env:USER
jeff
If you are running commands interactively use tab-completion to ensure you are using the correct casing.
Watch Aliases
You always here about the downside of using aliases in scripts. When you get to cross-platform scripting, this becomes especially important. First, on non-Windows systems, any alias that mirrors a native command will not work. If you are used to running ps
or ls
in Windows, under PowerShell 7 on Linux, the aliases do not exist. You will run the native ps
or ls
command.
Where this is most likely to trip you up is with aliases that you don't think about. This is something you would run in Windows without even thinking about it.
PS C:\> Get-Process | sort WS -Descending | select -first 5
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
0 1,146.32 1,144.02 2.52 39140 0 vmmemWSL
63 957.10 757.80 5.86 6820 0 sqlservr
134 393.15 488.29 73.22 30140 1 thunderbird
104 590.33 465.02 3.70 21076 1 Snagit32
207 277.72 456.29 85.25 26232 1 pwsh
But this will bite you on Linux.
PS /home/jeff> Get-Process | sort WS -Descending | select -first 5
/usr/bin/sort: invalid option -- 'D'
Try '/usr/bin/sort --help' for more information.
You may think you've at least remembered to use Get-Process
instead of ps
. But the alias of Sort-Object
is sort
which is also a native Linux command. Using the Select
alias probably won't be a problem, but who's to say there won't be a conflict in the future? I'd recommend getting in the habit of using tab-completion to use full cmdlet names. Then there are no surprises.
Working with Paths
Another area that will trip you up is working with paths. Normally, PowerShell is flexible and can make adjustments for you. But on non-Windows systems, you need to be more careful.
PathSeparator
Here's the example that I got called out for at the PowerShell Summit. You can look at your %PATH%
environment variable in Windows and see that the paths are separated by a semicolon. To make it easier to look at, you might want to split the string into an array using the separator character.
PS C:\> $env:Path -split ";"
C:\Program Files\PowerShell\7
C:\Program Files (X86)\Microsoft Sql Server\160\Dts\Binn\
C:\Program Files (X86)\Windows Kits\10\Windows Performance Toolkit\
C:\Program Files\Dotnet\
C:\Program Files\Gs\Gs10.00.0\Bin
C:\Program Files\Microsoft Sql Server\150\Dts\Binn\
C:\Program Files\Microsoft Sql Server\150\Tools\Binn\
C:\Program Files\Microsoft Sql Server\Client Sdk\Odbc\170\Tools\Binn\
C:\Windows
...
This will fail on non-Windows systems that use a different separator character. The best way to handle this is to use the .NET class.
PS /home/jeff> $env:PATH -split [System.IO.Path]::PathSeparator
/opt/microsoft/powershell/7
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
...
This will work on any platform.
Joining and Splitting
Likewise, the directory separator character is different on non-Windows systems.
PS C:\> [System.IO.Path]::DirectorySeparatorChar
\
PS /home/jeff> [System.IO.Path]::DirectorySeparatorChar
/
This is one reason you should avoid concatenating paths.
$p = $HOME + "FooDad"
This will not work unless you are paying close attention to the directory separator character. And then you need to take the platform into account.
The best practice is to use the path cmdlets.
PS C:\> Join-Path $HOME -ChildPath FooDad
C:\Users\Jeff\FooDad
PS /home/jeff> Join-Path $HOME -ChildPath FooDad
/home/jeff/FooDad
The cmdlet will use the right separator character for the platform.
Environment Variables
This is an area where you can run into trouble. The environment variables on Windows are not the same as on Linux or a Mac. You can't assume that a variable that exists on Windows will exist on a non-Windows system.
For example, I'm a big user of $ENV:Computername
to get me the value %COMPUTERNAME%
. But this doesn't exist on Linux. Instead, use the .NET Framework.
PS /home/jeff> Write-Host "Testing on $([System.Environment]::MachineName)" -ForegroundColor yellow
Testing on Prospero
Wherever you would normally use $ENV:Computername
, start using [System.Environment]::MachineName
.
This is also true for the user domain name and user name. Use these .NET classes instead of the environment variables from the ENV:
drive.
[System.Environment]::UserDomainName
[System.Environment]::UserName
The other environment variable that might bite you is %TEMP%
. I'm very used to using $env:TEMP
in my scripts. But this doesn't exist on Linux. Instead, use [System.IO.Path]::GetTempPath()
.
PS /home/jeff> $out = Join-Path -path ([System.IO.Path]::GetTempPath()) -ChildPath r.txt
PS /home/jeff> Get-Process | Out-File -FilePath $out
PS /home/jeff> Get-Item $out
Directory: /tmp
UnixMode User Group LastWriteTime Size Name
-------- ---- ----- ------------- ---- ----
-rw-r--r-- jeff jeff 04/14/2024 13:29 2139 r.txt
I'll let you try this on Windows.
GetTempPath()
is a method so you need to wrap the expression in parentheses.
Culture
The last area I'll mention is culture. On Windows platform, you might want to check culture for date and time formatting.

But on non-Windows systems, you might need to be careful.
PS /home/jeff> Get-Culture
LCID Name DisplayName
---- ---- -----------
127 Invariant Language (Invariant Country)
There may be default settings, but they might not be what you expect or need.
PS /home/jeff> (Get-Culture).DateTimeFormat.ShortDatePattern
MM/dd/yyyy
You might be able to script something based on the timezone.
PS /home/jeff> Get-TimeZone
Id : America/New_York
HasIanaId : True
DisplayName : (UTC-05:00) America/New_York
StandardName : EST
DaylightName : EDT
BaseUtcOffset : -05:00:00
SupportsDaylightSavingTime : True
But that is going to be very specific, I think, to your project.
Summary
The possibilities of cross-platform scripting are exciting, but not without risk. The first thing to do is stick to full cmdlet names and use tab-completion. Be consistent with casing in your script. You may want to write a set of Pester tests for non-Windows systems. Using containers or Windows Subsystem for Linux (WSL) can be helpful.
If you are using anything environment related, be sure to test cross-platform. I've been assuming you are a Windows user that might be writing for Linux. But you could be a Linux admin wanting to write a cross-platform script for a platform, like Windows, that you are not familiar with.
Did I miss any other potential problems? Let me know in the comments.