7AA7 博客

Windows 命令行效率指南:PowerShell 实用技巧

码同客 UID:88
2026-10-04 13:20
28 1 2

在 Windows 上折腾自动化,早晚会从 cmd 迁到 PowerShell。cmd 的命令是”字符串进、字符串出”,dir 的结果是一段文本,想二次处理还得靠 findstr 拼正则;PowerShell 从设计上就是对象管道,Get-ChildItem 输出的是带 Name、Length、LastWriteTime 属性的文件对象,下游 Cmdlet 直接按属性过滤排序。理解这一点差异,后面所有技巧都顺理成章。

管道里流的是对象,不是文本

cmd 里最常见的链式操作是 dir /b | findstr ".log" | sort,每一步都在对文本做正则切分。PowerShell 里同样意图的写法是:

Get-ChildItem -Path .logs -Filter *.log |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } |
    Select-Object Name, Length, LastWriteTime |
    Sort-Object Length -Descending

$_ 是管道里”当前这个对象”,属性名直接写出来即可,不用先解析文本。想看一个对象到底有哪些属性,Get-ChildItem .app.log | Get-Member 或者 Format-List * 都能列出,比翻文档快。

三个高频 Cmdlet 的分工:Where-Object 做筛选,Select-Object 做字段裁剪(也能 Select-Object -First 10 取前几条),ForEach-Object 对每条记录做任意操作。批量重命名就是典型的三者组合:

Get-ChildItem .*.txt |
    ForEach-Object {
        $new = $_.Name -replace '_draft$', '_final'
        Rename-Item -Path $_.FullName -NewName $new
    }

-replace 用正则替换,简单场景可以直接用 Replace() 字符串方法。重命名前先单独跑一遍 Get-ChildItem ... | Select-Object Name, @{n='NewName';e={$_.Name -replace ...}} 预览结果,确认无误再动手,这种”先预览再执行”的习惯能省下很多收拾残局的时间。

批量处理文件的几种常见形态

按扩展名分类归档:

Get-ChildItem .downloads -File |
    Group-Object Extension |
    ForEach-Object {
        $dir = Join-Path .downloads ($_.Name.TrimStart('.') )
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
        $_.Group | Move-Item -Destination $dir
    }

批量改文本内容,把一批配置里的旧域名换掉:

Get-ChildItem .conf*.json |
    ForEach-Object {
        (Get-Content $_.FullName -Raw) -replace 'old.example.com', 'new.example.com' |
            Set-Content $_.FullName -Encoding utf8
    }

-Raw 一次性把整个文件读成一个字符串,避免按行处理时丢掉换行结构;Set-Content 指定 -Encoding,防止默认编码在不同系统上表现不一致。

并行加速,处理几千个小文件时串行太慢,ForEach-Object -Parallel(PowerShell 7+)能直接开并行:

$files | ForEach-Object -Parallel { Convert-Image -Path $_ } -ThrottleLimit 8

并行块里的变量要通过 $using: 引用外部作用域的值,这是新手最容易漏写的地方。

格式化输出给报告用,管道结果要给人看时,Format-Table、Format-List 控制排版,再用 Out-File 或 Export-Csv 落地:

Get-Process |
    Sort-Object CPU -Descending |
    Select-Object -First 10 Name, CPU, WorkingSet |
    Format-Table -AutoSize |
    Out-File .top_cpu.txt

有一条经验值得反复强调:Format-* 会把对象降级成排版文本,一旦排版,后面的 Where-Object、Select-Object 就没法再按属性操作了。格式化只放在管道的最后一环,中间环节永远保持对象。

远程与脚本化

PowerShell Remoting 基于 WinRM,一台机器开启之后,其余操作跟本地几乎没差别:

Enter-PSSession -ComputerName web01 -Credential $cred        # 交互式进入
Invoke-Command -ComputerName web01,web02 -ScriptBlock {
    Get-Service w3svc | Select-Object MachineName, Status
}                                                            # 并行下发到多台

Invoke-Command 可以对多台机器同时下发,结果自动带上 PSComputerName 属性,汇总起来很方便。批量分发配置、收集服务状态、统一重启一组进程,都是几行脚本的事。对非 Windows 目标,PowerShell 7 支持 SSH 作为传输层,New-PSSession -HostName linux01 -UserName deploy 同样能用一套语法管起来,对混合环境比较友好。

脚本化方面,把常用流程写成 .ps1,参数用 param() 块声明,配合 -Verbose 输出进度,就是一套够用的小工具链:

param(
    [Parameter(Mandatory)][string]$Source,
    [ValidateSet('Copy','Move')][string]$Mode = 'Copy'
)
Write-Verbose "处理 $Source ..."

输出尽量走对象而不是 Write-Host 打印字符串:脚本返回的对象可以继续进管道被下游筛选汇总,而打印出来的文字只能靠肉眼读。定位给人看的信息才用 Write-Host 或 Write-Warning。

profile:把默认环境调顺手

$PROFILE 指向一个脚本,每次启动 PowerShell 都会执行。放在里面的东西建议以”提升体验”为主,不要塞业务逻辑:

# Microsoft.PowerShell_profile.ps1
Set-Location D:work
Set-PSReadLineOption -PredictionSource History
function ll { Get-ChildItem -Force | Sort-Object LastWriteTime -Descending }
Set-Alias -Name g -Value git

改完 profile 想立即生效,用 . $PROFILE 重新加载。团队共享的话,把 profile 放进仓库再用符号链接指过去,换机器就不需要重配。

需要按项目区分配置时,别把所有逻辑堆进全局 profile。常用做法是全局 profile 只放通用别名和提示符,项目相关的东西放项目根目录的 dev.ps1,进入目录后手动执行一次;或者写一个 Enter-Project 函数,按参数切换目录并加载对应的环境变量。全局默认保持轻量,切换成本才低。

常见的坑

执行策略。 默认策略可能阻止直接跑脚本,报”禁止运行脚本”。开发机上 Set-ExecutionPolicy -Scope CurrentUser RemoteSigned 即可,只影响当前用户,不需要管理员权限;服务器环境用组策略统一管,别在生产机器上随手放宽。

路径与引号。 路径带空格必须整体加引号;~ 只在 PowerShell 的路径解析里代表用户目录,拼进 cmd 命令时不生效。跨盘符操作用 Push-Location/Pop-Location 或显式 Set-Location,cd 只是别名,行为一致但记忆点更少。

编码。 Windows PowerShell 5.1 里 Get-Content 默认按 ANSI 读,Set-Content 默认写出的编码也未必是 UTF-8,中文文件来回倒腾很容易变乱码。显式加上 -Encoding utf8;如果团队都在 PowerShell 7 上,7 默认 UTF-8,坑会少很多,但写脚本时仍建议显式声明,跨版本更稳。带 BOM 的文件也要留意:网页或跨平台工具读到 BOM 可能出错,必要时用 [System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding $false)) 写出无 BOM 的 UTF-8。

参数绑定。 路径里带空格、逗号、括号时传参容易被解析错,脚本参数一律加引号;数组参数记得用逗号分隔或者连续传参,写错绑定不到参数上,报错信息还不一定指向这里。

别名的两面性。 ls、cat、%、? 这些别名敲着快,但写进脚本或贴给别人时可读性很差,而且部分别名在不同宿主下行为有细微差别。日常交互用别名,落地脚本用完整 Cmdlet 名,这个分界线值得守。

管道性能。 ForEach-Object 比 foreach ($x in $list) 慢一截,数据量大时优先用语言级循环;Where-Object 同理,能用 -Filter 参数(如 Get-ChildItem -Filter *.log)在 Provider 层就过滤掉的,别把几万个对象拉进管道再筛。对象管道的开销主要在装箱和方法调用,减少不必要的中间对象是关键。

调试与排查

脚本跑挂了先看 $Error,最近的错误对象里有 ScriptStackTrace,能直接指到出错行。Set-PSDebug -Trace 1 会逐行打印执行过程,排查流程控制类问题很直观。要观察管道中间结果,插一句 Tee-Object -FilePath debug.log,既输出到下游又留存一份,比来回注释代码干净。

另外值得养成的习惯是给函数写 param() 加上 [Parameter(Mandatory)]、类型标注和 ValidateSet,参数校验交给引擎,比在函数体里手写一堆 if 清晰,报错信息也更友好。

外部命令(git、curl 这类 exe)执行完用 $LASTEXITCODE 判断成败,它和 PowerShell 的 $? 语义不同:$? 反映的是上一条 Cmdlet/语句是否成功,外部命令退出码非零时两者未必同步。脚本里做条件分支,该看哪个就写哪个,别混用。

675浏览量
28赞
1评论
1参与用户
13 分钟阅读时间

评论(1)

  • Corin · 2026-10-04 16:20

    建议补一节边界情况的处理。

    回复