PowerShell and Linux: Cross-Platform Scripting with PS 7
─□✕

PowerShell and Linux: Cross-Platform Scripting with PS 7

PowerShell Tips Editor 5 min read
PowerShell and Linux: Cross-Platform Scripting with PS 7

PowerShell 7 has been installable on Linux since 2018, but writing scripts that actually run unmodified on both Windows and Linux requires deliberate choices. The problems are rarely about syntax — PowerShell syntax is the same on both platforms. They are about path separators, environment variable conventions, CRLF line endings baked into PS1 files, and the assumption that every tool you call is a Windows binary. This guide covers the practical decisions that separate a Windows-only script from a genuinely cross-platform one.

Installing PowerShell 7 on Ubuntu, Debian, and RHEL

Microsoft maintains an official package repository for all major Linux distributions. On Ubuntu and Debian, use the apt feed. On RHEL and CentOS Stream, use the rpm feed. Always install from the Microsoft repository rather than the snap or flatpak variants — those builds sometimes lag behind on minor version updates.

# Ubuntu 22.04 — one-liner install from Microsoft feed
sudo apt-get update && sudo apt-get install -y wget apt-transport-https
wget -q "https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb"
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update && sudo apt-get install -y powershell

# Verify version
pwsh -Command '$PSVersionTable.PSVersion'
Major  Minor  Patch  PreReleaseLabel BuildLabel
-----  -----  -----  --------------- ----------
7      4      6

On RHEL 9:

sudo rpm -Uvh https://packages.microsoft.com/config/rhel/9/packages-microsoft-prod.rpm
sudo dnf install -y powershell

Path Separator Differences: [IO.Path]::DirectorySeparatorChar

Hardcoding backslashes is the single most common source of cross-platform breakage. The fix is to stop constructing path strings manually and use .NET’s path utilities instead. [IO.Path]::Combine() uses the platform-correct separator automatically. [IO.Path]::DirectorySeparatorChar returns \ on Windows and / on Linux, useful when you need the character explicitly.

# Bad — hardcoded separator, breaks on Linux
$config = "C:\App\config\settings.json"

# Good — use Join-Path or [IO.Path]::Combine
$base   = if ($IsWindows) { 'C:\App' } else { '/opt/app' }
$config = Join-Path $base 'config' 'settings.json'

# Or using .NET directly
$config = [IO.Path]::Combine($base, 'config', 'settings.json')

# Detect current separator
Write-Host "Separator: $([IO.Path]::DirectorySeparatorChar)"

PowerShell 7 also exposes the automatic variables $IsWindows, $IsLinux, and $IsMacOS — use them for platform-conditional logic rather than checking $PSVersionTable.OS.

Calling Native Linux Commands and Capturing Output

PowerShell on Linux can call any executable on $PATH just as it would call a cmdlet. The difference is that native command output arrives as strings, not objects. Use | ConvertFrom-Json or manual string parsing to turn that output into structured data. For commands that support JSON output (like systemctl with --output json or ip -json), prefer JSON over screen-scraping.

# List systemd services and parse status
$services = systemctl list-units --type=service --output=json | ConvertFrom-Json

# Capture df output and parse free disk space
$dfLines = df -BM --output=source,size,used,avail,pcent / | Select-Object -Skip 1
$parts   = $dfLines -split '\s+'
[PSCustomObject]@{
    Filesystem = $parts[0]
    SizeMB     = [int]($parts[1] -replace 'M')
    UsedMB     = [int]($parts[2] -replace 'M')
    AvailMB    = [int]($parts[3] -replace 'M')
    UsePercent = $parts[4]
}

Environment Variables and Home Directory on Linux

On Windows, $env:USERPROFILE holds the home directory. On Linux it is $env:HOME. Scripts that hardcode either will fail on the other platform. Use [Environment]::GetFolderPath('UserProfile') — this .NET method resolves the correct path on both platforms. Similarly, $env:PATH uses colons as separators on Linux and semicolons on Windows; use [IO.Path]::PathSeparator if you need to split or join the PATH string programmatically.

# Cross-platform home directory
$home = [Environment]::GetFolderPath('UserProfile')
# On Linux this returns /home/username
# On Windows this returns C:\Users\username

# Cross-platform temp directory
$tmp = [IO.Path]::GetTempPath()

# Split PATH safely
$pathEntries = $env:PATH -split [IO.Path]::PathSeparator

File Permissions: chmod and chown from PowerShell

PowerShell does not have cmdlets that wrap chmod or chown — you call them directly. For scripts that need to run on both platforms, wrap permission operations in a platform check. On Windows you would use ACL cmdlets (Get-Acl, Set-Acl); on Linux you call the native tools.

function Set-ScriptPermissions {
    param([string]$Path)
    if ($IsLinux -or $IsMacOS) {
        chmod 750 $Path
        Write-Host "Set Unix permissions 750 on $Path"
    } else {
        $acl  = Get-Acl -Path $Path
        $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
            'BUILTIN\Administrators', 'FullControl', 'Allow'
        )
        $acl.SetAccessRule($rule)
        Set-Acl -Path $Path -AclObject $acl
        Write-Host "Set Windows ACL on $Path"
    }
}

Writing Scripts That Run Unmodified on Both OSes

Beyond paths and environment variables, the other silent killer is CRLF line endings. A PS1 file saved with Windows line endings will parse and run on Linux in most cases, but a shebang line (#!/usr/bin/env pwsh) followed by a carriage return will cause the kernel to fail to find the interpreter. Save cross-platform scripts with LF-only line endings. In VS Code, set the end-of-line sequence in the status bar or add "files.eol": "\n" to your workspace settings. In Git, use a .gitattributes entry: *.ps1 text eol=lf.

Common Errors

  • Script uses backslash paths hardcoded — fails silently on Linux. Linux treats a backslash as a valid character in a filename, not a separator, so Join-Path 'config' 'settings.json' creates a literal file named config\settings.json rather than traversing a directory. Always use Join-Path or [IO.Path]::Combine().
  • CRLF line endings in a PS1 file cause parse errors on Linux. If the shebang line ends with \r\n, Linux’s kernel sees the interpreter as /usr/bin/env pwsh\r, which does not exist, and returns “No such file or directory.” Convert the file with sed -i 's/\r//' script.ps1 or save from your editor with LF endings.

Related Cmdlets / See Also

Wrapping Up

Cross-platform PowerShell scripting is mostly about discipline: avoid hardcoded separators, save with LF line endings, use $IsWindows for platform branches, and prefer [IO.Path] and [Environment] methods over string concatenation. Scripts written with those habits will run on Ubuntu, RHEL, and Windows Server without a single change.

Send-Item -To