适用于 PowerShell 7.0 及以上版本(Windows / Linux / macOS)
PowerShell 7 是微软推出的跨平台版本,基于 .NET 构建,可以在 Windows、Linux 和 macOS 三个操作系统上运行。这为运维工程师带来了一个统一脚本语言的可能性——同一套 PowerShell 脚本理论上可以在不同平台上执行,减少了学习成本和维护负担。但现实中的跨平台开发远比”能跑起来”复杂得多。
不同操作系统在文件系统结构、路径规范、大小写敏感性、权限模型、包管理方式等方面存在显著差异。如果不做适配,一个在 Windows 上完美运行的脚本放到 Linux 上可能处处报错。比如路径分隔符的差异(\ vs /)、环境变量的读取方式、甚至某些 cmdlet 的可用性都会因平台而异。
本文将从平台检测与条件分支、路径与文件系统差异处理、跨平台工具函数库三个维度,介绍如何编写真正能在三大平台上稳定运行的 PowerShell 脚本。掌握这些技巧后,你可以将运维自动化脚本打包成跨平台模块,在混合环境中统一管理。
平台检测与条件分支
PowerShell 7 提供了三个内置布尔变量来标识当前运行平台:$IsWindows、$IsLinux 和 $IsMacOS。利用它们可以在脚本中实现条件分支,根据不同平台执行不同的逻辑。下面这个函数封装了平台检测逻辑,返回统一的结构化信息,方便在脚本的任何位置引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| function Get-PlatformInfo { [CmdletBinding()] param()
$result = [ordered]@{ Platform = 'Unknown' IsWindows = $false IsLinux = $false IsMacOS = $false Architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() OSVersion = '' ShellName = $PSVersionTable.ShellId PSVersion = $PSVersionTable.PSVersion.ToString() }
if ($IsWindows) { $result.Platform = 'Windows' $result.IsWindows = $true $result.OSVersion = [System.Environment]::OSVersion.VersionString } elseif ($IsLinux) { $result.Platform = 'Linux' $result.IsLinux = $true if (Test-Path /etc/os-release) { $osRelease = Get-Content /etc/os-release -ErrorAction SilentlyContinue $prettyName = $osRelease | Where-Object { $_ -match '^PRETTY_NAME=' } if ($prettyName) { $result.OSVersion = ($prettyName -split '=', 2)[1].Trim('"') } } } elseif ($IsMacOS) { $result.Platform = 'macOS' $result.IsMacOS = $true $result.OSVersion = sw_vers -productVersion 2>$null if ($result.OSVersion) { $result.OSVersion = "macOS $($result.OSVersion)" } }
return [PSCustomObject]$result }
$platform = Get-PlatformInfo Write-Host "当前平台: $($platform.Platform) ($($platform.Architecture))" Write-Host "操作系统: $($platform.OSVersion)" Write-Host "PowerShell 版本: $($platform.PSVersion)"
|
在 Windows 上运行的输出大致如下:
1 2 3
| 当前平台: Windows (X64) 操作系统: Microsoft Windows 10.0.26100 PowerShell 版本: 7.5.0
|
在 Linux (Ubuntu) 上运行的输出大致如下:
1 2 3
| 当前平台: Linux (X64) 操作系统: Ubuntu 24.04.2 LTS PowerShell 版本: 7.5.0
|
路径与文件系统差异处理
路径处理是跨平台脚本开发中最容易踩坑的地方。Windows 使用反斜杠 \ 作为路径分隔符,而 Linux 和 macOS 使用正斜杠 /。此外,Linux 的文件系统区分大小写,Windows 的 NTFS 默认不区分。下面这个函数库封装了常见的路径操作,确保在所有平台上行为一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| function Join-PlatformPath { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Base,
[Parameter(Mandatory)] [string]$Relative )
return [System.IO.Path]::Combine($Base, $Relative) }
function Get-PlatformTempPath { [CmdletBinding()] param()
if ($IsWindows) { return $env:TEMP } elseif ($IsMacOS -or $IsLinux) { $tmpDir = $env:TMPDIR if (-not $tmpDir) { $tmpDir = $env:XDG_RUNTIME_DIR } if (-not $tmpDir) { $tmpDir = '/tmp' } return $tmpDir } return '/tmp' }
function Get-PlatformHomePath { [CmdletBinding()] param()
if ($IsWindows) { return $env:USERPROFILE } return $env:HOME }
function Resolve-PlatformCase { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path )
if ($IsLinux) { if (-not (Test-Path $Path)) { Write-Warning "路径不存在: $Path" return $Path } $item = Get-Item $Path -ErrorAction SilentlyContinue if ($item) { return $item.FullName } } return (Resolve-Path $Path -ErrorAction SilentlyContinue).Path }
$homePath = Get-PlatformHomePath $configDir = Join-PlatformPath $homePath '.myapp' $configFile = Join-PlatformPath $configDir 'config.json' $tempDir = Get-PlatformTempPath
Write-Host "主目录: $homePath" Write-Host "配置目录: $configDir" Write-Host "配置文件: $configFile" Write-Host "临时目录: $tempDir"
if (-not (Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null Write-Host "已创建配置目录: $configDir" }
|
在 Windows 上的输出:
1 2 3 4 5
| 主目录: C:\Users\admin 配置目录: C:\Users\admin\.myapp 配置文件: C:\Users\admin\.myapp\config.json 临时目录: C:\Users\admin\AppData\Local\Temp 已创建配置目录: C:\Users\admin\.myapp
|
在 Linux 上的输出:
1 2 3 4 5
| 主目录: /home/admin 配置目录: /home/admin/.myapp 配置文件: /home/admin/.myapp/config.json 临时目录: /tmp 已创建配置目录: /home/admin/.myapp
|
跨平台工具函数库
在实际运维中,最常见的需求包括软件包管理、服务管理和用户管理。不同平台使用的底层命令各不相同:Windows 用 winget/choco,Linux 用 apt/yum/dnf,macOS 用 brew;服务管理在 Windows 上是 Get-Service,Linux 上是 systemctl,macOS 上是 launchctl。下面这个函数库将这些差异封装为统一的 PowerShell 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
|
function Install-PlatformPackage { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$Name,
[Parameter()] [switch]$Update )
if ($IsWindows) { $winget = Get-Command winget -ErrorAction SilentlyContinue if ($winget) { $args = @('install', '--id', $Name, '--accept-package-agreements', '--accept-source-agreements') if ($Update) { $args += '--upgrade' } if ($PSCmdlet.ShouldProcess($Name, 'Install via winget')) { & winget @args } } else { $choco = Get-Command choco -ErrorAction SilentlyContinue if ($choco) { if ($PSCmdlet.ShouldProcess($Name, 'Install via chocolatey')) { & choco install $Name -y } } else { Write-Error "未找到可用的包管理器(winget 或 chocolatey)" } } } elseif ($IsMacOS) { if ($PSCmdlet.ShouldProcess($Name, 'Install via brew')) { if ($Update) { & brew upgrade $Name } else { & brew install $Name } } } elseif ($IsLinux) { $pkgMgr = Get-LinuxPackageManager if ($PSCmdlet.ShouldProcess($Name, "Install via $pkgMgr")) { switch ($pkgMgr) { 'apt' { & sudo apt-get install -y $Name } 'dnf' { & sudo dnf install -y $Name } 'yum' { & sudo yum install -y $Name } 'pacman' { & sudo pacman -S --noconfirm $Name } default { Write-Error "不支持的包管理器: $pkgMgr" } } } } }
function Get-LinuxPackageManager { [CmdletBinding()] param()
if (Get-Command apt-get -ErrorAction SilentlyContinue) { return 'apt' } if (Get-Command dnf -ErrorAction SilentlyContinue) { return 'dnf' } if (Get-Command yum -ErrorAction SilentlyContinue) { return 'yum' } if (Get-Command pacman -ErrorAction SilentlyContinue) { return 'pacman' } return $null }
function Get-PlatformService { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Name )
if ($IsWindows) { return Get-Service -Name $Name -ErrorAction SilentlyContinue } elseif ($IsLinux) { $status = systemctl is-active $Name 2>$null $enabled = systemctl is-enabled $Name 2>$null return [PSCustomObject]@{ Name = $Name Status = if ($status -eq 'active') { 'Running' } else { 'Stopped' } Enabled = ($enabled -eq 'enabled') } } elseif ($IsMacOS) { $loaded = launchctl list $Name 2>$null return [PSCustomObject]@{ Name = $Name Status = if ($loaded) { 'Running' } else { 'Stopped' } Enabled = [bool]$loaded } } }
function Get-PlatformUser { [CmdletBinding()] param( [Parameter()] [string]$UserName = $env:USER )
if ($IsWindows) { $adsi = [ADSI]"WinNT://$env:COMPUTERNAME" $user = $adsi.Children | Where-Object { $_.SchemaClassName -eq 'User' -and $_.Name -eq $UserName } if ($user) { return [PSCustomObject]@{ Name = $user.Name[0] FullName = $user.FullName[0] Description = $user.Description[0] HomeDir = Join-PlatformPath $env:USERPROFILE $user.Name[0] Platform = 'Windows' } } } else { $passwd = getent passwd $UserName 2>$null if ($passwd) { $fields = $passwd -split ':' return [PSCustomObject]@{ Name = $fields[0] FullName = ($fields[4] -split ',', 2)[0] Description = '' HomeDir = $fields[5] Shell = $fields[6] Platform = if ($IsMacOS) { 'macOS' } else { 'Linux' } } } } }
$platform = Get-PlatformInfo Write-Host "`n===== 跨平台工具函数演示 =====" Write-Host "平台: $($platform.Platform)"
$currentUser = Get-PlatformUser Write-Host "当前用户: $($currentUser.Name)" Write-Host "主目录: $($currentUser.HomeDir)"
$tempPath = Get-PlatformTempPath Write-Host "临时目录: $tempPath"
|
在 Windows 上运行的输出:
1 2 3 4 5
| ===== 跨平台工具函数演示 ===== 平台: Windows 当前用户: admin 主目录: C:\Users\admin\admin 临时目录: C:\Users\admin\AppData\Local\Temp
|
在 Linux 上运行的输出:
1 2 3 4 5
| ===== 跨平台工具函数演示 ===== 平台: Linux 当前用户: admin 主目录: /home/admin 临时目录: /tmp
|
注意事项
始终使用 [System.IO.Path] 类处理路径拼接:不要手动拼接路径分隔符。[System.IO.Path]::Combine() 会自动根据当前平台选择正确的分隔符,从根本上避免路径格式错误。
利用 $PSVersionTable 做版本检查:跨平台脚本开头建议加上版本守卫,确保运行环境是 PowerShell 7+。例如 if ($PSVersionTable.PSVersion.Major -lt 7) { throw '此脚本需要 PowerShell 7.0 或更高版本' }。
注意 cmdlet 的平台差异:并非所有 cmdlet 在所有平台上都可用。例如 Get-Service 只在 Windows 上可用,Set-Clipboard 在 Linux 上需要 xclip 依赖。编写脚本前用 Get-Command 检查目标 cmdlet 的可用性。
文件权限模型不同:Windows 使用 ACL(访问控制列表),Linux/macOS 使用 POSIX 权限(rwx)。如果脚本涉及权限设置,需要根据平台分别调用 icacls(Windows)或 chmod/chown(Linux/macOS)。
环境变量大小写敏感:Windows 的环境变量名不区分大小写,而 Linux/macOS 区分。$env:PATH 在所有平台上都能工作,但自定义环境变量如 $env:MyApp_Home 在 Linux 上必须与设置时完全一致(包括大小写)。
测试覆盖三大平台:推荐使用 Docker(Linux)、WSL(Linux 交叉测试)和本地 macOS 环境进行实际验证。仅靠阅读代码很难发现所有平台特定问题,自动化测试是最好的保障。有条件的话可以使用 GitHub Actions 的多平台矩阵(os: [windows-latest, ubuntu-latest, macos-latest])实现 CI 级别的跨平台验证。