PowerShell 技能连载 - 跨平台脚本开发

适用于 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
)

# 统一使用 [System.IO.Path] 处理,自动适配当前平台
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
#region 包管理器抽象

function Install-PlatformPackage {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$Name,

[Parameter()]
[switch]$Update
)

if ($IsWindows) {
# 优先使用 winget,回退到 choco
$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
}

#endregion

#region 服务管理抽象

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
}
}
}

#endregion

#region 用户管理抽象

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' }
}
}
}
}

#endregion

# 使用示例
$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

注意事项

  1. 始终使用 [System.IO.Path] 类处理路径拼接:不要手动拼接路径分隔符。[System.IO.Path]::Combine() 会自动根据当前平台选择正确的分隔符,从根本上避免路径格式错误。

  2. 利用 $PSVersionTable 做版本检查:跨平台脚本开头建议加上版本守卫,确保运行环境是 PowerShell 7+。例如 if ($PSVersionTable.PSVersion.Major -lt 7) { throw '此脚本需要 PowerShell 7.0 或更高版本' }

  3. 注意 cmdlet 的平台差异:并非所有 cmdlet 在所有平台上都可用。例如 Get-Service 只在 Windows 上可用,Set-Clipboard 在 Linux 上需要 xclip 依赖。编写脚本前用 Get-Command 检查目标 cmdlet 的可用性。

  4. 文件权限模型不同:Windows 使用 ACL(访问控制列表),Linux/macOS 使用 POSIX 权限(rwx)。如果脚本涉及权限设置,需要根据平台分别调用 icacls(Windows)或 chmod/chown(Linux/macOS)。

  5. 环境变量大小写敏感:Windows 的环境变量名不区分大小写,而 Linux/macOS 区分。$env:PATH 在所有平台上都能工作,但自定义环境变量如 $env:MyApp_Home 在 Linux 上必须与设置时完全一致(包括大小写)。

  6. 测试覆盖三大平台:推荐使用 Docker(Linux)、WSL(Linux 交叉测试)和本地 macOS 环境进行实际验证。仅靠阅读代码很难发现所有平台特定问题,自动化测试是最好的保障。有条件的话可以使用 GitHub Actions 的多平台矩阵(os: [windows-latest, ubuntu-latest, macos-latest])实现 CI 级别的跨平台验证。

PowerShell 技能连载 - SSH 远程管理

适用于 PowerShell 7.0 及以上版本

传统的 PowerShell Remoting 基于 WinRM(Windows Remote Management)协议,虽然功能强大,但存在明显的平台限制——它只能在 Windows 环境中工作。在企业混合环境中,运维人员往往需要同时管理 Windows 和 Linux 服务器,WinRM 的局限性就成了一个棘手的问题。此外,WinRM 在防火墙策略严格的网络中配置繁琐,端口和认证方式也不够灵活。

PowerShell 7 引入了基于 SSH 的远程会话支持,彻底改变了这一局面。通过 Enter-PSSessionInvoke-Command-HostName 参数,PowerShell 可以通过 SSH 协议建立远程连接,实现真正的跨平台远程管理。这意味着你可以从 Windows 管理 Linux 服务器、从 Linux 管理 Windows 服务器,甚至可以在云环境和容器中使用统一的远程管理体验。

SSH Remoting 不仅能与现有的 SSH 基础设施无缝集成,还支持密钥认证、跳板机(ProxyJump)、多主机并行执行等高级场景。本文将介绍如何配置和使用 PowerShell SSH Remoting,以及在混合环境中的实战技巧。

SSH 远程会话配置与基础操作

在使用 SSH Remoting 之前,需要确保目标主机已安装并运行 SSH 服务。PowerShell 7 在连接时会使用系统自带的 SSH 客户端,因此无需额外安装 WinRM。

以下示例展示如何通过 SSH 建立交互式远程会话和执行远程命令:

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
# 查看当前 PowerShell 版本,确认是否支持 SSH Remoting
$PSVersionTable.PSVersion

# 使用 Enter-PSSession 通过 SSH 连接到远程 Linux 主机
Enter-PSSession -HostName admin@192.168.1.100
# 在远程会话中执行命令
hostname
uname -a
whoami
# 退出远程会话
Exit-PSSession

# 使用 Invoke-Command 通过 SSH 在远程主机上执行脚本块
Invoke-Command -HostName admin@192.168.1.100 -ScriptBlock {
# 获取系统信息
$osInfo = Get-Content /etc/os-release | ConvertFrom-StringData
[PSCustomObject]@{
Distribution = $osInfo.NAME
Version = $osInfo.VERSION
Kernel = uname -r
Uptime = (uptime -p)
MemoryTotal = "#{math.Round((Get-Content /proc/meminfo |
Select-String 'MemTotal').ToString().Split()[1] / 1MB, 2)} GB"
}
}

# 指定 SSH 端口(非默认 22 端口的情况)
$session = New-PSSession -HostName admin@192.168.1.100 -Port 2222
Invoke-Command -Session $session -ScriptBlock {
systemctl status nginx --no-pager
}
Remove-PSSession $session

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Major  Minor  Patch  PreReleaseLabel BuildLabel
----- ----- ----- --------------- ----------
7 4 0

[admin@192.168.1.100]: PS /home/admin> hostname
web-server-01
[admin@192.168.1.100]: PS /home/admin> uname -a
Linux web-server-01 5.15.0-91-generic #101-Ubuntu SMP x86_64 GNU/Linux
[admin@192.168.1.100]: PS /home/admin> whoami
admin

Distribution : Ubuntu 22.04.3 LTS
Version : 22.04.3 LTS (Jammy Jellyfish)
Kernel : 5.15.0-91-generic
Uptime : up 42 days, 3 hours
MemoryTotal : 15.62 GB

nginx.service - A high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled)
Active: active (running) since Mon 2025-12-01 08:00:00 UTC

SSH 密钥认证与多主机管理

密码认证虽然简单,但在自动化场景中存在安全和管理上的问题。SSH 密钥认证不仅更安全,还能让脚本在无人值守的情况下自动运行。PowerShell 7 的 SSH Remoting 原生支持密钥认证,同时可以通过 SSH 配置文件管理多个目标主机。

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
# 生成 SSH 密钥对(如果尚未生成)
ssh-keygen -t ed25519 -C "powershell-remoting" -f ~/.ssh/id_ed25519

# 将公钥复制到远程主机(使用 ssh-copy-id)
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@192.168.1.100
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@192.168.1.101
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@192.168.1.102

# 配置 SSH config 文件,简化连接管理
$sshConfig = @"
Host web-server
HostName 192.168.1.100
User admin
Port 22
IdentityFile ~/.ssh/id_ed25519

Host db-server
HostName 192.168.1.101
User admin
Port 22
IdentityFile ~/.ssh/id_ed25519

Host app-server
HostName 192.168.1.102
User admin
Port 2222
IdentityFile ~/.ssh/id_ed25519
"@

# 将配置写入 SSH config 文件
$sshConfigPath = "$HOME/.ssh/config"
$sshConfig | Set-Content -Path $sshConfigPath -Force
(Get-Item $sshConfigPath).Attributes = 'ReadOnly'

# 使用 SSH 别名直接连接(无需输入密码)
Enter-PSSession -HostName web-server

# 通过密钥认证在多台主机上并行执行命令
$servers = @('web-server', 'db-server', 'app-server')
$results = Invoke-Command -HostName $servers -ScriptBlock {
[PSCustomObject]@{
Host = $env:COMPUTERNAME ?? hostname
OS = try { (Get-Content /etc/os_release -ErrorAction Stop |
Select-String 'PRETTY_NAME').ToString().Split('"')[1] }
catch { $PSVersionTable.OS }
PSVersion = $PSVersionTable.PSVersion.ToString()
CPU_Usage = (Get-Process | Measure-Object CPU -Maximum).Maximum
Disk_Free = (Get-PSResourceInfo -Available -ErrorAction SilentlyContinue |
Measure-Object).Count
}
} | Select-Object Host, OS, PSVersion

$results | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
Generating public/private ed25519 key pair.
Your identification has been saved in /home/user/.ssh/id_ed25519
Your public key has been saved in /home/user/.ssh/id_ed25519.pub

Host OS PSVersion
---- -- ---------
web-server Ubuntu 22.04.3 LTS 7.4.0
db-server Ubuntu 22.04.3 LTS 7.4.0
app-server Debian 12.4 7.3.2

混合环境自动化:Windows + Linux 批量操作

在企业环境中,Windows 和 Linux 服务器通常共存。SSH Remoting 让我们可以用统一的 PowerShell 脚本同时管理两种平台,而不需要分别为 WinRM 和 SSH 编写不同的管理逻辑。下面的示例展示了如何在混合环境中进行批量巡检和配置管理。

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
# 定义混合主机列表(Windows 用 -ComputerName,Linux 用 -HostName)
$windowsServers = @('WIN-SVR01', 'WIN-SVR02')
$linuxServers = @('web-server', 'db-server', 'app-server')

# Windows 主机通过 WinRM 获取信息
$winResults = Invoke-Command -ComputerName $windowsServers -ScriptBlock {
[PSCustomObject]@{
Host = $env:COMPUTERNAME
Platform = 'Windows'
OS_Version = [System.Environment]::OSVersion.VersionString
CPU_Count = $env:NUMBER_OF_PROCESSORS
Memory_GB = [math]::Round(
(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2
)
Disk_Free_GB = [math]::Round(
(Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
Measure-Object FreeSpace -Sum).Sum / 1GB, 2
)
Uptime_Days = ((Get-Date) - (Get-CimInstance Win32_OperatingSystem).
LastBootUpTime).Days
}
}

# Linux 主机通过 SSH 获取信息
$linuxResults = Invoke-Command -HostName $linuxServers -ScriptBlock {
$memInfo = Get-Content /proc/meminfo
$memTotal = [math]::Round(
($memInfo | Select-String 'MemTotal').ToString().Split()[1] / 1MB, 2
)
$diskFree = [math]::Round(
(df -BG / | Select-Object -Last 1).ToString().Split()[3] -replace 'G',''
)
$uptimeDays = [math]::Floor(
(Get-Content /proc/uptime).ToString().Split('.')[0] / 86400
)

[PSCustomObject]@{
Host = hostname
Platform = 'Linux'
OS_Version = (Get-Content /etc/os-release |
Select-String 'PRETTY_NAME').ToString().Split('"')[1]
CPU_Count = nproc
Memory_GB = $memTotal
Disk_Free_GB = $diskFree
Uptime_Days = $uptimeDays
}
}

# 合并结果并生成巡检报告
$report = $winResults + $linuxResults |
Sort-Object Platform, Host |
Format-Table Host, Platform, OS_Version, CPU_Count,
Memory_GB, Disk_Free_GB, Uptime_Days -AutoSize

$report | Out-String | Write-Host

# 导出为 CSV 文件
$winResults + $linuxResults |
Sort-Object Platform, Host |
Export-Csv -Path "./server-inventory-$(Get-Date -Format 'yyyyMMdd').csv" `
-NoTypeInformation -Encoding UTF8

Write-Host "巡检报告已导出到 CSV 文件" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
7
8
9
Host        Platform OS_Version                      CPU_Count Memory_GB Disk_Free_GB Uptime_Days
---- -------- ---------- --------- --------- ------------ -----------
WIN-SVR01 Windows Microsoft Windows Server 2022 8 32.00 156.43 45
WIN-SVR02 Windows Microsoft Windows Server 2022 4 16.00 89.21 30
app-server Linux Debian GNU/Linux 12 (bookworm) 2 7.81 34 62
db-server Linux Ubuntu 22.04.3 LTS (Jammy) 4 15.62 120 42
web-server Linux Ubuntu 22.04.3 LTS (Jammy) 2 3.91 18 42

巡检报告已导出到 CSV 文件

注意事项

  1. SSH 服务必须预装:目标 Linux 主机需要安装并启动 sshd 服务,同时确保 PowerShell 7 已安装在远端(否则 Enter-PSSession 只会进入普通的 SSH Shell,而非 PowerShell 远程会话)。

  2. 认证方式选择:生产环境应优先使用 SSH 密钥认证(Ed25519 或 RSA),避免在脚本中硬编码密码。密钥的私钥文件务必设置严格的文件权限(chmod 600)。

  3. SSH 配置文件的作用:通过 ~/.ssh/config 文件管理主机别名、端口和密钥路径,可以大幅简化 Enter-PSSession -HostName 的使用,同时还能配置跳板机(ProxyJump)和连接超时等参数。

  4. 混合环境注意区分参数:Windows 主机使用 -ComputerName(走 WinRM),Linux 主机使用 -HostName(走 SSH)。如果 Windows 主机也配置了 SSH 服务,也可以统一使用 -HostName 参数。

  5. 错误处理与超时:SSH 连接可能因网络问题超时,建议在脚本中设置 $PSSessionOption 的超时参数,并使用 try-catch 包裹远程操作,避免单台主机故障导致整个批量任务中断。

  6. 安全加固建议:建议在 sshd_config 中禁用密码登录(PasswordAuthentication no)、禁用 root 远程登录(PermitRootLogin no),并使用 AllowUsers 限制可远程连接的用户范围,以降低安全风险。

PowerShell 技能连载 - WSL 集成与互操作

适用于 PowerShell 7.0 及以上版本

Windows Subsystem for Linux (WSL) 已经从一个实验性的兼容层发展为成熟的 Linux 运行环境。WSL2 基于真正的 Linux 内核,支持 systemd、Docker 以及绝大多数原生 Linux 应用。对于日常在 Windows 上工作的运维工程师和开发者来说,WSL 提供了一条低成本的 Linux 工具链接入路径——无需双系统,无需虚拟机管理程序的开销。

PowerShell 与 WSL 的互操作不仅限于简单的命令转发。借助 wsl 命令行工具、\\wsl$ 网络路径以及双向的进程调用机制,可以在一个脚本中自由混合 Windows 和 Linux 工具。例如,用 PowerShell 采集 Windows 事件日志,再通过管道传给 WSL 中的 awk 做文本分析;或者反过来,在 WSL 中编译项目后调用 PowerShell 部署到 Windows 服务。这种混合工作流在 DevOps 和跨平台自动化场景中尤为实用。

本文将从实例管理、跨平台数据交换和自动化部署三个维度,展示 PowerShell 与 WSL 深度集成的实战技巧。

管理 WSL 实例生命周期

日常工作中,我们经常需要创建、备份、迁移 WSL 实例。PowerShell 可以把 wsl.exe 的能力封装成可复用的管理函数,实现实例的批量运维。

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
# 定义 WSL 实例管理工具集
function Get-WslInstance {
<# 获取所有 WSL 发行版的详细信息 #>
$raw = wsl --list --verbose 2>&1
$lines = $raw | Select-Object -Skip 2 | Where-Object { $_ -match '\S' }

foreach ($line in $lines) {
$parts = $line.Trim() -split '\s+'
[PSCustomObject]@{
IsDefault = $line -match '^\*'
Name = $parts[0] -replace '^\*', ''
State = $parts[1]
Version = $parts[2]
}
}
}

function Export-WslInstance {
<# 将 WSL 实例导出为 tar 文件,用于备份或迁移 #>
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$OutputPath
)

if (-not (Test-Path (Split-Path $OutputPath -Parent))) {
New-Item -ItemType Directory -Path (Split-Path $OutputPath -Parent) -Force |
Out-Null
}

Write-Host "正在导出 $Name ..." -ForegroundColor Cyan
wsl --export $Name $OutputPath

$size = [math]::Round((Get-Item $OutputPath).Length / 1GB, 2)
Write-Host "导出完成:$OutputPath (${size} GB)" -ForegroundColor Green
}

function Import-WslInstance {
<# 从 tar 文件导入为新的 WSL 实例 #>
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$InstallPath,
[Parameter(Mandatory)][string]$SourceFile
)

if (-not (Test-Path $InstallPath)) {
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
}

Write-Host "正在导入 $Name ..." -ForegroundColor Cyan
wsl --import $Name $InstallPath $SourceFile
Write-Host "导入完成:$Name => $InstallPath" -ForegroundColor Green
}

function Remove-WslInstance {
<# 注销并删除 WSL 实例(谨慎使用) #>
param(
[Parameter(Mandatory)][string]$Name,
[switch]$Force
)

if (-not $Force) {
$confirm = Read-Host "确认要删除 WSL 实例 '$Name' 吗?(yes/no)"
if ($confirm -ne 'yes') {
Write-Host "已取消" -ForegroundColor Yellow
return
}
}

wsl --unregister $Name
Write-Host "已删除 WSL 实例:$Name" -ForegroundColor Red
}

# 批量备份所有运行中的实例
$instances = Get-WslInstance | Where-Object State -eq 'Running'
$backupDir = "D:\WSL-Backups\$(Get-Date -Format 'yyyyMMdd')"

foreach ($inst in $instances) {
$tar = Join-Path $backupDir "$($inst.Name).tar"
Export-WslInstance -Name $inst.Name -OutputPath $tar
}

执行结果示例:

1
2
3
4
正在导出 Ubuntu-24.04 ...
导出完成:D:\WSL-Backups\20251217\Ubuntu-24.04.tar (1.85 GB)
正在导出 Debian ...
导出完成:D:\WSL-Backups\20251217\Debian.tar (0.92 GB)

跨平台命令调用与数据交换

PowerShell 与 WSL 之间的数据交换是互操作的核心。下面展示几种常见模式:结构化数据传递、环境变量共享,以及双向脚本编排。

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
# 封装一个通用的 WSL 命令执行器
function Invoke-WslCommand {
param(
[Parameter(Mandatory)][string]$Command,
[string]$Distribution,
[switch]$AsJson
)

$args = @()
if ($Distribution) { $args += '-d', $Distribution }
$args += '--', 'bash', '-c', $Command

$result = & wsl @args 2>&1
$result = $result | Out-String

if ($AsJson -and $result) {
try {
return $result | ConvertFrom-Json
} catch {
Write-Warning "JSON 解析失败,返回原始文本"
return $result
}
}
return $result.Trim()
}

# 场景 1:用 Linux 工具分析 Windows 日志
# 先用 PowerShell 导出事件日志为 CSV,再用 WSL 的 awk 统计
$csvPath = "$env:TEMP\security_events.csv"
Get-WinEvent -LogName Security -MaxEvents 500 |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Export-Csv $csvPath -NoTypeInformation -Encoding UTF8

# 将 Windows 路径转为 WSL 可访问的路径
$wslCsv = ($csvPath -replace '^([A-Z]):', { '/mnt/' + $_.Groups[1].Value.ToLower() } `
-replace '\\', '/')

$stats = Invoke-WslCommand "awk -F',' '{print `$3}' $wslCsv | sort | uniq -c | sort -rn"
Write-Host "安全事件级别分布:" -ForegroundColor Cyan
Write-Host $stats

# 场景 2:从 WSL 获取系统信息,返回结构化对象
$sysInfo = Invoke-WslCommand -AsJson -Command @"
cat /etc/os-release | grep -E '^(NAME|VERSION)=' | while IFS='=' read -r k v; do echo "\"`$k\": \"`$v\","; done | sed '1s/^/{/;$s/,$/}/'
"@

if ($sysInfo -is [string]) {
# 备用方案:手动收集
$osName = (Invoke-WslCommand "cat /etc/os-release | grep '^NAME=' | cut -d'=' -f2").Trim('"')
$osVer = (Invoke-WslCommand "cat /etc/os-release | grep '^VERSION=' | cut -d'=' -f2").Trim('"')
$sysInfo = [PSCustomObject]@{ NAME = $osName; VERSION = $osVer }
}

Write-Host "`nWSL 系统信息:" -ForegroundColor Cyan
Write-Host " 发行版:$($sysInfo.NAME)"
Write-Host " 版本:$($sysInfo.VERSION)"

# 场景 3:利用 WSL 中独有的工具处理数据
# 使用 xq(jq 的 XML 版本)解析 Windows 无法原生处理的 XML
$xmlReport = Invoke-WslCommand @"
if command -v xq &>/dev/null; then
curl -s https://example.com/feed.xml | xq '.rss.channel.item[] | .title' 2>/dev/null
else
echo 'xq not installed, fallback to xmlstarlet'
curl -s https://example.com/feed.xml | xmlstarlet sel -t -m '//item/title' -v '.' -n 2>/dev/null
fi
"@

Write-Host "`nRSS 标题提取:" -ForegroundColor Cyan
Write-Host $xmlReport

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
安全事件级别分布:
420 信息
45 审核
28 警告
7 错误

WSL 系统信息:
发行版:Ubuntu
版本:24.04.1 LTS (Noble Numbat)

RSS 标题提取:
PowerShell 7.5 发布:新特性一览
.NET 9 正式可用
Azure 新增多云管理功能

自动化部署场景:混合 Windows/Linux 工具链

在实际的 CI/CD 和运维自动化中,经常需要将 Windows 原生工具与 Linux 工具链串联起来。下面的示例展示了一个完整的混合部署脚本:在 WSL 中编译 Node.js 项目,然后在 Windows 上完成 IIS 部署。

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
# 混合平台部署脚本
function Invoke-HybridDeploy {
param(
[string]$ProjectName = "webapp",
[string]$WslProjectPath = "/home/user/projects/$ProjectName",
[string]$WinDeployPath = "C:\inetpub\wwwroot\$ProjectName",
[string]$Distribution = "Ubuntu-24.04"
)

# 阶段 1:在 WSL 中拉取代码并构建
Write-Host "`n[1/4] 在 WSL 中拉取最新代码..." -ForegroundColor Cyan
$pullResult = Invoke-WslCommand -Distribution $Distribution -Command @"
cd $WslProjectPath && git pull origin main 2>&1 | tail -5
"@
Write-Host $pullResult

# 阶段 2:在 WSL 中执行 npm 构建
Write-Host "`n[2/4] 在 WSL 中执行构建..." -ForegroundColor Cyan
$buildResult = Invoke-WslCommand -Distribution $Distribution -Command @"
cd $WslProjectPath &&
npm ci --prefer-offline 2>&1 | tail -3 &&
npm run build 2>&1 | tail -5
"@
Write-Host $buildResult

# 阶段 3:将构建产物从 WSL 文件系统复制到 Windows
Write-Host "`n[3/4] 复制构建产物到 Windows..." -ForegroundColor Cyan
$wslDistPath = "\\wsl$\$Distribution$WslProjectPath\dist"

if (Test-Path $WinDeployPath) {
$backup = "$WinDeployPath.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
Move-Item $WinDeployPath $backup -Force
Write-Host "已备份旧版本到 $backup" -ForegroundColor Yellow
}

Copy-Item $wslDistPath $WinDeployPath -Recurse -Force
$fileCount = (Get-ChildItem $WinDeployPath -Recurse -File).Count
Write-Host "已复制 $fileCount 个文件到 $WinDeployPath" -ForegroundColor Green

# 阶段 4:用 PowerShell 进行 Windows 端配置
Write-Host "`n[4/4] 更新 IIS 配置..." -ForegroundColor Cyan

# 确保 IIS 应用池存在
$poolName = "${ProjectName}Pool"
if (-not (Get-IISAppPool -Name $poolName -ErrorAction SilentlyContinue)) {
New-IISAppPool -Name $poolName -Force
Write-Host "已创建应用池:$poolName"
}

# 更新站点物理路径
$site = Get-IISSite -Name $ProjectName -ErrorAction SilentlyContinue
if ($site) {
$site.Applications["/"].VirtualDirectories["/"].PhysicalPath = $WinDeployPath
Write-Host "已更新站点路径" -ForegroundColor Green
}

# 验证部署结果
$indexPath = Join-Path $WinDeployPath "index.html"
if (Test-Path $indexPath) {
$hash = (Get-FileHash $indexPath -Algorithm SHA256).Hash.Substring(0, 16)
Write-Host "`n部署成功!index.html 校验:$hash" -ForegroundColor Green
}

# 阶段 5:健康检查(用 WSL 中的 curl)
Write-Host "`n健康检查..." -ForegroundColor Cyan
$health = Invoke-WslCommand -Distribution $Distribution -Command @"
curl -sS -o /dev/null -w '%{http_code} %{time_total}s' http://localhost:8080/ 2>/dev/null || echo 'unreachable'
"@
Write-Host "HTTP 状态:$health"
}

# 执行部署
Invoke-HybridDeploy -ProjectName "webapp"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[1/4] 在 WSL 中拉取最新代码...
Already up to date.
Updating 3f2a1b0..7c8d9e0
Fast-forward
src/App.js | 12 ++++++------
2 files changed, 6 insertions(+), 6 deletions(-)

[2/4] 在 WSL 中执行构建...
added 245 packages in 3.2s
> webapp@1.0.0 build
> vite build
✓ built in 4.21s

[3/4] 复制构建产物到 Windows...
已备份旧版本到 C:\inetpub\wwwroot\webapp.bak.20251217_083000
已复制 42 个文件到 C:\inetpub\wwwroot\webapp

[4/4] 更新 IIS 配置...
已更新站点路径

部署成功!index.html 校验:A3F8B2C1D4E5F607

健康检查...
HTTP 状态:200 0.032s

注意事项

  1. 路径风格转换:Windows 路径(C:\Users\)和 WSL 路径(/mnt/c/Users/)之间转换时,注意盘符大小写和斜杠方向,建议封装通用的转换函数避免手动拼接出错。
  2. \\wsl$ 网络路径性能:通过 \\wsl$\ 访问 WSL 文件系统比 /mnt/c 跨分区访问快得多,大文件操作优先在 WSL 原生文件系统中完成后再复制到 Windows。
  3. 环境变量隔离:Windows 和 WSL 的环境变量相互独立,$env:PATH 不会自动共享。如果需要传递变量,使用 WSLENV 配置共享映射规则。
  4. 并发安全:多个 PowerShell 脚本同时向同一个 WSL 实例发送命令可能导致输出交错,生产环境中建议对 WSL 调用加锁或使用独立实例。
  5. 退出码传递wsl -- command$LASTEXITCODE 返回的是 Linux 进程的退出码,但经过 PowerShell 管道处理后可能丢失,关键操作建议显式检查输出内容而非仅依赖退出码。
  6. 换行符陷阱:WSL 输出是 LF 换行,Windows 文件默认 CRLF。跨系统写入文本文件时注意使用统一的编码和换行符,否则可能导致配置文件解析失败。

PowerShell 技能连载 - PowerShell 7 新特性实战

适用于 PowerShell 7.0 及以上版本

背景

PowerShell 7 是 PowerShell 团队基于 .NET Core 重新构建的重大版本,标志着 PowerShell 从 Windows 专属工具蜕变为跨平台自动化引擎。无论你管理的服务器运行的是 Windows、Linux 还是 macOS,同一套 PowerShell 脚本都能直接运行。对于仍在使用 Windows PowerShell 5.1 的管理员来说,升级到 PowerShell 7 不仅是版本号的变更,更是开发效率的质变。

PowerShell 7 引入了大量源自 C# 和其他现代语言的新运算符与语法糖。三元运算符让条件赋值从四行缩减为一行,null 合并运算符 ?? 和 null 条件赋值 ??= 彻底改变了处理 $null 的方式,管道链操作符 &&|| 则让命令行操作如 Bash 般流畅。这些特性不是锦上添花,而是日常脚本编写中每天都在用的基础设施。

除了语法层面的改进,PowerShell 7 还带来了 ForEach-Object -Parallel 并行执行能力、结构化错误信息的 ErrorRecord 改进、以及跨平台文件路径处理等底层优化。本文将通过三个实战场景,展示这些新特性如何解决实际问题,帮助你评估升级的价值和路径。

新运算符:让脚本更简洁、更安全

PowerShell 7 引入的四组新运算符直接改变了日常脚本编写的习惯。三元运算符 ? : 替代了冗长的 if-else 赋值,null 合并运算符 ?? 让默认值处理变得优雅,null 条件赋值 ??= 避免了重复的 null 检查,管道链操作符 &&|| 则让多步操作的错误处理变得直观。下面的示例展示了这些运算符在配置管理场景中的综合运用。

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
# 场景:读取应用配置,合并默认值,根据环境执行不同的部署步骤

# 默认配置
$defaultConfig = @{
Port = 8080
MaxRetry = 3
LogLevel = "Information"
BasePath = "/opt/app"
Features = @("auth", "logging")
}

# 模拟从环境变量读取的配置(部分字段缺失)
$envConfig = @{
Port = 9090
LogLevel = $null
BasePath = $null
}

# 三元运算符:根据环境决定日志前缀
$env = $env:APP_ENV ?? "development"
$logPrefix = ($env -eq "production") ? "[PROD]" : "[DEV]"

# null 合并运算符 ?? :用默认值填充缺失的配置
$port = $envConfig.Port ?? $defaultConfig.Port
$maxRetry = $envConfig.MaxRetry ?? $defaultConfig.MaxRetry
$logLevel = $envConfig.LogLevel ?? $defaultConfig.LogLevel
$basePath = $envConfig.BasePath ?? $defaultConfig.BasePath

# null 条件赋值 ??= :只在变量为 null 时赋值
$features = $envConfig.Features
$features ??= $defaultConfig.Features

Write-Host "$logPrefix 应用配置已加载"
Write-Host " 端口: $port"
Write-Host " 最大重试: $maxRetry"
Write-Host " 日志级别: $logLevel"
Write-Host " 基础路径: $basePath"
Write-Host " 功能模块: $($features -join ', ')"

# 管道链操作符 && 和 ||
Write-Host "`n--- 部署流程 ---"

# && 前面的命令成功才执行后面的命令
# || 前面的命令失败才执行后面的命令
Test-Path $basePath && Write-Host "目录已存在: $basePath" || Write-Host "目录不存在,将使用默认路径"

# 构建最终配置对象
$appConfig = [PSCustomObject]@{
Port = $port
MaxRetry = $maxRetry
LogLevel = $logLevel
BasePath = $basePath
Features = $features
Env = $env
Prefix = $logPrefix
}

Write-Host "`n最终配置:"
$appConfig | Format-List

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[DEV] 应用配置已加载
端口: 9090
最大重试: 3
日志级别: Information
基础路径: /opt/app
功能模块: auth, logging

--- 部署流程 ---
目录不存在,将使用默认路径

最终配置:

Port : 9090
MaxRetry : 3
LogLevel : Information
BasePath : /opt/app
Features : {auth, logging}
Env : development
Prefix : [DEV]

这段代码的关键在于理解每个运算符的语义。$envConfig.Port ?? $defaultConfig.Port 中,Port$envConfig 中存在且为 9090,所以 ?? 直接返回 9090,不会 fallback 到默认值。而 $envConfig.LogLevel ?? $defaultConfig.LogLevel 中,LogLevel$null,所以 ?? 返回右侧的 "Information"$features ??= $defaultConfig.Features 则是一种”惰性初始化”模式——只有当 $features$null 时才赋值,否则保留原值。管道链操作符 &&|| 的行为与 Bash 一致:Test-Path $basePath 返回 $false 后,&& 后面的命令被跳过,|| 后面的命令被执行。

并行执行:ForEach-Object -Parallel 提升批量操作效率

在 Windows PowerShell 5.1 中处理大批量操作时,唯一的做法是串行遍历——100 台服务器逐台执行,即使每台耗时 5 秒,总计也要 8 分钟以上。PowerShell 7 的 ForEach-Object -Parallel 基于 runspace 池实现了真正的并发执行,将同样的任务压缩到数十秒内完成。但并发也带来了变量作用域、资源竞争等新问题,需要理解其工作原理才能用好。

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
# 场景:批量检查远程服务器的健康状态

# 模拟服务器列表
$servers = @(
"web-prod-01", "web-prod-02", "web-prod-03",
"api-prod-01", "api-prod-02",
"db-prod-01", "db-prod-02",
"cache-prod-01", "cache-prod-02", "cache-prod-03"
)

# 模拟健康检查函数(实际环境中会调用 Invoke-RestMethod 或 Test-Connection)
function Test-ServerHealth {
param([string]$ServerName)

# 模拟网络延迟
Start-Sleep -Milliseconds (Get-Random -Min 100 -Max 500)

$statuses = @("健康", "健康", "健康", "警告", "健康", "异常")
$status = $statuses | Get-Random

return [PSCustomObject]@{
Server = $ServerName
Status = $status
CpuUsage = [math]::Round((Get-Random -Min 10 -Max 95), 1)
MemUsage = [math]::Round((Get-Random -Min 20 -Max 90), 1)
CheckAt = Get-Date -Format "HH:mm:ss"
}
}

# 串行执行(作为对比)
$serialWatch = [System.Diagnostics.Stopwatch]::StartNew()
$serialResults = $servers | ForEach-Object {
Test-ServerHealth -ServerName $_
}
$serialWatch.Stop()

Write-Host "串行执行耗时: $($serialWatch.ElapsedMilliseconds) ms"
Write-Host "串行结果数: $($serialResults.Count)"

# 并行执行(限制并发数为 4)
$parallelWatch = [System.Diagnostics.Stopwatch]::StartNew()
$parallelResults = $servers | ForEach-Object -ThrottleLimit 4 -Parallel {
# 注意:-Parallel 脚本块内无法直接访问外部变量和函数
# 需要通过 $using: 引用外部变量,或重新定义函数

# 在脚本块内定义健康检查逻辑
$statuses = @("健康", "健康", "健康", "警告", "健康", "异常")
$status = $statuses | Get-Random

# 模拟网络延迟
Start-Sleep -Milliseconds (Get-Random -Min 100 -Max 500)

[PSCustomObject]@{
Server = $_
Status = $status
CpuUsage = [math]::Round((Get-Random -Min 10 -Max 95), 1)
MemUsage = [math]::Round((Get-Random -Min 20 -Max 90), 1)
CheckAt = Get-Date -Format "HH:mm:ss"
}
}
$parallelWatch.Stop()

Write-Host "`n并行执行耗时: $($parallelWatch.ElapsedMilliseconds) ms"
Write-Host "并行结果数: $($parallelResults.Count)"

# 使用 $using: 传递外部变量的示例
$timeoutSec = 5
$parallelWithUsing = $servers | ForEach-Object -ThrottleLimit 4 -Parallel {
$timeout = $using:timeoutSec
# 可以在日志中引用外部配置
[PSCustomObject]@{
Server = $_
Timeout = $timeout
Message = "超时设置: ${timeout}s,检查完成"
}
}

# 输出加速比
$speedup = [math]::Round($serialWatch.ElapsedMilliseconds / $parallelWatch.ElapsedMilliseconds, 1)
Write-Host "`n加速比: ${speedup}x"

# 汇总健康状态
Write-Host "`n--- 健康检查汇总 ---"
$parallelResults | Group-Object Status | ForEach-Object {
Write-Host " $($_.Name): $($_.Count) 台"
}

# 筛选异常服务器
$unhealthy = $parallelResults | Where-Object { $_.Status -ne "健康" }
if ($unhealthy) {
Write-Host "`n需要关注的服务器:"
$unhealthy | Format-Table -AutoSize
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
串行执行耗时: 3247 ms
串行结果数: 10

并行执行耗时: 812 ms
并行结果数: 10

加速比: 4.0x

--- 健康检查汇总 ---
健康: 7 台
警告: 2 台
异常: 1 台

需要关注的服务器:

Server Status CpuUsage MemUsage CheckAt
------ ------ -------- -------- -------
api-prod-01 警告 78.3 65.2 14:23:17
cache-prod-02 警告 55.9 82.4 14:23:17
db-prod-01 异常 92.1 88.7 14:23:18

并行执行的核心参数是 -ThrottleLimit,它控制同时运行的 runspace 数量。示例中设为 4,意味着 10 台服务器分 3 批执行(4+4+2),总耗时接近最长的一批而非全部之和。需要注意的关键限制是:-Parallel 脚本块运行在独立的 runspace 中,无法直接访问外部作用域的变量和函数。必须通过 $using: 语法引用外部变量(如 $using:timeoutSec),或者将函数定义在脚本块内部。这种隔离虽然增加了编码复杂度,但避免了并发访问共享状态导致的竞态条件。

跨平台兼容性:一套脚本运行在 Windows 和 Linux 上

PowerShell 7 最大的架构变化是基于 .NET Core(现为 .NET 5+)构建,使其能在 Windows、Linux 和 macOS 上原生运行。但这并不意味着所有 Windows PowerShell 脚本都能直接在 Linux 上运行——路径分隔符、注册表操作、WMI/CIM 差异等问题都需要处理。PowerShell 7 通过跨平台兼容性模块和新的 API 来弥合这些差异。

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
# 场景:编写一个跨平台的系统信息收集脚本

# 自动适应的路径处理
# PowerShell 7 对 Join-Path 和路径处理做了跨平台优化
function Get-SystemReport {
$report = [ordered]@{}

# 平台检测
$report["操作系统"] = [System.Environment]::OSVersion.Platform.ToString()
$report["机器名"] = [System.Environment]::MachineName
$report["PowerShell 版本"] = $PSVersionTable.PSVersion.ToString()
$report["是否为管理员"] = (
[System.Security.Principal.WindowsPrincipal]::new(
[System.Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole("Administrators")
) ? "是" : "否"

# 跨平台路径处理
$configDir = Join-Path $HOME "app-config"
$logDir = Join-Path $HOME "app-logs"
$dataFile = Join-Path $configDir "data.json"

$report["配置目录"] = $configDir
$report["日志目录"] = $logDir

# 创建目录(跨平台兼容)
$null = New-Item -ItemType Directory -Path $configDir -Force
$null = New-Item -ItemType Directory -Path $logDir -Force

# 跨平台环境变量访问
$report["用户主目录"] = $HOME
$report["PATH 条目数"] = ($env:PATH -split ([System.IO.Path]::PathSeparator)).Count
$report["临时目录"] = [System.IO.Path]::GetTempPath()

# 根据平台使用不同的系统信息获取方式
if ($IsWindows) {
# Windows 平台:使用 CIM 获取详细信息
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$report["系统版本"] = $os.Caption
$report["可用内存(MB)"] = [math]::Round($os.FreePhysicalMemory / 1024, 0)
$report["CPU 核心数"] = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
}
elseif ($IsLinux) {
# Linux 平台:读取 /proc 文件系统
$report["系统版本"] = (Get-Content /etc/os-release |
Where-Object { $_ -match "^PRETTY_NAME=" }) -replace 'PRETTY_NAME="(.+)"', '$1'
$report["可用内存(MB)"] = [math]::Round(
(Get-Content /proc/meminfo |
Where-Object { $_ -match "^MemAvailable:" } |
ForEach-Object { ($_ -split '\s+')[1] }) / 1024, 0
)
$report["CPU 核心数"] = (Get-Content /proc/cpuinfo |
Where-Object { $_ -match "^processor" }).Count
}
elseif ($IsMacOS) {
$report["系统版本"] = "macOS $(sw_vers -productVersion 2>$null)"
$report["CPU 核心数"] = [int](sysctl -n hw.ncpu 2>$null)
}

# 写入配置文件(跨平台路径)
$reportJson = $report | ConvertTo-Json -Depth 3
Set-Content -Path $dataFile -Value $reportJson -Encoding UTF8

return [PSCustomObject]$report
}

# 执行并展示结果
$systemReport = Get-SystemReport
Write-Host "`n=== 系统信息报告 ==="
$systemReport | Format-List

# 展示跨平台文件路径
Write-Host "`n=== 跨平台路径示例 ==="
$paths = @(
(Join-Path $HOME "Documents" "notes.txt"),
(Join-Path $HOME ".config" "app" "settings.json"),
(Join-Path $HOME "logs" "app-$(Get-Date -Format 'yyyyMMdd').log")
)
$paths | ForEach-Object {
Write-Host " $($_ -replace '\\', '/')"
}

# Windows 兼容性模块提示
if ($IsWindows) {
Write-Host "`n=== Windows 兼容性模块 ==="
$compatModules = @(
"Microsoft.PowerShell.Archive",
"Microsoft.PowerShell.Management",
"Microsoft.PowerShell.Security",
"CimCmdlets",
"Microsoft.WSMan.Management"
)
$compatModules | ForEach-Object {
$installed = Get-Module -ListAvailable -Name $_ -ErrorAction SilentlyContinue
$status = ($null -ne $installed) ? "已安装" : "未安装"
Write-Host " $_`: $status"
}
}

执行结果示例:

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
=== 系统信息报告 ===

操作系统 : Win32NT
机器名 : DESKTOP-WORKSTATION
PowerShell 版本 : 7.4.6
是否为管理员 : 否
配置目录 : C:\Users\admin\app-config
日志目录 : C:\Users\admin\app-logs
用户主目录 : C:\Users\admin
PATH 条目数 : 18
临时目录 : C:\Users\admin\AppData\Local\Temp\
系统版本 : Microsoft Windows 11 Pro
可用内存(MB) : 8192
CPU 核心数 : 8

=== 跨平台路径示例 ===
C:/Users/admin/Documents/notes.txt
C:/Users/admin/.config/app/settings.json
C:/Users/admin/logs/app-20251209.log

=== Windows 兼容性模块 ===
Microsoft.PowerShell.Archive: 已安装
Microsoft.PowerShell.Management: 已安装
Microsoft.PowerShell.Security: 已安装
CimCmdlets: 已安装
Microsoft.WSMan.Management: 已安装

跨平台脚本的关键在于善用 PowerShell 7 提供的抽象层。$IsWindows$IsLinux$IsMacOS 三个自动变量让平台检测变得简单直观,Join-Path[System.IO.Path] 类自动处理不同操作系统的路径分隔符。Windows 环境下,兼容性模块确保了 CIM、WSMan 等 Windows 特有的功能仍然可用。编写跨平台脚本时,核心逻辑应尽量使用 PowerShell 通用 cmdlet,将平台相关的代码隔离在 if ($IsWindows) 等条件块中。

注意事项

  1. ForEach-Object -Parallel 的脚本块运行在独立 runspace 中。这意味着外部作用域的变量、函数、别名都无法直接访问。必须使用 $using: 语法传递外部变量,或者将函数定义复制到脚本块内部。对于复杂的业务逻辑,建议将核心逻辑封装在模块中,在 -Parallel 脚本块内通过 Import-Module 加载。

  2. ????= 对空字符串的处理与 $null 不同"" ?? "default" 的结果是空字符串 "",而不是 "default"。null 合并运算符只在值为 $null 时才触发。如果你的配置允许空字符串表示”未设置”,需要额外判断,比如 ($config -eq "") ? $default : $config 或者 $config ?? $default 配合前期的空字符串清理。

  3. 三元运算符 ? : 的优先级低于管道$a | Some-Cmdlet ? "yes" : "no" 会被解析为 $a | (Some-Cmdlet ? "yes" : "no") 而非 ($a | Some-Cmdlet) ? "yes" : "no"。在管道表达式中使用三元运算符时,务必用括号明确优先级,避免逻辑错误。

  4. -ThrottleLimit 的默认值为 5,不宜盲目调大。并行数设置过高会消耗大量系统资源(每个 runspace 约占用数十 MB 内存),还可能触发目标服务器的限流机制。建议根据目标系统的承受能力和本地资源情况,将并发数控制在 4 到 16 之间。对于数据库操作等重 IO 场景,建议从较小的值开始测试。

  5. $IsWindows 等平台变量在 Windows PowerShell 5.1 中不存在。如果你的脚本需要同时兼容 5.1 和 7,应该先检测 $PSVersionTable.PSVersion.Major,或者在脚本开头通过 $IsWindows = $IsWindows -or ($PSVersionTable.PSVersion.Major -le 5) 来兼容旧版本。直接使用 $IsWindows 而不做版本检查,会导致 5.1 环境中变量不存在而报错。

  6. 管道链操作符 &&|| 依赖命令的退出码而非布尔返回值。对于返回 $false 或空数组的 cmdlet,&& 的行为可能不符合直觉。例如 Get-Item nonexistent.txt 抛出错误时,&& 后面的命令不会执行,但 Get-Item 加上 -ErrorAction SilentlyContinue 后不再抛错,&& 会认为命令成功而继续执行。理解”命令成功”和”命令不抛错”的区别,是正确使用管道链的关键。

PowerShell 技能连载 - SSH 远程管理

适用于 PowerShell 7.0 及以上版本(跨平台)

在传统的 Windows 环境中,PowerShell 远程管理主要依赖 WinRM(Windows Remote Management)协议和 Enter-PSSessionInvoke-Command 等 cmdlet。然而,WinRM 是 Windows 专属协议,无法在 Linux 或 macOS 上运行,这给跨平台运维带来了不小的障碍。

从 PowerShell 7.0 开始,PowerShell 正式支持基于 SSH 的远程连接。通过 SSH 传输层,你可以使用熟悉的 Enter-PSSessionInvoke-Command 连接到任何安装了 SSH 服务的远程主机,无论对方运行的是 Windows、Linux 还是 macOS。这让 PowerShell 真正成为了一门跨平台的运维语言。

本文将介绍如何配置 SSH 远程管理环境,以及如何使用 PowerShell 通过 SSH 执行远程命令、管理多台服务器。

环境准备

在使用 SSH 远程管理之前,需要确保本地和远程主机都已正确配置。首先,远程主机上需要安装并运行 SSH 服务(sshd),同时需要安装 PowerShell 7。在 Windows 上,可以通过安装 OpenSSH 可选功能来实现;在 Linux 上则通常使用系统自带的 openssh-server 包。

以下脚本检查本地 SSH 客户端是否可用,并测试到远程主机的连通性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 检查 SSH 客户端是否已安装
$sshClient = Get-Command ssh -ErrorAction SilentlyContinue
if ($sshClient) {
Write-Host "SSH 客户端版本:"
ssh -V 2>&1
} else {
Write-Host "未找到 SSH 客户端,请先安装 OpenSSH" -ForegroundColor Red
}

# 测试到远程主机的 SSH 连通性
$remoteHost = "192.168.1.100"
$sshTest = Test-Connection -ComputerName $remoteHost -Count 2 -Quiet
if ($sshTest) {
Write-Host "主机 $remoteHost 网络可达"
} else {
Write-Host "主机 $remoteHost 无法访问" -ForegroundColor Yellow
}
1
2
3
SSH 客户端版本:
OpenSSH_for_Windows_9.5, LibreSSL 3.8.2
主机 192.168.1.100 网络可达

建立 SSH 远程会话

配置好环境后,就可以使用 Enter-PSSession 通过 SSH 连接到远程主机了。与传统的 WinRM 会话不同,SSH 会话需要在 -HostName 参数中指定远程主机地址。如果 SSH 使用非默认端口,可以通过 -Port 参数指定。

以下代码演示如何建立交互式 SSH 远程会话,以及如何通过 PSSessionOption 自定义连接行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 定义远程主机信息
$remoteHost = "admin@192.168.1.100"
$sshPort = 22

# 创建会话选项(连接超时 30 秒)
$sessionOption = New-PSSessionOption -OpenTimeout 30000

# 通过 SSH 建立交互式会话
Enter-PSSession -HostName $remoteHost -Port $sshPort -Options $sessionOption

# 进入远程会话后,可以像本地一样执行命令
# PS /home/admin> $PSVersionTable
# PS /home/admin> Get-Process | Select-Object -First 5

# 退出远程会话
Exit-PSSession
1
2
3
4
5
6
7
8
9
10
11
12
13
[admin@192.168.1.100]: PS /home/admin> $PSVersionTable

Name Value
---- -----
PSVersion 7.4.6
PSEdition Core
GitCommitId 7.4.6
OS Ubuntu 24.04.1 LTS
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0

使用 Invoke-Command 执行远程命令

在实际运维场景中,我们经常需要同时在多台远程主机上执行命令。通过 Invoke-Command 配合 SSH 传输,可以向多台 Linux 或 Windows 主机批量推送脚本。Invoke-Command-HostName 参数接受一个字符串数组,因此可以一次连接多台主机。

以下示例展示如何批量查询多台远程服务器的系统信息:

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
# 定义多台远程主机
$servers = @(
"admin@web-server-01",
"admin@web-server-02",
"admin@db-server-01"
)

# 批量查询远程主机的系统信息
$results = Invoke-Command -HostName $servers -ScriptBlock {
$osInfo = Get-Content /etc/os-release |
Where-Object { $_ -match '^PRETTY_NAME=' } |
ForEach-Object { ($_ -split '=')[1].Trim('"') }

$cpuUsage = (Get-Process | Measure-Object -Property CPU -Sum).Sum
$memInfo = Get-Content /proc/meminfo |
Where-Object { $_ -match '^MemAvailable:' }

[PSCustomObject]@{
HostName = $env:COMPUTERNAME ?? hostname
OS = $osInfo
CPU_Total = [math]::Round($cpuUsage, 2)
Mem_Info = $memInfo
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
}
}

# 展示结果
$results | Format-Table -AutoSize
1
2
3
4
5
HostName        OS                         CPU_Total Mem_Info                   Timestamp
-------- -- --------- -------- ---------
web-server-01 Ubuntu 24.04.1 LTS 3.42 MemAvailable: 3842 kB 2025-09-29 10:15:32
web-server-02 Ubuntu 24.04.1 LTS 1.87 MemAvailable: 6128 kB 2025-09-29 10:15:33
db-server-01 CentOS Stream 9 8.15 MemAvailable: 12244 kB 2025-09-29 10:15:33

使用 SSH 密钥认证

在生产环境中,每次连接都输入密码既不安全也不方便。配置 SSH 密钥认证可以让 PowerShell 远程会话更加流畅。下面展示如何在 PowerShell 中生成 SSH 密钥并将公钥分发到远程主机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 检查是否已有 SSH 密钥
$sshDir = Join-Path $env:USERPROFILE ".ssh"
$keyPath = Join-Path $sshDir "id_ed25519"

if (-not (Test-Path $keyPath)) {
# 生成新的 Ed25519 密钥(更安全、更快速)
Write-Host "正在生成 SSH 密钥..." -ForegroundColor Cyan
ssh-keygen -t ed25519 -f $keyPath -C "powershell-remoting" -N '""'
Write-Host "密钥已生成:$keyPath" -ForegroundColor Green
} else {
Write-Host "已有 SSH 密钥:$keyPath"
}

# 读取公钥内容
$publicKey = Get-Content "$keyPath.pub" -Raw
Write-Host "公钥内容(前 80 字符):"
Write-Host $publicKey.Substring(0, [Math]::Min(80, $publicKey.Length))
1
2
3
4
5
6
7
正在生成 SSH 密钥...
Generating public/private ed25519 key pair.
Your identification has been saved in C:\Users\admin\.ssh\id_ed25519
Your public key has been saved in C:\Users\admin\.ssh\id_ed25519.pub
密钥已生成:C:\Users\admin\.ssh\id_ed25519
公钥内容(前 80 字符):
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxR9fG3hK7YpBzM2vWqLpC4T5JkR powershell-remoting

配置 SSH Config 文件简化连接

当需要管理大量服务器时,可以在 ~/.ssh/config 文件中预定义主机别名和连接参数。PowerShell 远程管理可以直接使用这些别名,而不必每次都输入完整的用户名和地址。

以下脚本用于生成和维护 SSH Config 文件:

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
# 定义服务器清单
$serverList = @(
[PSCustomObject]@{ Alias = "web01"; Host = "192.168.1.101"; User = "admin"; Port = 22 }
[PSCustomObject]@{ Alias = "web02"; Host = "192.168.1.102"; User = "admin"; Port = 22 }
[PSCustomObject]@{ Alias = "db01"; Host = "192.168.1.201"; User = "dba"; Port = 2222 }
[PSCustomObject]@{ Alias = "proxy"; Host = "10.0.0.50"; User = "ops"; Port = 22 }
)

# 生成 SSH Config 内容
$configLines = @()
$configLines += "# PowerShell SSH Remoting - Auto Generated"
$configLines += ""

foreach ($server in $serverList) {
$configLines += "Host $($server.Alias)"
$configLines += " HostName $($server.Host)"
$configLines += " User $($server.User)"
$configLines += " Port $($server.Port)"
$configLines += " IdentityFile ~/.ssh/id_ed25519"
$configLines += " StrictHostKeyChecking accept-new"
$configLines += ""
}

# 写入 SSH Config 文件
$sshConfigPath = Join-Path $env:USERPROFILE ".ssh" "config"
$configLines | Set-Content -Path $sshConfigPath -Encoding UTF8
Write-Host "SSH Config 已写入:$sshConfigPath" -ForegroundColor Green

# 验证配置
Write-Host "`n配置内容预览:"
Get-Content $sshConfigPath
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
SSH Config 已写入:C:\Users\admin\.ssh\config

配置内容预览:
# PowerShell SSH Remoting - Auto Generated

Host web01
HostName 192.168.1.101
User admin
Port 22
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking accept-new

Host web02
HostName 192.168.1.102
User admin
Port 22
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking accept-new

Host db01
HostName 192.168.1.201
User dba
Port 2222
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking accept-new

Host proxy
HostName 10.0.0.50
User ops
Port 22
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking accept-new

配置完成后,可以直接使用别名建立远程会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用 SSH Config 中定义的别名连接
Enter-PSSession -HostName web01

# 也可以批量执行命令
$webServers = @("web01", "web02")
$diskReport = Invoke-Command -HostName $webServers -ScriptBlock {
$df = df -h / | Select-Object -Last 1
$uptime = uptime -p
[PSCustomObject]@{
Server = hostname
Disk = $df
Uptime = $uptime
Collected = Get-Date -Format 'HH:mm:ss'
}
}

$diskReport | Format-List
1
2
3
4
5
6
7
8
9
Server    : web-server-01
Disk : /dev/sda1 50G 23G 25G 48% /
Uptime : up 42 days, 3 hours, 17 minutes
Collected : 10:22:45

Server : web-server-02
Disk : /dev/sda1 50G 18G 29G 39% /
Uptime : up 42 days, 3 hours, 22 minutes
Collected : 10:22:46

监控远程主机状态

结合 SSH 远程管理,可以编写一个实用的监控脚本,定期检查远程服务器的健康状态。以下示例通过 SSH 同时检查多台服务器的 CPU、内存和磁盘使用情况:

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
# 监控目标列表
$targets = @("web01", "web02", "db01")

$healthReport = Invoke-Command -HostName $targets -ScriptBlock {
# CPU 使用率(取 top 的第 3 行)
$cpuLine = top -bn1 | Select-Object -Index 2
$cpuIdle = if ($cpuLine -match '(\d+\.\d+)\s*id') {
[math]::Round(100 - [double]$Matches[1], 1)
} else {
-1
}

# 内存使用率
$memLine = free -m | Select-Object -Index 1
$memParts = $memLine -split '\s+'
$memTotal = [int]$memParts[1]
$memUsed = [int]$memParts[2]
$memPct = [math]::Round(($memUsed / $memTotal) * 100, 1)

# 磁盘使用率
$diskLine = df -h / | Select-Object -Last 1
$diskPct = if ($diskLine -match '(\d+)%') { [int]$Matches[1] } else { -1 }

# 判断健康状态
$status = "Healthy"
if ($cpuIdle -gt 80 -or $memPct -gt 85 -or $diskPct -gt 80) {
$status = "Warning"
}
if ($cpuIdle -gt 95 -or $memPct -gt 95 -or $diskPct -gt 90) {
$status = "Critical"
}

[PSCustomObject]@{
Server = hostname
CPU_Pct = $cpuIdle
Mem_Pct = $memPct
Disk_Pct = $diskPct
Status = $status
}
}

# 输出报告,按状态排序
$healthReport |
Sort-Object { switch ($_.Status) { "Critical" { 0 } "Warning" { 1 } default { 2 } } } |
Format-Table -AutoSize

# 统计
$critical = ($healthReport | Where-Object { $_.Status -eq "Critical" }).Count
$warning = ($healthReport | Where-Object { $_.Status -eq "Warning" }).Count
$healthy = ($healthReport | Where-Object { $_.Status -eq "Healthy" }).Count

Write-Host "`n汇总:健康 $healthy 台 / 警告 $warning 台 / 严重 $critical 台"
1
2
3
4
5
6
7
Server        CPU_Pct Mem_Pct Disk_Pct Status
------ ------- ------- -------- ------
db-server-01 12.3 88.2 72 Warning
web-server-01 5.1 42.6 48 Healthy
web-server-02 3.8 38.1 39 Healthy

汇总:健康 2 台 / 警告 1 台 / 严重 0 台

注意事项

  1. SSH 服务必须运行:远程主机上的 sshd 服务必须处于运行状态。在 Linux 上可通过 systemctl status sshd 检查,在 Windows 上可通过 Get-Service sshd 查看。

  2. PowerShell 版本要求:远程主机上必须安装 PowerShell 7 及以上版本。仅安装 SSH 不够,sshd 还需要配置 powershell 作为可用子系统(在 sshd_config 中添加 Subsystem powershell /usr/bin/pwsh -sshs -NoLogo)。

  3. 密钥权限:在 Linux/macOS 上,SSH 私钥文件的权限必须设为 600,否则 SSH 会拒绝使用该密钥。可通过 chmod 600 ~/.ssh/id_ed25519 修复。

  4. 防火墙与端口:确保本地到远程主机的 SSH 端口(默认 22)在防火墙规则中放行。如果使用非默认端口,需要在 SSH Config 中显式指定或通过 -Port 参数传入。

  5. 错误排查:如果连接失败,先用 ssh user@host 命令行工具手动测试,排除认证和网络问题。PowerShell SSH 远程管理底层依赖系统的 SSH 客户端,所以基础连通性必须先保证。

  6. 与 WinRM 的区别:SSH 远程会话不支持 CimCmdlets 和部分 WMI 相关操作(这些是 Windows 专属)。如果需要管理 Windows 专属功能(如注册表、服务控制管理器),建议对 Windows 目标仍然使用 WinRM 传输,对 Linux/macOS 目标使用 SSH 传输。

PowerShell 技能连载 - WSL 集成操作

适用于 PowerShell 5.1 及以上版本(Windows),需要安装 WSL

Windows Subsystem for Linux (WSL) 让 Windows 和 Linux 无缝协作——在 Windows 上运行 Linux 工具链,同时享受 Windows 的图形界面和生态。PowerShell 可以通过 wsl 命令与 WSL 交互,调用 Linux 命令、管理 WSL 发行版、在两个环境之间传递数据。这种能力让运维人员可以在一个终端中同时使用 PowerShell 和 Bash 的优势。

本文将讲解 PowerShell 与 WSL 的集成操作技巧。

WSL 基本管理

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
# 查看已安装的 WSL 发行版
wsl --list --verbose

# 安装新的发行版
# wsl --install -d Ubuntu-24.04

# 设置默认发行版
wsl --set-default Ubuntu-24.04

# 管理 WSL 实例
function Get-WslDistributions {
$output = wsl --list --verbose 2>&1
# 解析输出(跳过标题行)
$lines = $output | Select-Object -Skip 2 | Where-Object { $_.Trim() }

foreach ($line in $lines) {
$parts = $line.Trim() -split '\s+'
if ($parts.Count -ge 3) {
$isDefault = $line -match '^\*'
[PSCustomObject]@{
IsDefault = $isDefault
Name = $parts[0] -replace '^\*', ''
State = $parts[1]
Version = $parts[2]
}
}
}
}

Get-WslDistributions | Format-Table -AutoSize

# 启动/停止 WSL
function Start-WslInstance {
param([string]$Distribution = "Ubuntu-24.04")

Write-Host "启动 WSL:$Distribution" -ForegroundColor Cyan
wsl -d $Distribution -- echo "WSL $Distribution 已启动"
}

function Stop-WslInstance {
param([string]$Distribution)

if ($Distribution) {
wsl -t $Distribution
Write-Host "已停止 WSL:$Distribution" -ForegroundColor Yellow
} else {
wsl --shutdown
Write-Host "已停止所有 WSL 实例" -ForegroundColor Yellow
}
}

# 查看 WSL 资源使用
function Get-WslResourceUsage {
$output = wsl -- bash -c "free -h && echo '---' && df -h / && echo '---' && nproc"
Write-Host $output
}

Get-WslResourceUsage

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  NAME            STATE       VERSION
* Ubuntu-24.04 Running 2
Debian Stopped 2

IsDefault Name State Version
--------- ---- ----- -------
True Ubuntu-24.04 Running 2
False Debian Stopped 2

启动 WSL:Ubuntu-24.04
WSL Ubuntu-24.04 已启动

total used free
Mem: 15Gi 4.2Gi 11Gi
Swap: 4.0Gi 0B 4.0Gi
---
Filesystem Size Used Avail Use% Mounted on
/dev/sdb 250G 45G 205G 18% /
---
8

跨系统命令调用

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
# 从 PowerShell 调用 Linux 命令
function Invoke-WslCommand {
param(
[Parameter(Mandatory)]
[string]$Command,

[string]$Distribution = "Ubuntu-24.04"
)

$result = wsl -d $Distribution -- bash -c $Command 2>&1
return $result
}

# 使用 Linux 工具处理数据
# 用 grep 过滤日志
$nginxErrors = Invoke-WslCommand "grep -c 'ERROR' /var/log/nginx/error.log 2>/dev/null || echo 0"
Write-Host "Nginx 错误数:$nginxErrors"

# 用 awk 处理 CSV
$awkResult = Invoke-WslCommand @"
awk -F',' '{sum += $3; count++} END {print "Average:", sum/count}' /tmp/data.csv
"@
Write-Host "AWK 结果:$awkResult"

# 用 jq 处理 JSON
$jsonResult = Invoke-WslCommand "echo '{\"name\":\"test\",\"value\":42}' | jq '.value'"
Write-Host "JSON 处理:$jsonResult"

# 用 find 查找文件
$found = Invoke-WslCommand "find /home -name '*.sh' -mtime -7 2>/dev/null | head -10"
Write-Host "最近修改的脚本:`n$found"

# 从 WSL 调用 PowerShell(反向调用)
$psResult = wsl -- powershell.exe -Command "Get-Date -Format 'yyyy-MM-dd HH:mm:ss'"
Write-Host "从 WSL 调用 PowerShell:$psResult"

# 混合管道:PowerShell 采集数据,Linux 处理
$processes = Get-Process | Select-Object Name, CPU,
@{N='MemMB'; E={[math]::Round($_.WorkingSet64/1MB,1)}} |
ConvertTo-Csv -NoTypeInformation

# 通过管道传给 Linux sort
$sorted = $processes | wsl -- sort -t',' -k3 -rn | wsl -- head -5
Write-Host "`n内存排序 Top 5(Linux sort):" -ForegroundColor Cyan
Write-Host $sorted

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Nginx 错误数:23
JSON 处理:42

最近修改的脚本:
/home/user/scripts/backup.sh
/home/user/scripts/deploy.sh

从 WSL 调用 PowerShell:2025-07-25 08:30:15

内存排序 Top 5(Linux sort):
"chrome",1250.34,512.5
"vscode",234.56,345.2
"node",456.78,256.3
"msedge",345.12,198.7
"powershell",123.45,85.6

文件系统互操作

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
# 从 Windows 访问 WSL 文件
# WSL 文件系统在 \\wsl$\<发行版名>\ 下
$wslHome = "\\wsl$\Ubuntu-24.04\home"
Write-Host "WSL Home 目录内容:" -ForegroundColor Cyan
Get-ChildItem $wslHome | Select-Object Name, Length, LastWriteTime |
Format-Table -AutoSize

# 从 WSL 访问 Windows 文件
# Windows 的 C 盘在 WSL 中是 /mnt/c/
Invoke-WslCommand "ls /mnt/c/Users/$env:USERNAME/Desktop | head -5"

# 双向文件复制
function Copy-WslFile {
param(
[Parameter(Mandatory)][string]$Source,
[Parameter(Mandatory)][string]$Destination,
[ValidateSet("WindowsToWsl", "WslToWindows")]
[string]$Direction = "WindowsToWsl"
)

if ($Direction -eq "WindowsToWsl") {
# Windows 路径转 WSL 路径
$wslSource = $Source -replace '^([A-Z]):', '/mnt/$1' -replace '\\', '/'
$wslDest = $Destination
wsl -- cp -r $wslSource $wslDest
Write-Host "已复制(Win->WSL):$Source => $Destination" -ForegroundColor Green
} else {
$wslSource = $Source
$winDest = $Destination
wsl -- cp -r $wslSource "/mnt/c/$($winDest -replace '^([A-Z]):\\' , { $_.Groups[1].Value.ToLower() + '/' } -replace '\\', '/')"
Write-Host "已复制(WSL->Win):$Source => $Destination" -ForegroundColor Green
}
}

# 同步项目目录到 WSL
$projectDir = "C:\Projects\MyApp"
$wslDest = "/home/user/projects/"
Copy-WslFile -Source $projectDir -Destination $wslDest -Direction WindowsToWsl

# 在 WSL 中执行构建
Invoke-WslCommand "cd /home/user/projects/MyApp && make build"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
WSL Home 目录内容:
Name Length LastWriteTime
---- ------ -------------
user 0 2025-07-25 08:00:00
projects 0 2025-07-25 08:00:00
scripts 0 2025-07-25 08:00:00

Desktop
Documents
Downloads
已复制(Win->WSL):C:\Projects\MyApp => /home/user/projects/
Build complete.

开发环境管理

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
# 在 WSL 中管理开发环境
function Enter-WslDev {
param(
[string]$Distribution = "Ubuntu-24.04",
[string]$ProjectPath = "/home/user/projects"
)

Write-Host "进入 WSL 开发环境..." -ForegroundColor Cyan
wsl -d $Distribution -- cd $ProjectPath '&&' bash -l
}

# 检查 WSL 中的开发工具
function Test-WslDevTools {
$tools = @(
@{ Name = "git"; Command = "git --version" },
@{ Name = "node"; Command = "node --version" },
@{ Name = "python"; Command = "python3 --version" },
@{ Name = "docker"; Command = "docker --version" },
@{ Name = "make"; Command = "make --version | head -1" }
)

Write-Host "WSL 开发工具检查:" -ForegroundColor Cyan

foreach ($tool in $tools) {
try {
$version = (Invoke-WslCommand $tool.Command 2>&1) | Select-Object -First 1
if ($LASTEXITCODE -eq 0 -or $version) {
Write-Host " $($tool.Name):$version" -ForegroundColor Green
} else {
Write-Host " $($tool.Name):未安装" -ForegroundColor Yellow
}
} catch {
Write-Host " $($tool.Name):未安装" -ForegroundColor Yellow
}
}
}

Test-WslDevTools

# 在 WSL 中运行 Docker
function Invoke-WslDocker {
param(
[Parameter(Mandatory)]
[string]$Image,

[string]$Command = "echo 'Container running'"
)

$dockerCmd = "docker run --rm $Image bash -c '$Command'"
$result = Invoke-WslCommand $dockerCmd
Write-Host $result
}

# 快速启动 MySQL 测试容器
Invoke-WslDocker -Image "mysql:8.0" -Command "mysql --version"

执行结果示例:

1
2
3
4
5
6
7
8
WSL 开发工具检查:
git:git version 2.45.1
node:v20.14.0
python:Python 3.12.4
docker:Docker version 26.1.3
make:GNU Make 4.3

mysql Ver 8.0.38 for Linux on x86_64 (MySQL Community Server - GPL)

注意事项

  1. 路径转换:Windows 使用反斜杠和盘符(C:\),WSL 使用正斜杠和 /mnt/c/,注意转换
  2. 性能差异:跨文件系统(/mnt/c/)的 IO 性能远低于 WSL 原生文件系统(~/
  3. 换行符:Windows 用 \r\n,Linux 用 \n,跨系统处理文本时注意换行符转换
  4. 网络访问:WSL2 有独立的虚拟网络,从 WSL 访问 Windows 服务使用 $(hostname).local
  5. systemd 支持:WSL2 支持 systemd,需要在 /etc/wsl.conf 中启用
  6. 内存限制:WSL2 默认使用系统一半内存,可通过 .wslconfig 文件配置

PowerShell 技能连载 - 跨平台 PowerShell 实践

适用于 PowerShell 7.0 及以上版本(跨平台)

PowerShell 7 的跨平台能力是一个里程碑式的变化——同一套脚本语言可以在 Windows、Linux 和 macOS 上运行。对于管理混合环境的运维团队来说,这意味着只需学习一种语言就能管理所有平台。但跨平台并非”写一次到处运行”那么简单,不同操作系统的路径格式、包管理器、服务管理和服务发现机制都有差异。

本文将讲解跨平台 PowerShell 脚本的最佳实践,包括平台检测、路径处理、包管理适配和实用案例。

安装 PowerShell 7

在 Linux 上安装 PowerShell 7 比较简单,各主流发行版都有官方包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 检查当前平台信息
$PSVersionTable
Write-Host "操作系统:$( [System.Runtime.InteropServices.RuntimeInformation]::OSDescription )"
Write-Host "平台架构:$( [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture )"

# 平台检测变量
Write-Host "`n内置平台变量:"
Write-Host " `$IsWindows : $IsWindows"
Write-Host " `$IsLinux : $IsLinux"
Write-Host " `$IsMacOS : $IsMacOS"

# Ubuntu/Debian 安装命令(在 Bash 中运行)
# wget 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

# CentOS/RHEL 安装
# sudo yum install -y https://packages.microsoft.com/config/rhel/8/packages-microsoft-prod.rpm
# sudo yum install -y powershell

# macOS 安装
# brew install --cask powershell

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name                           Value
---- -----
PSVersion 7.4.2
PSEdition Core
Platform Unix
OS Ubuntu 22.04.4 LTS

操作系统:Linux 5.15.0-101-generic #111-Ubuntu SMP
平台架构:X64

内置平台变量:
$IsWindows : False
$IsLinux : True
$IsMacOS : False

跨平台路径处理

路径分隔符是跨平台脚本最常见的陷阱。Windows 使用反斜杠 \,Linux/macOS 使用正斜杠 /。PowerShell 7 推荐使用 Join-PathSplit-Path 来处理路径:

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
# 错误做法:硬编码路径分隔符
# $path = "C:\Users\admin\Documents" # Windows only
# $path = "/home/admin/documents" # Linux only

# 正确做法:使用 Join-Path
$configDir = Join-Path $HOME "config"
$dataFile = Join-Path $configDir "data.json"
Write-Host "配置文件路径:$dataFile"

# 跨平台常用路径映射
function Get-PlatformPath {
param([string]$Segment)

$basePaths = @{
Config = if ($IsWindows) { Join-Path $env:APPDATA "MyApp" }
elseif ($IsMacOS) { Join-Path $HOME "Library" "Application Support" "MyApp" }
else { Join-Path $HOME ".config" "myapp" }
Data = if ($IsWindows) { Join-Path $env:LOCALAPPDATA "MyApp" }
elseif ($IsMacOS) { Join-Path $HOME "Library" "Application Support" "MyApp" "Data" }
else { Join-Path $HOME ".local" "share" "myapp" }
Temp = if ($IsWindows) { $env:TEMP }
else { "/tmp" }
Log = if ($IsWindows) { Join-Path $env:LOCALAPPDATA "MyApp" "Logs" }
else { Join-Path "/var" "log" "myapp" }
}

return $basePaths[$Segment]
}

foreach ($type in @('Config', 'Data', 'Temp', 'Log')) {
$path = Get-PlatformPath -Segment $type
Write-Host "$type => $path"
}

执行结果示例:

1
2
3
4
5
6
配置文件路径:/home/admin/config/data.json

Config => /home/admin/.config/myapp
Data => /home/admin/.local/share/myapp
Temp => /tmp
Log => /var/log/myapp

注意:在 PowerShell 7 中,即使 Windows 上也可以使用正斜杠 / 作为路径分隔符。但为了代码清晰,始终使用 Join-Path 是最佳实践。

跨平台服务管理

不同平台使用不同的服务管理器。以下是统一的跨平台服务管理函数:

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
function Get-ServiceStatus {
<#
.SYNOPSIS
跨平台获取服务状态
#>
param(
[Parameter(Mandatory)]
[string]$ServiceName
)

if ($IsWindows) {
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($svc) {
return [PSCustomObject]@{
Name = $svc.Name
Status = $svc.Status.ToString()
StartType = $svc.StartType.ToString()
}
}
} else {
# Linux/macOS 使用 systemctl 或 launchctl
if (Get-Command systemctl -ErrorAction SilentlyContinue) {
$output = systemctl is-active $ServiceName 2>$null
$enabled = systemctl is-enabled $ServiceName 2>$null
return [PSCustomObject]@{
Name = $ServiceName
Status = if ($output -eq 'active') { 'Running' } else { 'Stopped' }
StartType = if ($enabled -eq 'enabled') { 'Automatic' } else { 'Manual' }
}
} elseif ($IsMacOS -and (Get-Command launchctl -ErrorAction SilentlyContinue)) {
$output = launchctl list 2>$null | Select-String $ServiceName
return [PSCustomObject]@{
Name = $ServiceName
Status = if ($output) { 'Running' } else { 'Stopped' }
StartType = 'Unknown'
}
}
}
}

# 跨平台重启服务
function Restart-PlatformService {
param([string]$ServiceName)

if ($IsWindows) {
Restart-Service -Name $ServiceName -Force
} elseif (Get-Command systemctl -ErrorAction SilentlyContinue) {
sudo systemctl restart $ServiceName
} elseif ($IsMacOS) {
sudo launchctl stop "com.$ServiceName"
sudo launchctl start "com.$ServiceName"
}
Write-Host "已重启服务:$ServiceName" -ForegroundColor Green
}

# 示例
Get-ServiceStatus -ServiceName 'ssh'
Get-ServiceStatus -ServiceName 'nginx'

执行结果示例:

1
2
3
4
5
6
7
Name Status StartType
---- ------ ---------
ssh Running Automatic

Name Status StartType
---- ------ ---------
nginx Running Automatic

跨平台系统信息采集

统一的系统信息采集函数,适配三个平台:

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
function Get-CrossPlatformSystemInfo {
<#
.SYNOPSIS
跨平台采集系统信息
#>

$info = [ordered]@{}

# 基础平台信息
$info['主机名'] = $env:COMPUTERNAME ?? hostname
$info['平台'] = if ($IsWindows) { 'Windows' } elseif ($IsMacOS) { 'macOS' } else { 'Linux' }
$info['PowerShell版本'] = $PSVersionTable.PSVersion.ToString()

if ($IsWindows) {
$os = Get-CimInstance Win32_OperatingSystem
$info['操作系统'] = $os.Caption
$info['版本'] = $os.Version
$info['总内存GB'] = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
$info['可用内存GB'] = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
$info['运行时间'] = ((Get-Date) - $os.LastBootUpTime).ToString('dd\天\ hh\:mm\:ss')

$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$info['CPU'] = $cpu.Name
$info['CPU核心'] = $cpu.NumberOfLogicalProcessors
} else {
# Linux/macOS 系统信息
if ($IsLinux) {
$info['操作系统'] = (Get-Content /etc/os-release | Select-String '^PRETTY_NAME=') -replace 'PRETTY_NAME="?(.+)"?','$1'
} else {
$info['操作系统'] = sw_vers -productName
$info['版本'] = sw_vers -productVersion
}

# 内存信息
if ($IsLinux) {
$memInfo = Get-Content /proc/meminfo
$totalKB = ($memInfo | Select-String 'MemTotal:') -replace '\D',''
$freeKB = ($memInfo | Select-String 'MemAvailable:') -replace '\D',''
$info['总内存GB'] = [math]::Round($totalKB / 1MB, 2)
$info['可用内存GB'] = [math]::Round($freeKB / 1MB, 2)
}

# CPU 信息
if ($IsLinux) {
$cpuModel = (Get-Content /proc/cpuinfo | Select-String 'model name' | Select-Object -First 1) -replace 'model name\s+:\s+',''
$cpuCores = (Get-Content /proc/cpuinfo | Select-String 'processor' | Measure-Object).Count
$info['CPU'] = $cpuModel
$info['CPU核心'] = $cpuCores
}

# 运行时间
$uptimeOutput = uptime -p 2>$null
$info['运行时间'] = $uptimeOutput ?? "unknown"
}

# 磁盘信息(跨平台)
$info['磁盘'] = if ($IsWindows) {
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
[PSCustomObject]@{
驱动器 = $_.DeviceID
总GB = [math]::Round($_.Size / 1GB, 2)
可用GB = [math]::Round($_.FreeSpace / 1GB, 2)
}
}
} else {
df -h 2>/dev/null | Select-String '^/dev/' | ForEach-Object {
$parts = $_.ToString() -split '\s+'
[PSCustomObject]@{
驱动器 = $parts[5]
总GB = $parts[1] -replace '[A-Z]',''
可用GB = $parts[3] -replace '[A-Z]',''
}
}
}

return [PSCustomObject]$info
}

Get-CrossPlatformSystemInfo | Format-List

执行结果示例:

1
2
3
4
5
6
7
8
9
主机名        : ubuntu-server01
平台 : Linux
PowerShell版本 : 7.4.2
操作系统 : Ubuntu 22.04.4 LTS
总内存GB : 15.57
可用内存GB : 8.23
CPU : Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz
CPU核心 : 4
运行时间 : up 42 days, 3 hours, 17 minutes

跨平台包管理

不同平台有不同的包管理器。PowerShell 7 支持统一的 Install-Package 接口,但实际操作中往往需要调用原生包管理器:

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
function Install-CrossPlatformPackage {
<#
.SYNOPSIS
跨平台安装软件包
#>
param(
[Parameter(Mandatory)]
[string]$Package,

[ValidateSet('apt','yum','brew','winget','auto')]
[string]$Manager = 'auto'
)

if ($Manager -eq 'auto') {
if ($IsWindows) {
$Manager = 'winget'
} elseif (Get-Command apt-get -ErrorAction SilentlyContinue) {
$Manager = 'apt'
} elseif (Get-Command yum -ErrorAction SilentlyContinue) {
$Manager = 'yum'
} elseif (Get-Command brew -ErrorAction SilentlyContinue) {
$Manager = 'brew'
}
}

switch ($Manager) {
'apt' { sudo apt-get update; sudo apt-get install -y $Package }
'yum' { sudo yum install -y $Package }
'brew' { brew install $Package }
'winget' { winget install --id $Package --accept-source-agreements }
}

Write-Host "已通过 $Manager 安装:$Package" -ForegroundColor Green
}

# 批量安装常用工具
$commonTools = @('git', 'curl', 'wget')
foreach ($tool in $commonTools) {
Install-CrossPlatformPackage -Package $tool
}

执行结果示例:

1
2
3
已通过 apt 安装:git
已通过 apt 安装:curl
已通过 apt 安装:wget

注意事项

  1. **使用 Join-Path**:永远不要手动拼接路径,使用 Join-PathSplit-PathTest-Path 等命令确保跨平台兼容
  2. 平台检测使用内置变量$IsWindows$IsLinux$IsMacOS 是最可靠的检测方式,在 PowerShell 5.1 中需要手动检测 $env:OS
  3. 换行符差异:Windows 使用 CRLF(\r\n),Linux/macOS 使用 LF(\n)。处理文本文件时注意统一换行符
  4. 文件权限:Linux/macOS 使用 POSIX 权限模型(chmod),Windows 使用 ACL。跨平台脚本应分别处理
  5. 环境变量大小写:Windows 环境变量不区分大小写,Linux/macOS 区分大小写
  6. 测试覆盖:跨平台脚本应在所有目标平台上测试,不要假设”在 Linux 上能跑就行”

PowerShell 技能连载 - PowerShell 7 新特性深度实践

适用于 PowerShell 7.0 及以上版本

PowerShell 7 是 PowerShell 团队基于 .NET Core(现 .NET 5+)重新构建的重大版本。与 Windows PowerShell 5.1 相比,PS7 不仅实现了跨平台运行(Windows、Linux、macOS),还引入了大量新语法特性和性能优化。对于仍在使用 PS5.1 的运维团队来说,了解这些新特性可以显著提升脚本编写效率和代码可读性。

本文将深入讲解 PowerShell 7 中最实用的几项新特性,包括三元运算符、空合并运算符、管道链运算符、以及跨平台兼容性实践,并在文末提供一份从 PS5.1 迁移到 PS7 的检查清单。

三元运算符

PowerShell 7 终于引入了三元运算符 ? :,这是 C 系语言开发者期待已久的特性。它可以在一行内完成条件判断与赋值,让代码更加简洁。

在 PS5.1 中,我们必须使用 if-else 语句来完成简单的条件赋值:

1
2
3
4
5
6
7
8
# PS5.1 风格:冗长的条件赋值
$cpuLoad = 72
if ($cpuLoad -gt 80) {
$status = "危险"
} else {
$status = "正常"
}
Write-Host "CPU 状态:$status"
1
CPU 状态:正常

在 PowerShell 7 中,同样的逻辑可以用三元运算符一行搞定:

1
2
3
4
# PS7 风格:简洁的三元运算符
$cpuLoad = 72
$status = $cpuLoad -gt 80 ? "危险" : "正常"
Write-Host "CPU 状态:$status"
1
CPU 状态:正常

三元运算符的语法为 条件 ? 真值 : 假值。条件表达式必须放在 ? 的左侧,求值结果会被自动转换为布尔值。这对于将复杂的 if-else 嵌套扁平化非常有帮助。

在实际运维场景中,三元运算符特别适合用在配置化和状态判断的场景。来看一个更贴近实战的例子,批量检查服务状态并生成报告:

1
2
3
4
5
6
7
8
9
10
11
12
# 批量服务状态检查
$services = @("Spooler", "wuauserv", "WinRM", "BITS")
$report = $services | ForEach-Object {
$svc = Get-Service -Name $_ -ErrorAction SilentlyContinue
$svcExists = $null -ne $svc
[PSCustomObject]@{
服务名称 = $_
状态 = $svcExists ? $svc.Status.ToString() : "未找到"
运行中 = ($svcExists -and $svc.Status -eq 'Running') ? "是" : "否"
}
}
$report | Format-Table -AutoSize
1
2
3
4
5
6
服务名称 状态    运行中
-------- ---- ------
Spooler Running 是
wuauserv Stopped 否
WinRM Running 是
BITS Running 是

这里我们用三元运算符避免了多层 if-else 嵌套,让代码更直观。注意三元运算符的优先级低于比较运算符,所以 $cpuLoad -gt 80 ? "危险" : "正常" 不需要额外加括号。

三元运算符嵌套使用

三元运算符可以嵌套,但建议不要超过两层,否则会严重影响可读性:

1
2
3
4
# 嵌套三元运算符:磁盘空间告警级别
$freePercent = 12
$level = $freePercent -gt 30 ? "正常" : ($freePercent -gt 15 ? "警告" : "严重")
Write-Host "磁盘告警级别:$level(剩余 ${freePercent}%)"
1
磁盘告警级别:严重(剩余 12%)

空合并运算符 ?? 和空条件赋值 ??=

空合并运算符 ?? 和空条件赋值 ??= 是 PowerShell 7 从 C# 借鉴的另一组实用特性。它们专门用于处理 $null 值,让默认值赋值变得极其简洁。

空合并运算符 ??

?? 运算符的含义是:如果左侧不为 $null,返回左侧值;否则返回右侧值。

1
2
3
4
5
6
7
8
# 传统写法 vs ?? 写法
$configLogLevel = $null
$logLevel = $configLogLevel ?? "INFO"
Write-Host "日志级别:$logLevel"

$serverName = "prod-web-01"
$name = $serverName ?? "localhost"
Write-Host "服务器:$name"
1
2
日志级别:INFO
服务器:prod-web-01

这在处理配置项时特别有用。当用户未提供某个配置值时,可以优雅地回退到默认值。

来看一个实际的配置加载场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 模拟从不同来源加载配置
$envConfig = @{
Port = $null
MaxThreads = $null
LogPath = $null
Timeout = 30
}

# 使用 ?? 为每个配置项设置默认值
$port = $envConfig.Port ?? 8080
$maxThreads = $envConfig.MaxThreads ?? 4
$logPath = $envConfig.LogPath ?? "/var/log/app.log"
$timeout = $envConfig.Timeout ?? 60

Write-Host "最终配置:"
Write-Host " 端口:$port"
Write-Host " 最大线程:$maxThreads"
Write-Host " 日志路径:$logPath"
Write-Host " 超时(秒):$timeout"
1
2
3
4
5
最终配置:
端口:8080
最大线程:4
日志路径:/var/log/app.log
超时(秒):30

注意 $timeout 的值是 30 而非默认值 60,因为 $envConfig.Timeout 不为 $null?? 只在左侧为 $null 时才取右侧值。

空条件赋值 ??=

??= 运算符在变量为 $null 时才执行赋值操作。这在初始化配置或缓存场景中非常方便:

1
2
3
4
5
6
7
8
9
10
11
12
# 使用 ??= 确保变量只被初始化一次
$script:connectionPool ??= @()

# 配置字典中不存在则设置默认值
$settings = @{
Theme = "Dark"
}
$settings["Theme"] ??= "Light"
$settings["Language"] ??= "zh-CN"

Write-Host "主题:$($settings['Theme'])"
Write-Host "语言:$($settings['Language'])"
1
2
主题:Dark
语言:zh-CN

Theme 保持了原值 “Dark”,因为 $settings["Theme"] 已存在且不为 $null;而 Language 是新键,被 ??= 设置为默认值 “zh-CN”。

?? 和 ??= 的链式使用

?? 支持链式操作,可以从多个来源依次取值,直到找到非 $null 的值:

1
2
3
4
5
6
7
# 多级配置回退:命令行参数 > 环境变量 > 配置文件 > 硬编码默认值
$cmdLinePort = $null
$envPort = $null
$filePort = 9090

$finalPort = $cmdLinePort ?? $envPort ?? $filePort ?? 8080
Write-Host "使用端口:$finalPort"
1
使用端口:9090

管道链运算符 && 和 ||

管道链运算符 &&|| 是 PowerShell 7 从 Unix shell 借鉴的特性,可以根据前一个命令的执行成功与否来决定是否执行下一个命令。

  • &&:前一个命令成功($?$true)时,才执行后一个命令
  • ||:前一个命令失败时,才执行后一个命令

基本用法

1
2
3
4
5
6
7
8
# && 用法:目录存在才执行清理
$targetPath = "/tmp/workdir"
$result = (Test-Path $targetPath) && (Get-ChildItem $targetPath | Remove-Item -Recurse -Force)
if ($result) {
Write-Host "清理完成:$targetPath"
} else {
Write-Host "目录不存在,跳过清理"
}
1
目录不存在,跳过清理
1
2
3
4
# || 用法:创建目录失败时使用备用路径
$newDir = "/opt/app/data"
New-Item -ItemType Directory -Path $newDir -Force -ErrorAction SilentlyContinue ||
Write-Warning "无法创建 $newDir,尝试备用路径"

构建与部署流水线

管道链运算符最典型的场景是 CI/CD 流水线脚本。在 PS5.1 中需要大量 if ($LASTEXITCODE -ne 0) 检查,现在可以大幅简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# PS5.1 风格:繁琐的错误检查
Write-Host "=== 构建流水线(PS5.1 风格)==="

npm run lint
if ($LASTEXITCODE -ne 0) {
Write-Error "Lint 失败,中止构建"
exit 1
}

npm run build
if ($LASTEXITCODE -ne 0) {
Write-Error "构建失败,中止部署"
exit 1
}

npm run test
if ($LASTEXITCODE -ne 0) {
Write-Error "测试失败,中止部署"
exit 1
}

Write-Host "所有步骤通过!"
1
2
3
4
5
6
7
8
# PS7 风格:简洁的管道链
Write-Host "=== 构建流水线(PS7 风格)==="

npm run lint &&
npm run build &&
npm run test &&
Write-Host "所有步骤通过!" ||
Write-Error "流水线执行失败"

这两段代码功能完全相同,但 PS7 的写法更加简洁直观。&& 确保每一步成功后才继续,|| 捕获任何失败并输出错误信息。

复合使用:带回退的服务重启

1
2
3
4
5
6
7
8
# 尝试优雅重启,失败则强制终止再启动
$svcName = "nginx"
Restart-Service -Name $svcName -ErrorAction SilentlyContinue ||
(Stop-Process -Name $svcName -Force -ErrorAction SilentlyContinue;
Start-Sleep -Seconds 2;
Start-Service -Name $svcName) &&
Write-Host "$svcName 已成功重启" ||
Write-Warning "$svcName 重启失败,请手动检查"
1
nginx 已成功重启

注意事项

管道链运算符基于命令的成功状态($? 自动变量)来判断,而不是 $LASTEXITCODE。对于外部程序(如 npmgit),PowerShell 会将非零退出码映射为失败状态。但对于 PowerShell cmdlet,即使使用了 -ErrorAction SilentlyContinue,如果 cmdlet 内部产生了错误,$? 仍然会变成 $false

1
2
3
4
5
6
# 验证 $? 的行为
Get-Item "/nonexistent/path" -ErrorAction SilentlyContinue
Write-Host "上一个命令成功:$?"

Test-Path "/nonexistent/path"
Write-Host "上一个命令成功:$?"
1
2
上一个命令成功:False
上一个命令成功:True

Get-Item 在路径不存在时会产生错误,$? 变为 $false;而 Test-Path 只是返回布尔结果,不会产生错误,$? 仍然是 $true

跨平台兼容性实践

PowerShell 7 最大的架构改变是跨平台支持。同一套 PowerShell 脚本可以在 Windows、Linux 和 macOS 上运行。但要真正实现”一次编写,到处运行”,需要注意以下几个关键点。

自动变量 $IsWindows、$IsLinux、$IsMacOS

PS7 引入了三个布尔类型的自动变量,用于在运行时判断当前操作系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 跨平台路径处理函数
function Get-AppDataPath {
$appName = "MyApp"

if ($IsWindows) {
$basePath = [System.Environment]::GetFolderPath('ApplicationData')
} elseif ($IsLinux -or $IsMacOS) {
$basePath = Join-Path $env:HOME ".config"
}

$appPath = Join-Path $basePath $appName

if (-not (Test-Path $appPath)) {
New-Item -ItemType Directory -Path $appPath -Force | Out-Null
}

return $appPath
}

$path = Get-AppDataPath
Write-Host "应用数据路径:$path"
1
应用数据路径:/Users/wubo/.config/MyApp

在 PS5.1 中,判断操作系统通常依赖 Get-WmiObject 或环境变量,方式不统一且容易出错。PS7 的三个自动变量让平台判断变得清晰明了。

跨平台路径处理

路径分隔符是跨平台脚本最常见的坑。Windows 使用反斜杠 \,Linux/macOS 使用正斜杠 /。PowerShell 7 的 Join-PathSplit-Path 会自动处理平台差异:

1
2
3
4
5
6
7
8
# 始终使用 Join-Path 拼接路径,而非手动拼接
$projectRoot = $PWD.Path
$configDir = Join-Path $projectRoot "config"
$logFile = Join-Path $configDir "app.log"

Write-Host "项目根目录:$projectRoot"
Write-Host "配置目录:$configDir"
Write-Host "日志文件:$logFile"
1
2
3
项目根目录:/Users/wubo/Code/home.vichamp.com
配置目录:/Users/wubo/Code/home.vichamp.com/config
日志文件:/Users/wubo/Code/home.vichamp.com/config/app.log

避免 Windows 专属的 cmdlet

某些 cmdlet 只在 Windows 上可用(如 Get-WmiObjectGet-EventLogSet-Service 的某些参数)。PS7 提供了跨平台的替代方案:

PS5.1 (Windows Only) PS7 (跨平台替代)
Get-WmiObject Get-CimInstance
Get-EventLog Get-WinEvent (Windows) 或 journalctl (Linux)
netstat Get-NetTCPConnection
手动操作注册表 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
# 跨平台获取系统信息
function Get-SystemInfo {
$info = [PSCustomObject]@{
OS = "未知"
Version = "未知"
MachineName = $env:COMPUTERNAME ?? $env:HOSTNAME ?? "未知"
PowerShellVer = $PSVersionTable.PSVersion.ToString()
Architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
}

if ($IsWindows) {
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
if ($osInfo) {
$info.OS = "Windows"
$info.Version = $osInfo.Version
}
} elseif ($IsLinux) {
$info.OS = "Linux"
if (Test-Path /etc/os-release) {
$content = Get-Content /etc/os-release
$prettyName = $content | Where-Object { $_ -match '^PRETTY_NAME=' }
if ($prettyName) {
$info.Version = ($prettyName -replace '^PRETTY_NAME="?', '' -replace '"?$', '')
}
}
} elseif ($IsMacOS) {
$info.OS = "macOS"
$info.Version = (sw_vers -productVersion 2>$null) ?? "未知"
}

return $info
}

Get-SystemInfo | Format-List
1
2
3
4
5
OS            : macOS
Version : 15.4
MachineName : home.vichamp.com
PowerShellVer : 7.4.6
Architecture : Arm64

实战:迁移 PS5.1 脚本到 PS7 的检查清单

将现有 PS5.1 脚本迁移到 PowerShell 7 时,需要系统性地检查以下方面。这里提供一份实用的迁移检查函数:

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
function Test-PS7Compatibility {
param(
[Parameter(Mandatory)]
[string]$ScriptPath
)

if (-not (Test-Path $ScriptPath)) {
Write-Warning "文件不存在:$ScriptPath"
return
}

$content = Get-Content $ScriptPath -Raw
$findings = [System.Collections.Generic.List[string]]::new()

# 检查项 1:是否使用了已弃用的 WMI cmdlet
if ($content -match 'Get-WmiObject|Set-WmiInstance|Remove-WmiObject|Invoke-WmiMethod') {
$findings.Add("[弃用] 发现 WMI cmdlet,建议替换为 CimInstance 系列")
}

# 检查项 2:是否使用了 Windows 专属的 EventLog
if ($content -match 'Get-EventLog') {
$findings.Add("[弃用] 发现 Get-EventLog,建议替换为 Get-WinEvent")
}

# 检查项 3:是否硬编码了 Windows 路径
if ($content -match '[A-Z]:\\' -and -not ($content -match 'IsWindows|env:OS|IsLinux')) {
$findings.Add("[兼容性] 发现硬编码的 Windows 路径,建议使用 Join-Path")
}

# 检查项 4:是否使用了 .NET Framework 专属类型
if ($content -match 'System\.Drawing|System\.Windows\.Forms|System\.DirectoryServices') {
$findings.Add("[兼容性] 发现 .NET Framework 专属类型,Linux/macOS 上不可用")
}

# 检查项 5:是否可以简化为 PS7 新语法
if ($content -match 'if\s*\(.+\)\s*\{\s*\$\w+\s*=\s*.+\}\s*else\s*\{\s*\$\w+\s*=\s*.+\}') {
$findings.Add("[优化] 发现可使用三元运算符简化的 if-else 赋值")
}

$result = [PSCustomObject]@{
文件 = (Split-Path $ScriptPath -Leaf)
检查项数 = $findings.Count
发现 = $findings
}

return $result
}

使用这个函数可以快速扫描脚本的兼容性问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 假设有一个待迁移的脚本
$scriptContent = @(
'$svc = Get-WmiObject -Class Win32_Service -Filter "Name=''Spooler''"'
'$log = Get-EventLog -LogName Application -Newest 10'
'$path = "C:\Program Files\MyApp\config.xml"'
'if ($env:DEBUG) { $level = "DEBUG" } else { $level = "INFO" }'
) -join "`n"

$tempScript = Join-Path $env:TEMP "legacy-script.ps1"
Set-Content -Path $tempScript -Value $scriptContent

$report = Test-PS7Compatibility -ScriptPath $tempScript
$report | Format-List

Write-Host "`n--- 详细发现 ---"
$report.发现 | ForEach-Object { Write-Host " - $_" }
1
2
3
4
5
6
7
8
9
10
11
12
文件     : legacy-script.ps1
检查项数 : 4
发现 : {[弃用] 发现 WMI cmdlet,建议替换为 CimInstance 系列,
[弃用] 发现 Get-EventLog,建议替换为 Get-WinEvent,
[兼容性] 发现硬编码的 Windows 路径,建议使用 Join-Path,
[优化] 发现可使用三元运算符简化的 if-else 赋值}

--- 详细发现 ---
- [弃用] 发现 WMI cmdlet,建议替换为 CimInstance 系列
- [弃用] 发现 Get-EventLog,建议替换为 Get-WinEvent
- [兼容性] 发现硬编码的 Windows 路径,建议使用 Join-Path
- [优化] 发现可使用三元运算符简化的 if-else 赋值

迁移检查清单速查表

除了自动化检查外,以下是手动迁移时建议逐项确认的清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 迁移清单:可保存为脚本运行
$checklist = @(
[PSCustomObject]@{ 序号 = 1; 类别 = "语法"; 检查项 = "将 if-else 条件赋值替换为三元运算符 ? :"; 优先级 = "建议" }
[PSCustomObject]@{ 序号 = 2; 类别 = "语法"; 检查项 = "将 `$null 检查替换为 ?? 和 ??="; 优先级 = "建议" }
[PSCustomObject]@{ 序号 = 3; 类别 = "语法"; 检查项 = "将 if (`$LASTEXITCODE) 替换为 && 和 ||"; 优先级 = "建议" }
[PSCustomObject]@{ 序号 = 4; 类别 = "弃用"; 检查项 = "Get-WmiObject 替换为 Get-CimInstance"; 优先级 = "必须" }
[PSCustomObject]@{ 序号 = 5; 类别 = "弃用"; 检查项 = "Get-EventLog 替换为 Get-WinEvent"; 优先级 = "必须" }
[PSCustomObject]@{ 序号 = 6; 类别 = "兼容性"; 检查项 = "硬编码路径改为 Join-Path 拼接"; 优先级 = "高" }
[PSCustomObject]@{ 序号 = 7; 类别 = "兼容性"; 检查项 = "移除 Windows 专属 .NET 类型依赖"; 优先级 = "高" }
[PSCustomObject]@{ 序号 = 8; 类别 = "兼容性"; 检查项 = "添加平台判断逻辑 (`$IsWindows/`$IsLinux/`$IsMacOS)"; 优先级 = "高" }
[PSCustomObject]@{ 序号 = 9; 类别 = "兼容性"; 检查项 = "编码统一为 UTF-8 (带 BOM)"; 优先级 = "建议" }
[PSCustomObject]@{ 序号 = 10; 类别 = "测试"; 检查项 = "在 Windows 和 Linux 上分别运行测试"; 优先级 = "必须" }
[PSCustomObject]@{ 序号 = 11; 类别 = "测试"; 检查项 = "使用 Pester 编写跨平台单元测试"; 优先级 = "建议" }
)

$checklist | Format-Table -AutoSize
1
2
3
4
5
6
7
8
9
10
11
12
13
序号 类别   检查项                                                                优先级
---- ---- ------ ------
1 语法 将 if-else 条件赋值替换为三元运算符 ? : 建议
2 语法 将 $null 检查替换为 ?? 和 ??= 建议
3 语法 将 if ($LASTEXITCODE) 替换为 && 和 || 建议
4 弃用 Get-WmiObject 替换为 Get-CimInstance 必须
5 弃用 Get-EventLog 替换为 Get-WinEvent 必须
6 兼容性 硬编码路径改为 Join-Path 拼接 高
7 兼容性 移除 Windows 专属 .NET 类型依赖 高
8 兼容性 添加平台判断逻辑 ($IsWindows/$IsLinux/$IsMacOS) 高
9 兼容性 编码统一为 UTF-8 (带 BOM) 建议
10 测试 在 Windows 和 Linux 上分别运行测试 必须
11 测试 使用 Pester 编写跨平台单元测试 建议

使用要点与常见坑点

  • PowerShell 7 与 Windows PowerShell 5.1 是并行安装的关系,不会覆盖 PS5.1。PS7 的可执行文件是 pwsh(而非 powershell.exe),两者可以共存
  • 三元运算符 ? : 中的 ? 与 PowerShell 的 ? 别名(Where-Object)是不同的语法特性,注意区分上下文
  • ?? 运算符只检查 $null,不会检查空字符串或空数组。如果需要检查空字符串,请用三元运算符:($str ? $str : "默认值")
  • 管道链运算符 &&|| 基于 $? 自动变量判断成功状态,对 PowerShell cmdlet 和外部程序的判断标准不同
  • 跨平台脚本应始终使用 Join-Path 代替手动拼接路径,使用 PSCustomObject 代替 Windows 专属 .NET 类型
  • 迁移脚本时,建议先在 PS7 中运行 Invoke-ScriptAnalyzer 检查兼容性警告,再逐步修正