PowerShell 跨平台脚本

讲解 PowerShell 的对象管道与 cmdlet 体系、与 POSIX 工具的双向互操作、SSH 远程执行与凭据管理,并系统梳理 Linux 与 macOS 上的跨平台差异、编码陷阱、CI 落地要点与常见排错速查表,给出可复用的跨平台脚本骨架。

1. 对象管道:PowerShell 的立身之本

一句话总结: bash 的管道传的是文本行,PowerShell 的管道传的是 .NET 对象——这个差异决定了它不需要 awk 和 cut,但也决定了它必须先理解「对象类型」才能写好脚本。

1.1 文本管道 vs 对象管道

同一件事——列出占用最大的 5 个进程——两种模型的写法差异:

# bash:靠列位置约定,ps 的输出格式变了就崩
ps aux | sort -k 4 -nr | head -5 | awk '{print $2, $4, $11}'
# PowerShell:按属性名取值,输出格式变化不影响逻辑
Get-Process |
    Sort-Object -Property WorkingSet64 -Descending |
    Select-Object -First 5 Id, WorkingSet64, ProcessName

bash 里 awk '{print $2}' 依赖 ps 输出的第二列恰好是 PID;一旦 ps 版本换了列顺序,脚本静默算错。PowerShell 里 Sort-Object WorkingSet64 依赖的是对象上真实存在的属性,编译器与运行时都能校验。

1.2 cmdlet 命名规范

PowerShell 的所有内置命令都遵守 动词-名词 结构,且动词来自一份受控列表:

动词语义例子
Get读取Get-ChildItem
Set修改Set-Content
New创建New-Item
Remove删除Remove-Item
Test判定Test-Path
Invoke执行Invoke-Command
# 按动词前缀发现命令,无需记忆完整名字
Get-Command -Verb Get -Noun *Item
# 查某个命令的用法
Get-Help Get-ChildItem -Examples
# 查看命令返回的对象长什么样
Get-Process | Get-Member

Get-Member 是 PowerShell 里最重要的调试命令:它告诉你管道里流动的到底是什么对象、有哪些属性和方法,等价于 bash 里「先看一眼输出再决定怎么切」。

1.3 别名与兼容层

PowerShell 在类 Unix 系统上提供了一批别名,让 ls、cat、rm 这些习惯命令可用:

Get-Alias ls        # → Get-ChildItem
Get-Alias cat       # → Get-Content
Get-Alias rm        # → Remove-Item

一句话总结: ls 在 PowerShell 里是别名而非 /bin/ls,因此 ls -la 这类 GNU 参数会失败——写脚本时永远用完整 cmdlet 名,别名只留给交互。

2. 与 POSIX 工具互操作

一句话总结: PowerShell 既能调用系统二进制,也能被 bash 调用,双向互操作的关键是「显式转换」:外部命令给的是字符串,要主动转成对象。

2.1 调用外部命令

# 直接调用系统二进制,参数原样传递
git status --short
curl -fsS https://example.com -o /tmp/index.html

# 外部命令输出是字符串数组,需要结构化
(df -h /) -split "`n" | Select-Object -Skip 1

外部命令的输出会以「行数组」形式进入管道,因此 Where-Object 之类的对象操作无法直接使用,必须先解析。

2.2 解析外部输出

# 方式一:正则逐行解析
$df = df -h / | Select-Object -Skip 1 |
    ForEach-Object { $_ -split '\s+' } |
    Where-Object { $_ -ne '' }

[PSCustomObject]@{
    Filesystem = $df[0]
    Size       = $df[1]
    Used       = $df[2]
    Mounted    = $df[5]
}

# 方式二:调用 JSON 友好的输出,再 ConvertFrom-Json
docker ps --format '{{json .}}' | ForEach-Object { $_ | ConvertFrom-Json }

一句话总结: 能拿到 JSON 就别解析文本——ConvertFrom-Json 把外部工具的输出一步变成对象,比手工 -split 稳健得多。

2.3 在 bash 里调用 pwsh

#!/usr/bin/env bash
set -euo pipefail

# 用 -Command 传单行,用 -File 传脚本
pwsh -NoProfile -NonInteractive -Command 'Get-Date -Format o'

# 需要可靠退出码时,显式包装
if ! pwsh -NoProfile -File ./deploy.ps1 -Env prod; then
    echo "PowerShell 部署失败" >&2
    exit 1
fi

三个开关必须记住:-NoProfile 跳过用户配置(保证 CI 与本地行为一致)、-NonInteractive 禁止提示(否则卡死流水线)、-File 让脚本以文件语义执行(参数绑定更严格)。

2.4 环境变量与路径

# 环境变量语法不同:$env:NAME
$env:API_TOKEN = "abc123"
Write-Output $env:PATH

# 路径用 Join-Path 而非字符串拼接,跨平台安全
$log = Join-Path $HOME "logs" "app.log"

# 分隔符差异由 .NET 处理
[System.IO.Path]::DirectorySeparatorChar

$env:PATH 在 Windows 上以 ; 分隔、在 Unix 上以 : 分隔,因此拼接 PATH 永远用 $env:PATH = "$newDir$([IO.Path]::PathSeparator)$env:PATH" 而不是硬编码分隔符。

3. 远程执行与凭据管理

一句话总结: PowerShell 的远程能力在跨平台后从 WinRM 转向 SSH,凭据则从 PSCredential 转向 SecretManagement——老教程里的写法在 Linux 上大概率跑不通。

3.1 从 WinRM 到 SSH

Windows 上的 Invoke-Command -ComputerName 走 WinRM;Linux/macOS 上必须显式指定 SSH 传输:

# 跨平台远程:走 SSH 传输层,复用 ~/.ssh/config 与密钥
Invoke-Command -HostName web-01 -UserName deploy -ScriptBlock {
    Get-Process | Sort-Object CPU -Descending | Select-Object -First 3
}

# 多主机并行
Invoke-Command -HostName web-01, web-02 -ScriptBlock { uptime }

前提是远端启用了 PowerShell Remoting over SSH:

# 远端 sshd_config 无需改动,只要装了 pwsh 并注册子系统
sudo "$(command -v pwsh)" -Command 'Enable-PSRemoting -Force' 2>/dev/null || true

实际上 Linux 上更常用的方式是直接 ssh host pwsh -Command '...',避免依赖子系统注册。这与 SSH 远程自动化 里的批量执行套路可以互换。

3.2 凭据对象

# 交互式收集凭据(会弹窗,CI 里禁用)
$cred = Get-Credential

# 从环境变量构造,适合 CI
$sec = ConvertTo-SecureString $env:DEPLOY_PASSWORD -AsPlainText -Force
$cred = [PSCredential]::new($env:DEPLOY_USER, $sec)

ConvertTo-SecureString -AsPlainText 只做内存中的明文包装,不提供加密;真正的密钥保管应交给 SecretManagement:

Install-Module Microsoft.PowerShell.SecretManagement -Scope CurrentUser
Register-SecretVault -Name Local -ModuleName Microsoft.PowerShell.SecretStore

Set-Secret -Name prod-token -Secret "s3cr3t"
$token = Get-Secret -Name prod-token -AsPlainText

凭据管理的通用原则(最小权限、短时效、不落盘、不进日志)在 密钥与凭据处理 中有更细的清单,PowerShell 侧同样适用。

3.3 非交互认证

# CI 中用服务账号 + 令牌,避免任何弹窗
$headers = @{ Authorization = "Bearer $env:GITLAB_TOKEN" }
Invoke-RestMethod -Uri "https://gitlab.example.com/api/v4/projects" -Headers $headers

# 失败时明确抛错,不要静默返回 $null
if (-not $result) { throw "API 返回为空,检查令牌权限" }

4. 跨平台差异与陷阱

一句话总结: PowerShell 在 Linux 上最大的坑不是语法,而是「大小写敏感、路径、编码」这三件 Windows 程序员从不操心的事。

4.1 大小写敏感性

# Windows 上两者等价;Linux 上第二条会失败
Get-Item ./Config.json
Get-Item ./config.json

更隐蔽的是哈希表与比较运算符:

# 默认 -eq 不区分大小写,Linux 上建议显式用 -ceq
"ABC" -eq "abc"    # True
"ABC" -ceq "abc"   # False

# 哈希表键在 Windows 上大小写不敏感,Linux 上敏感
$h = @{ Name = "web" }
$h["name"]         # Windows: web / Linux: 空

处理配置键时统一转小写或用 -ceq,是避免「本地能跑、CI 挂掉」的最实用手段。

4.2 路径与 PSDrive

# Windows 专属提供器在 Linux 上不存在
Get-PSDrive        # Linux 上只有 / 与临时挂载,没有 C:

# 用跨平台 API 而非硬编码
Join-Path $PSScriptRoot "config" "app.json"
Resolve-Path "~/data"
Test-Path -PathType Leaf ./a.txt

~ 在 PowerShell 里被解析成 $HOME,跨平台可用;但 C:\temp 这种绝对路径在 Linux 上会被当作相对路径处理,务必用 $env:TEMP 或 [IO.Path]::GetTempPath()。

4.3 默认编码

这是跨平台脚本最经典的静默错误来源:

# 显式指定编码,别依赖默认值
Set-Content -Path out.txt -Value $data -Encoding utf8NoBOM
Get-Content -Path in.txt -Encoding utf8

# 读取二进制要声明为 Byte
Get-Content -Path blob.bin -AsByteStream -TotalCount 16

PowerShell 7 的 utf8NoBOM 是推荐值;utf8(带 BOM)在 Linux 工具链里经常引发「文件头多了三个字节」的问题,比如 shell 脚本被加上 BOM 后 shebang 失效。

4.4 Windows 专属功能

功能Linux 可用性替代方案
注册表 HKLM:无配置文件
Get-Service部分systemctl
COM 对象无无
Get-WmiObject无Get-CimInstance(也受限)
# 用 $IsWindows / $IsLinux 做能力分支,而非 $PSVersionTable
if ($IsLinux) {
    systemctl is-active nginx
} elseif ($IsWindows) {
    (Get-Service nginx).Status
}

$IsWindows / $IsLinux / $IsMacOS 是 PowerShell 6+ 引入的自动变量,比手工判断 $PSVersionTable.Platform 更可靠。

5. 一个可复用的跨平台脚本骨架

一句话总结: 一个能同时在 Windows 与 Linux 跑的 PowerShell 脚本,必须有严格模式、显式编码、能力分支与结构化输出四件套。

#!/usr/bin/env pwsh
[CmdletBinding()]
param(
    [Parameter(Mandatory)][string]$Environment,
    [string]$OutputPath = (Join-Path ([IO.Path]::GetTempPath()) "report.json"),
    [switch]$DryRun
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)

function Get-SystemInfo {
    [CmdletBinding()]
    param()
    [PSCustomObject]@{
        OS       = if ($IsWindows) { "windows" } elseif ($IsLinux) { "linux" } else { "macos" }
        Hostname = [Environment]::MachineName
        Pwsh     = $PSVersionTable.PSVersion.ToString()
        Cores    = [Environment]::ProcessorCount
    }
}

try {
    $info = Get-SystemInfo
    if ($DryRun) {
        $info | Format-List
        return
    }
    $info | ConvertTo-Json -Depth 3 | Set-Content -Path $OutputPath -Encoding utf8NoBOM
    Write-Verbose "已写入 $OutputPath"
}
catch {
    Write-Error "采集失败: $($_.Exception.Message)"
    exit 1
}

要点逐条说明:Set-StrictMode 让未定义变量报错而非静默为空;$ErrorActionPreference = 'Stop' 等价于 bash 的 set -e;[CmdletBinding()] 启用 -Verbose 等公共参数;try/catch 包住主逻辑并返回非零退出码,让外层 bash 或 CI 能感知失败。参数校验与错误分层的通用做法,可以参考 参数解析与错误处理 。

5.1 在 CI 里调用

# GitHub Actions 中显式安装指定版本,避免 runner 默认版本漂移
- name: Run PowerShell audit
  shell: pwsh
  run: |
    ./scripts/audit.ps1 -Environment prod -OutputPath ./audit.json

GitHub Actions 的 shell: pwsh 会自动选择跨平台 PowerShell,比 powershell(仅 Windows)更通用。流水线相关的更多细节见 GitHub Actions 专题 。

5.2 与容器结合

# 用官方镜像固定版本,避免 latest 漂移
FROM mcr.microsoft.com/powershell:7.4-ubuntu-22.04

COPY scripts/ /scripts/
RUN pwsh -NoProfile -Command 'Get-Module -ListAvailable | Measure-Object'

ENTRYPOINT ["pwsh", "-NoProfile", "-File", "/scripts/entrypoint.ps1"]

镜像层面同样适用「固定版本、非交互、显式编码」三原则,容器入口脚本的通用设计可参考 容器入口脚本 。

6. 踩坑速查

症状原因处理
CI 卡住不动命令在等交互输入加 -NonInteractive
本地正常 CI 报错用户 Profile 干扰加 -NoProfile
哈希表取值拿到空Linux 键大小写敏感统一小写或 -ceq
shebang 失效文件写入带了 BOM用 utf8NoBOM
中文乱码默认编码非 UTF-8显式 -Encoding utf8
Get-Service 报错Windows 专属 cmdlet用 $IsLinux 分支
远程连接超时未指定 SSH 传输-HostName 而非 -ComputerName
退出码始终为 0未处理 $ErrorActionPreference设 Stop 并显式 exit 1

7. 总结

PowerShell 跨平台的价值不在于「用一套语法统治所有系统」,而在于它把「结构化数据 + 强类型校验 + 统一错误处理」带进了脚本世界。实践建议:

  1. 交互可以随意,脚本必须显式——完整 cmdlet 名、显式编码、显式传输层。
  2. 能拿 JSON 就别解析文本,ConvertFrom-Json 是跨工具协作的最优接口。
  3. 能力分支用自动变量($IsLinux 等),不要靠版本号猜平台。
  4. 凭据走 SecretManagement,不要 -AsPlainText 之后就写进文件。

如果你更偏好纯文本管道的世界观,zsh 与 bash 兼容移植 提供了另一条在既有 Shell 生态里做结构化处理的路径;而无论用哪种 Shell,Shell 专题总览 里的工具链与模式都可以互相借鉴。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「shell」更多文章

  1. 日志轮转与归档
  2. 监控采集与告警脚本
  3. Shell 处理二进制数据