命令行脚本实战(二):PowerShell 语法与对象管道

写在前面

PowerShell 的关键不是“另一种 Shell 语法”,而是对象管道。Bash 和 CMD 通常在命令间传递文本,PowerShell 则传递带类型、属性和方法的 .NET 对象。理解这一点,Where-ObjectSelect-ObjectGet-Member 就不再是需要死记的命令。

本文以跨平台 PowerShell 7 为主。Windows 自带的 Windows PowerShell 5.1 与 PowerShell 7 可以并存,前者通常由 powershell.exe 启动,后者由 pwsh 启动。

系列导航:上一篇:Windows Batch · 下一篇:Bash Shell


一、Cmdlet 与发现式学习

Cmdlet 通常采用 Verb-Noun 命名:

1
2
3
4
5
Get-Process
Get-Service
Get-ChildItem
Set-Location
Remove-Item

不要先背整张命令表,而是学会发现:

1
2
3
Get-Command -Noun Service
Get-Help Get-ChildItem -Examples
Get-Process | Get-Member

Get-Command 找命令,Get-Help 看用法,Get-Member 看管道中对象的类型、属性和方法。


二、变量、集合与哈希表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$name = 'demo'
$count = 3
$enabled = $true
$nothing = $null

$numbers = 1, 2, 3
$config = @{
    Environment = 'production'
    Port        = 8080
}

PowerShell 变量以 $ 开头,默认可动态变更类型,也可显式约束:

1
2
3
[int]$port = 8080
[datetime]$startedAt = Get-Date
[string[]]$names = 'api', 'worker'

自定义结构化数据可以用 [pscustomobject]

1
2
3
4
5
6
7
$app = [pscustomobject]@{
    Name = 'orders-api'
    Port = 8080
    Healthy = $true
}

$app.Name

三、字符串与展开

1
2
3
4
$name = 'PowerShell'
'Hello, $name'            # 不展开
"Hello, $name"            # 展开
"2 + 3 = $(2 + 3)"        # 子表达式

多行字符串使用 Here-string:

1
2
3
4
5
6
$json = @'
{
  "name": "demo",
  "enabled": true
}
'@

路径优先用 Join-Path,脚本目录使用 $PSScriptRoot

1
$output = Join-Path $PSScriptRoot 'artifacts'

四、条件、循环与 switch

1
2
3
4
5
6
7
if ($port -ge 1 -and $port -le 65535) {
    'valid port'
} elseif ($port -eq 0) {
    'automatic port'
} else {
    throw "Invalid port: $port"
}

常见比较符有 -eq-ne-gt-ge-lt-le-like-match

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
foreach ($file in Get-ChildItem -Filter *.md) {
    $file.FullName
}

1..5 | ForEach-Object { $_ * 2 }

switch -Regex ($status) {
    '^2\d\d$' { 'success'; break }
    '^4\d\d$' { 'client error'; break }
    default   { 'other' }
}

foreach 是语言关键字,ForEach-Object 是管道命令,两者不是一回事。


五、对象管道

1
2
3
4
Get-Process |
    Where-Object CPU -GT 10 |
    Sort-Object CPU -Descending |
    Select-Object -First 5 Name, Id, CPU

Get-Process 输出的不是已排版的表格字符串,而是进程对象。后续命令直接读取 CPUName 等属性,无需切割文本列。

1
2
3
4
Get-ChildItem -File -Recurse |
    Where-Object Length -GT 10MB |
    Select-Object FullName, Length, LastWriteTime |
    Export-Csv large-files.csv -NoTypeInformation

Format-TableFormat-List 用于最终展示,通常应放在管道末端,不要在中途把格式化对象交给数据处理命令。


六、函数与参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Get-LargeFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [ValidateRange(1, 10240)]
        [int]$MinimumMB = 100
    )

    Get-ChildItem -LiteralPath $Path -File -Recurse |
        Where-Object Length -GE ($MinimumMB * 1MB)
}

Get-LargeFile -Path . -MinimumMB 20

可接收管道输入的高级函数可使用 beginprocessend 和 PowerShell 7.3 引入的 clean 块。大多数普通脚本先写清晰的 param() 就足够了。


七、文件、JSON 与 HTTP

1
2
3
4
5
6
$config = Get-Content -LiteralPath ./appsettings.json -Raw |
    ConvertFrom-Json

$config.Logging.LogLevel.Default = 'Warning'
$config | ConvertTo-Json -Depth 10 |
    Set-Content -LiteralPath ./appsettings.generated.json -Encoding utf8
1
2
3
4
5
6
$request = @{
    Uri    = 'https://api.example.com/health'
    Method = 'Get'
}
$result = Invoke-RestMethod @request
$result.status

PowerShell 适合自动化的重要原因,就是 JSON、CSV、HTTP 和文件系统都能直接变成对象处理。PowerShell 不使用 Bash 的反斜杠续行;参数较多时,splatting 通常比行尾反引号更清晰。


八、错误处理与原生程序

1
2
3
4
5
6
7
8
try {
    Get-Content -LiteralPath ./missing.txt -ErrorAction Stop
} catch {
    Write-Error "Read failed: $($_.Exception.Message)"
    exit 1
} finally {
    'cleanup'
}

PowerShell 区分终止错误和非终止错误,需要 catch 的 Cmdlet 调用常配合 -ErrorAction Stop

调用 gitdotnet 等原生程序后,检查 $LASTEXITCODE

1
2
3
4
dotnet test
if ($LASTEXITCODE -ne 0) {
    throw "dotnet test failed: $LASTEXITCODE"
}

PowerShell 7 支持 &&|| 管道链操作符:

1
dotnet test && dotnet publish -c Release

九、完整实战:生成文件清单

 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
[CmdletBinding()]
param(
    [string]$Path = $PSScriptRoot,
    [string]$Output = (Join-Path $PSScriptRoot 'files.json')
)

$ErrorActionPreference = 'Stop'

try {
    $root = (Resolve-Path -LiteralPath $Path).Path

    $items = Get-ChildItem -LiteralPath $root -File -Recurse |
        Select-Object @{Name='Path'; Expression={
            [IO.Path]::GetRelativePath($root, $_.FullName)
        }}, Length, LastWriteTimeUtc

    $items |
        ConvertTo-Json -Depth 3 |
        Set-Content -LiteralPath $Output -Encoding utf8

    Write-Host "Generated $Output ($($items.Count) files)"
} catch {
    Write-Error $_
    exit 1
}

这段脚本展示了 PowerShell 的典型优势:文件本来就是对象,投影后可以直接序列化成 JSON,不需要手工拼接文本。


十、快速选择

  • 服务和进程:Get-ServiceGet-Process
  • 文件:Get-ChildItemCopy-ItemMove-ItemRemove-Item
  • 过滤:Where-Object
  • 投影:Select-Object
  • 排序与分组:Sort-ObjectGroup-Object
  • 数据交换:ConvertFrom-JsonConvertTo-JsonImport-CsvExport-Csv
  • HTTP:Invoke-RestMethodInvoke-WebRequest
  • 自我发现:Get-CommandGet-HelpGet-Member

PowerShell 脚本的可维护性来自保留对象,而不是过早把一切变成文本。在管道中处理对象,在边界上输入或输出 JSON、CSV 和格式化文本,是最值得保留的设计原则。

系列导航:上一篇:Windows Batch · 下一篇:Bash Shell

参考资料