Windows 双 Shell 全面对比:CMD 与 PowerShell 的技术剖析

本文从架构、管道模型、命令发现、语法能力、错误处理、系统管理、安全模型、性能、兼容性九个维度,对 Windows 内置的两代 Shell------CMD(cmd.exe)与 PowerShell------做一次系统性的技术对比。结论先行:CMD 是一个文本批处理解释器,PowerShell 是一个构建在 .NET 类型系统之上的自动化框架与对象管道引擎。新写的自动化脚本应一律使用 PowerShell(7.x 优先),CMD 进入维护状态仅作历史兼容。


一、介绍:问题定义与背景

Windows 环境下的命令行自动化长期存在两个入口:双击或调度一个 .bat/.cmd 批处理文件(由 cmd.exe 解释执行),或者在 PowerShell 中运行 .ps1 脚本。很多工程师把两者当作"功能相同、语法略异"的工具,这是一个普遍性的认知偏差。

本文要回答的核心问题有三个:

  1. 两者在架构层面的本质差异是什么?------不是命令名的差异,而是进程模型、类型系统、扩展机制的差异。
  2. 哪些场景必须用 PowerShell、哪些场景 CMD 仍有不可替代性?
  3. 从 CMD 迁移到 PowerShell 时,哪些坑是语法层面的、哪些是语义层面的?

Microsoft 官方文档(Windows Commands A--Z)已明确表态:"对于最新且可靠的 Windows 自动化,我们建议使用 PowerShell 而不是 Windows 命令或 Windows 脚本主机。"同时,cmd.exe 自 2020 年起被微软官方声明进入维护模式(maintenance mode)------只修严重缺陷,不添新功能。微软高级项目经理 Rich Turner 举过一个著名的例子:仅仅修改了 cmd.exe 的版权字符串,就导致一家 OEM 厂商的生产线罢工一周------这个工具的兼容性包袱之重,可见一斑。

但"维护模式"不等于"被移除"。截至目前,cmd.exe 仍是 Windows 11 全系版本的内置组件,Windows Terminal 默认提供其 Profile。CMD 的存量价值(旧脚本、WinPE/恢复环境、登录脚本)与 PowerShell 的增量价值(新自动化、跨平台、云管理)将在未来很多年内并存,这正是本文对比的意义。


二、历史演进与定位

时间节点 CMD 线 PowerShell 线
1987 前身 COMMAND.COM(MS-DOS) ---
1993 cmd.exe 随 Windows NT 3.1 发布,血统来自 OS/2 ---
2002 --- Jeffrey Snover 发表《Monad Manifesto》,提出"面向管理的命令行"
2006 --- PowerShell 1.0 发布(.NET Framework 2.0)
2012 Windows 8 内置 PS 3.0,引入模块自动加载 ---
2016 --- PowerShell Core 6.0 开源(MIT,GitHub),跨平台,基于 .NET Core
2019 Windows Terminal 发布,两种 Shell 共享终端宿主 PowerShell 7.0 发布,与 5.1 并排安装
2020 cmd.exe 被官方声明进入维护模式 PowerShell 7.1/7.2 发布,引入启动缓存
2022--2025 wmic.exe 弃用,官方指路 PowerShell CIM 工具 PS 7.4(.NET 8 LTS)→ 7.5(.NET 9)→ 7.6(.NET 10 LTS)
2025 Windows PowerShell 2.0 引擎从 Win11 24H2 起移除 PS 7 持续演进,5.1 仅安全更新

两个关键定位判断:

  • CMD 的定位 :兼容层。它是 NT 系统对 DOS 时代命令行习惯的延续,.bat 语法与 1980 年代的 COMMAND.COM 几乎一脉相承。微软不删除它是因为存量脚本的兼容性风险远大于收益。
  • PowerShell 7 的定位 :现役主力。注意 PowerShell 现在是两个产品 :内置的 Windows PowerShell 5.1(powershell.exe,.NET Framework,仅安全更新)与可独立安装的 PowerShell 7.x(pwsh.exe,现代 .NET,跨平台)。二者并排安装、互不替代,这是迁移策略的基础。

三、架构对比:进程模型与命令模型

3.1 CMD:单体文本解释器

cmd.exe 是一个体量很小的单线程 Win32 程序,其命令分两类:

  • 内部命令(internal) :dir、cd、echo、set、for、if 等由 cmd.exe 自身直接解释执行,不创建子进程;
  • 外部命令(external) :ipconfig、robocopy 等真实的 .exe,由 cmd 通过 CreateProcess 启动子进程执行。

cmd.exe 没有模块系统、没有类型系统、没有异常机制。它对世界的全部认知就是:一行字符文本、一个环境变量表、一个 %ERRORLEVEL% 整数。所有外部命令返回的信息,只能通过标准输出的文本流与退出码传递。

一个容易被忽略的硬限制:cmd 的命令行总长度上限为 8191 字符 ,同样作用于批处理命令行和单个展开后的环境变量。超长的 PATH、拼接的大命令在 CMD 中会失败,即使父进程本身能容纳该值。这是 Windows 命令行历史包袱的直接体现(源自 DOS 时代 256 字符限制的放宽版)。

3.2 PowerShell:引擎/宿主分离 + .NET 类型系统

PowerShell 的架构是分层的:

复制代码
┌─────────────────────────────────────────────┐
│  宿主 (Host):控制台 / ISE / VS Code 扩展 /   │
│               Windows Terminal / 自研 GUI     │
├─────────────────────────────────────────────┤
│  引擎 (Engine):System.Management.Automation │
│    ├── 命令发现与解析器 (Parser)              │
│    ├── 管道处理器 (Pipeline Processor)        │
│    ├── 运行空间 (Runspace) 与会话状态          │
│    └── 类型适配器 (ETS: 扩展类型系统)          │
├─────────────────────────────────────────────┤
│  .NET 运行时:5.1 → .NET Framework 4.x       │
│               7.x → .NET 8/9/10 (Core)       │
├─────────────────────────────────────────────┤
│  提供程序 (Providers):文件系统/注册表/证书/   │
│  环境变量/WSMan,统一以 PSDrive 路径暴露       │
└─────────────────────────────────────────────┘

关键架构决策有三个:

  1. 引擎与宿主分离 。pwsh.exe 只是一个默认控制台宿主,引擎本体 System.Management.Automation.dll 是一个普通 .NET 程序集------这意味着你可以在自己的 C# 应用里内嵌一个 Runspace 执行 PowerShell 脚本,也可以从 Windows 服务、Web API 中调用它。CMD 没有这种嵌入式能力。

  2. 命令四分类 。PowerShell 会话中的命令统一为四类,优先级从高到低:Alias(别名)> Function(脚本函数)> Cmdlet(编译的 .NET 类)> Native(外部 exe)。Cmdlet 是继承自 Cmdlet/PSCmdlet 基类的 C# 类,采用 动词-名词(Verb-Noun)命名规范(如 Get-Process),参数是带 [Parameter] 特性标注的属性,由引擎做声明式参数绑定------这是 CMD 完全没有的能力:CMD 的外部命令参数是纯粹的字符串切割。

  3. 提供程序统一抽象 。文件系统、注册表(HKLM:\、HKCU:\)、环境变量(Env:)、证书存储、IIS 站点等,都被抽象为带盘符的 PSDrive。Get-ChildItem HKLM:\SOFTWARE 与 Get-ChildItem C:\Windows 走的是同一套 Item 抽象,遍历逻辑完全复用。

3.3 一句话总结架构差异

CMD 是"一个解释文本行的程序";PowerShell 是"一个暴露了 .NET 对象模型的管理自动化框架,恰好带一个命令行界面"。


四、核心差异:文本管道 vs 对象管道

这是两种 Shell 最本质的分歧,值得从机制层面拆解。

4.1 CMD 的管道:字节流

CMD(以及 Unix 传统 Shell)的管道传递的是未经解释的字节流。典型的"找出 CPU 占用最高的进程"在 CMD 里的写法:

bat 复制代码
:: CMD:解析文本的痛苦
tasklist /fo csv | findstr /i "chrome" | more
:: 想按内存排序?tasklist 没有 sort 参数,需要:
for /f "skip=1 tokens=1,2,4,5 delims=," %%a in ('tasklist /fo csv ^| findstr chrome') do echo %%a %%b %%c %%d

问题在于:tasklist 输出的每一列(进程名、PID、内存)在管道中退化为以逗号/空格分隔的字符串 。下游命令必须:猜测列位置 → delims 切割 → tokens 提取 → 处理"进程名里恰好含逗号"之类的边缘情况。每一个解析步骤都是脆弱点,且数据类型信息彻底丢失------内存占用从数字变成了字符串,无法参与数值比较,除非再做一次类型转换。

4.2 PowerShell 的管道:.NET 对象流

同样的需求在 PowerShell 中:

powershell 复制代码
Get-Process | Where-Object CPU -gt 100 |
    Sort-Object WS -Descending |
    Select-Object -First 5 Name, Id, WS

管道中流动的是 System.Diagnostics.Process 类型的完整对象实例,携带 CPU、WS(工作集)、StartTime 等几十个强类型属性。Where-Object 直接访问属性做数值比较,没有任何文本解析。列对齐、颜色、表格这些"外观",被推迟到管道末端由格式化系统(Format-Table/Format-List)统一处理 ------这就是 PowerShell 的核心设计原则:Output Formatting 与 Data Processing 解耦。

4.3 机制细节:三段式 Cmdlet 与流式处理

Cmdlet 不是一次性函数,而是实现了 BeginProcessing() → ProcessRecord() → EndProcessing() 三段生命周期。以 Get-Process | Sort-Object | Format-Table 为例:

  • Get-Process.ProcessRecord() 每枚举到一个进程就调用 WriteObject(process) 推入管道------流式、低内存;
  • Sort-Object 属于阻塞性 Cmdlet,必须在 EndProcessing 阶段攒齐全部输入才能输出;
  • Format-Table 收到对象后只做一件事:为每个对象生成格式化记录,最终交给宿主渲染。

这套机制带来两个实际收益:其一,长管道的首字节延迟 很低(Get-ChildItem C:\ -Recurse | ... 边遍历边处理);其二,内存中不需要同时持有"中间文本行"的解析副本。

4.4 结构化数据的原生支持

CMD 处理 CSV/JSON 只能靠 for /f 逐字符切割或借助第三方工具;PowerShell 内置了完整的数据编解码栈:

powershell 复制代码
# 读取 CSV → 按列名过滤 → 类型化排序 → 导出
Import-Csv .\servers.csv |
    Where-Object { [int]$_.CPU > 8 } |
    Sort-Object { [datetime]$_.InstallDate } |
    Export-Csv .\filtered.csv -NoTypeInformation -Encoding utf8

# JSON(7.x 还内置 ConvertFrom-Ini、Test-Json)
Invoke-RestMethod https://api.github.com/repos/PowerShell/PowerShell  # 自动反序列化为 PSObject

Invoke-RestMethod 直接把 HTTP 响应反序列化为对象树(JSON → PSCustomObject,XML → XmlDocument),这在 CMD 里需要 curl + 手写解析器才能达成。


五、命令发现机制对比

5.1 CMD 的查找顺序与风险

CMD 解析一个命令名的顺序:

  1. 内部命令(dir、echo 等,永远最先命中);
  2. 当前目录下的可执行文件(DOS 传统,cmd /k 与脚本中均如此);
  3. 按 PATH 逐目录搜索,目录内按 PATHEXT 中列出的扩展名顺序匹配(默认 .COM;.EXE;.BAT;.CMD;...)。

两个值得注意的安全/行为细节:

  • AutoRun 注册表键 :HKLM\SOFTWARE\Microsoft\Command Processor\AutoRun 与 HKCU 下的同名键,会在每次 cmd.exe 启动时执行。这是已知的持久化后门位,也是某些"明明没动过配置却行为异常"的排查点;
  • 当前目录优先 意味着 evil.exe 放进用户的工作目录就可能劫持同名的 PATH 命令。

5.2 PowerShell 的发现模型

PowerShell 用统一的 Get-Command 暴露发现机制:

powershell 复制代码
Get-Command *service          # 列出所有名词含 service 的命令
Get-Command -Type Cmdlet      # 只看 Cmdlet
Get-Command -Module NetSecurity
$ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction  # 7.x 可自定义未命中行为

其解析优先级为 Alias > Function > Cmdlet > External 。同名冲突时内部命令压过外部 exe------最经典的案例:Windows PowerShell 5.1 中 curl 和 wget 是 Invoke-WebRequest 的别名 ,导致大量"照抄 Linux 教程"的调用行为完全不同;PowerShell 6 起官方删掉了这组别名,把 curl.exe 还给了系统自带的真 curl。

模块系统是 CMD 完全缺失的一层:

  • PSModulePath 环境变量定义模块搜索路径(用户级 / 系统级 / PS7 专用目录);
  • 模块自动加载 (PS 3.0+):调用 Get-EventLog 时无需先 Import-Module Microsoft.PowerShell.Diagnostics,引擎在会话中懒加载命令目录(用 Get-Command -ListImported 可验证);
  • 模块可包含导出的 Cmdlet/函数、格式化文件(.ps1xml)、类型扩展、甚至原生二进制。

另一个安全向差异:PowerShell 默认不执行当前目录下的脚本 ------运行 .\deploy.ps1 必须显式带 .\ 前缀,这是对 CMD"当前目录优先"历史问题的直接修正。


六、语法能力逐项对比

6.1 变量与展开时机(最容易踩坑的差异)

CMD 的 %var% 是解析期展开(parse-time expansion):整条命令/整个代码块被读取时变量就被替换为字面值。这直接产生了批处理最著名的坑:

bat 复制代码
@echo off
for %%i in (1 2 3) do (
    set /a x=%%i * 2
    echo x = %x%        :: 错!三行都输出 0 或同一个旧值
)

原因:echo %x% 在整个 for 块被读入解析器时 就已展开。补救方案是开启延迟展开并改用 !x!:

bat 复制代码
setlocal EnableDelayedExpansion
for %%i in (1 2 3) do (
    set /a x=%%i * 2
    echo x = !x!       :: 正确,运行期展开
)

PowerShell 的 $var 是运行期求值 ,作用域为词法作用域链(local → script → global),子作用域可以读 父作用域变量,赋值则创建局部副本(可用 $script:x、$global:x 显式指定):

powershell 复制代码
1..3 | ForEach-Object {
    $x = $_ * 2
    "x = $x"           # 直接正确,无需任何开关
}

6.2 字符串与类型系统

能力 CMD PowerShell
字符串类型 无,一切都是字符流 一等 .NET 类型,所有 String 方法可用
插值 %var%(无表达式能力) 双引号内 $var、子表达式 $(Get-Date -f u)
转义 ^(引号内行为诡异,%% 转义百分号) 反引号 `````;单引号为字面量、双引号做插值
格式化 无 "{0:N2} MB" -f $size、-f 复合格式
多行文本 不支持 here-string @'...'@ / @"..."@
正则 无(findstr 有限支持) -match、-replace、.NET Regex 全量 API
数值比较 if %n% LSS 10(仅整数语义) -lt/-gt/-eq 对任意 IComparable 类型

举一个体现类型系统价值的例子------CMD 判断文件大小必须借力第三方或 for 的特殊语法,而 PowerShell:

powershell 复制代码
Get-ChildItem C:\logs -Recurse |
    Where-Object Length -gt 100MB |
    Measure-Object Length -Sum |
    ForEach-Object { "总计 {0:N2} GB" -f ($_.Sum / 1GB) }

100MB 是 PowerShell 内建的数字字面量(KB/MB/GB/TB/PB 后缀),Length 是 [long] 类型------整数运算全程无文本介入。

6.3 流程控制与并行

CMD 的流程控制止步于 if/else、for、goto :label、call,没有任何异常处理------一个命令失败后脚本默认继续往下跑(除非逐条检查 errorlevel)。PowerShell 提供完整的结构化控制:foreach/for/while/do、switch(支持正则与通配分支)、try/catch/finally,并且从 PS 7.0 起补齐现代语言特性:

powershell 复制代码
# 三目运算符(7.0+)
$status = $code -eq 0 ? '成功' : '失败'
# 空值合并 / 空值赋值(7.0+)
$port = $config.Port ?? 8080
# 管道链(7.0+):语义同 CMD 的 && 和 ||
Build.exe && Sign.exe || Log-Failure.ps1
# 并行(7.0+,基于 Runspace 池)
1..10 | ForEach-Object -Parallel { Invoke-WebRequest "http://host$_/health" } -ThrottleLimit 5

ForEach-Object -Parallel 是 PS7 独有能力,每个并行分支运行在独立 Runspace 中(变量需用 $using: 传入)------这解决了 PowerShell 多年以来"并行只能靠 Job(进程级,开销大)"的问题。


七、错误处理模型对比

7.1 CMD:单一退出码

CMD 的错误模型只有 %ERRORLEVEL%。三个高频坑:

  1. if %errorlevel%==0 的写法陷阱 :变量展开时机问题常导致判断取到旧值,规范写法是 if not errorlevel 1(含义为 errorlevel < 1)或使用 || 运算符;
  2. 管道后 errorlevel 取值不确定 :a.exe | b.exe 之后 %errorlevel% 反映的多是最后一个命令,无法感知 a.exe 失败;
  3. 部分命令根本不设置 errorlevel,或成功时输出报错信息但返回 0------判断成败只能依据官方文档中记载的退出码约定,而非屏幕输出。

7.2 PowerShell:双轨错误模型

PowerShell 有两套信号,混淆它们是迁移期最常见错误:

信号 含义 适用对象
$? 最后一条命令是否成功(布尔) Cmdlet、函数、外部命令均置位
$LASTEXITCODE 最近一次外部 exe 的退出码(整数) 仅外部命令更新,Cmdlet 不触碰它

在此之上是真正的异常体系:

powershell 复制代码
# 错误偏好:默认 Continue(记录错误继续跑),可全局或单条覆盖
$ErrorActionPreference = 'Stop'        # 生产脚本强烈建议

try {
    Get-Content 'C:\not-exist.txt' -ErrorAction Stop   # 非终止错误 → 强制终止
    robocopy.exe src dst /mir          # 外部命令失败不会 throw,需手动判:
    if ($LASTEXITCODE -ge 8) { throw "robocopy 失败,退出码 $LASTEXITCODE" }
}
catch [System.IO.FileNotFoundException] {
    Write-Warning "文件缺失: $($_.TargetObject)"
}
catch {
    # $_ 是 ErrorRecord 对象:带 CategoryInfo / FullyQualifiedErrorId / ScriptStackTrace
    Write-Error "未分类错误: $($_.Exception.Message)"
}
finally { Remove-Item $tmpFile -ErrorAction SilentlyContinue }

两个工程要点:

  • 非终止错误(Non-terminating)与终止错误(Terminating)之别 :Cmdlet 调 WriteError() 产生的非终止错误默认不会 被 try/catch 捕获,必须 -ErrorAction Stop 或提升全局偏好------这是 PowerShell 脚本"catch 了却没 catch 到"的头号原因;
  • robocopy 的退出码语义 :0~7 均为成功(含"有文件被复制"=1),8+ 才是失败。在 CMD 里这个约定同样坑,但 PowerShell 里至少可以把语义显式编码进 throw。

八、系统管理能力:WMI/CIM、COM、.NET、远程

8.1 CMD 的"管理能力"边界

CMD 本身没有管理 API,只有散装命令行工具:net user、sc、wmic(已弃用)、reg、netsh、dism。这些工具输出格式各异、脚本化能力参差(wmic 的输出 famously 带尾部空白和 Unicode 怪癖)、且没有统一错误模型。官方已明确 wmic 弃用,指路的替代品正是 PowerShell 的 CIM 工具集。

8.2 PowerShell 的管理栈

powershell 复制代码
# WMI/CIM:查系统信息(替代 wmic / Get-WmiObject)
Get-CimInstance Win32_OperatingSystem | Select-Object Caption, LastBootUpTime
# LastBootUpTime 直接返回 [datetime] 类型,不再需要 ConvertToDateTime 的字符串转换

# COM 自动化:操控 Excel、IE 等任意注册了 COM 接口的组件
$excel = New-Object -ComObject Excel.Application

# 直接调 .NET 全量 API
[System.IO.File]::ReadAllLines('x.log')[-100..-1]        # 读日志尾部
[System.Security.Cryptography.SHA256]::HashData([IO.File]::ReadAllBytes($f))  # 7.x API

# 注册表即文件系统
Set-ItemProperty HKLM:\SOFTWARE\MyApp -Name Timeout -Value 60

8.3 远程管理:代际差

CMD 没有任何内建远程执行能力,实践上依赖第三方(PsExec)或 net use + 计划任务的组合拳。PowerShell 则内置了完整的远程栈:

  • WinRM/PSRP (5985/5986 端口):Invoke-Command -ComputerName SRV01 -ScriptBlock { ... },脚本块在远端序列化执行,本地收到的是对象 (在线模式)或属性快照(反序列化模式,PSObject 带 PSComputerName 注记);
  • Enter-PSSession 交互式远程 Shell;
  • JEA(Just Enough Administration):基于会话配置的细粒度授权,可限制某个运维账号只能执行白名单 Cmdlet 且以虚拟账户身份运行------这是 Windows 服务器权限治理的核心机制之一;
  • SSH Remoting(PS 6+):与 Linux 生态同一套密钥/known_hosts 体系;
  • PowerShell Direct :从宿主机直连 Hyper-V 虚拟机执行命令,不依赖网络与 WinRM 配置,初始化自举场景的利器。

九、安全模型对比

机制 CMD PowerShell 7
脚本执行控制 无。.bat 可被直接/计划任务执行 执行策略:Restricted / RemoteSigned / AllSigned / Unrestricted / Bypass(多作用域:MachinePolicy→UserPolicy→...→Process)
代码签名 无 Authenticode 签名 + AllSigned 策略下的证书链校验
语言限制 无 Constrained Language Mode(限制 .NET 类型调用,用于非受信会话)
权限收敛 无 JEA 角色能力(Role Capability)白名单
内存防护 无 AMSI 集成(脚本内容运行前送反恶意软件引擎扫描)
日志 无 模块日志、脚本块日志(可记录脱混淆后的代码)、转录(Transcript)

必须澄清一个广泛误解:执行策略不是安全边界 (微软文档原话),它防的是"用户无意识地运行了不信任脚本",用 Set-ExecutionPolicy -Scope Process Bypass 或直接 Get-Content a.ps1 | iex 就能绕过------它的设计目标是"防止误用"而非"对抗攻击者"。真正的防线是 AppLocker/WDAC(对脚本做签名哈希强制)+ CLM + AMSI 的组合。

反向说,CMD 的"无安全模型"本身就是一个模型:脚本随便跑、日志全没有,在等保与审计要求下这已经是扣分项。企业环境里 PowerShell 的脚本块日志与转录往往是选它的隐性理由。


十、性能对比

性能需要分三个正交维度看,混在一起谈只会得出错误结论。

10.1 启动延迟(Shell 的固定成本)

进程 典型冷启动 说明
cmd /c echo ok 约 30--60 ms 几乎无初始化
powershell -NoProfile -Command (5.1) 约 200--400 ms .NET Framework JIT
pwsh -NoProfile -Command (7.x) 约 300--500 ms 现代 .NET;7.2+ 引入启动缓存(预编译扩展程序集缓存)显著缩短温启动
pwsh(含 Profile) 与 Profile 复杂度线性相关 Profile 里塞满模块是"PowerShell 慢"的最大单一来源

结论 :被高频、短生命周期调用的场景(每秒拉起数百次的调度任务、Makefile/CMake 的自定义命令、CI 每步新开 Shell)应使用 CMD/直接调 exe,或对 PowerShell 使用 -NoProfile -Command 且尽量常驻进程。反之,单次任务本身耗时超过秒级时,几百毫秒的启动差可忽略。

实测方法(可复现,勿凭体感):

powershell 复制代码
Measure-Command { pwsh -NoProfile -Command "1+1" }
# 循环取样 20 次取中位数更稳

10.2 管道吞吐与数据处理

处理大文件时 CMD 的 for /f 逐行循环是纯解释器循环,性能塌方;PowerShell 底层是 .NET:

powershell 复制代码
# 快:流式 StreamReader,避免 Get-Content 全量载入
$sr = [System.IO.StreamReader]::new('big.log')
while ($null -ne ($line = $sr.ReadLine())) { if ($line -match 'ERROR') { $n++ } }
$sr.Close()
# 更快(7.x):Get-Content 大文件有内部优化;正则热点可换 [regex]::Matches

经验量级:百万行文本的过滤统计,PowerShell 秒级完成,CMD 的 for /f 往往分钟级。但反过来,极其简单的单命令单用途工具链 (type a | findstr x > out.txt)CMD 更快,因为对象包装(每个对象要过 PSObject 与格式化系统)有固有开销。

10.3 启动成本 vs 任务成本的决策公式

选 CMD 的近似条件:任务自身耗时 < 启动差 × 调用频次 且任务不涉及结构化数据处理。

反之选 PowerShell。现代 CI 中更优的解法是把 PS 脚本常驻为一个守护进程/Agent,用 stdin/out 通信,把启动成本摊薄为零。


十一、兼容性与共存策略

  • .bat 在 PowerShell 中可直接调用 :cmdlet 与 .bat/.cmd/.exe 混用没有障碍(PowerShell 会为外部命令桥接环境变量与退出码)。反向不成立------CMD 无法执行 Cmdlet ,只能 powershell -Command "..." 反向拉起;
  • PS 5.1 与 PS 7 并排共存 :不同可执行名(powershell.exe vs pwsh.exe)保证了确定性共存。旧模块(依赖 .NET Framework 专属 API 或 WMI v1 Cmdlet 的)只认 5.1,新代码写 7;
  • 模块迁移雷区 :Get-WmiObject/Invoke-WmiMethod 等 5 个 WMI v1 Cmdlet 在 PS 7 中已移除 ,统一换 Get-CimInstance/Invoke-CimMethod(5.1 也同样有 CIM 工具集,可全量替换,建议直接改写而非写版本分支);Get-EventLog → Get-WinEvent;Add-PSSnapin(快照式扩展)在 PS 7 不存在;
  • 编码:CMD/旧控制台默认代码页 936(GBK)或 437,PS 7 全链路默认 UTF-8(无 BOM),跨系统日志管道时这是隐形杀手;
  • Windows Terminal:Win11 22H2 起为默认终端宿主,CMD/PS 5.1/PS 7 共享窗口管理,切换成本为零------"用哪个 Shell"已与"用哪个终端"解耦。

十二、典型任务对照表

任务 CMD PowerShell
列出最近 7 天修改的文件 forfiles /d -7(语法脆弱,递归受限) `gci -Recurse
找出占用 CPU 最高的进程 无原生方案(文本解析 tasklist) `ps
批量重命名 for 循环 + set 字符串截取 `gci *.txt
查服务并重启 sc query + sc start(多步、无过滤) Restart-Service -Name Spooler -Force
读注册表 reg query(文本输出) Get-ItemProperty HKLM:\...\Name
解析 JSON 不可行 ConvertFrom-Json
HTTP 请求 curl.exe(Win10 1803+ 自带) Invoke-RestMethod(自动反序列化)
远程执行 PsExec(第三方) Invoke-Command -ComputerName X { ... }
计算哈希 certutil -hashfile Get-FileHash
网络共享映射 net use New-PSDrive -Persist
环境变量(永久) setx(截断 >1024 字符的坑) [Environment]::SetEnvironmentVariable(k,v,'Machine')

十三、踩坑点清单(迁移期高发)

  1. .ps1 双击默认用记事本打开 ------Windows 出于安全不把 .ps1 关联到执行器,这是特性不是缺陷;调度任务中要用 pwsh -File x.ps1 显式拉起;
  2. $LASTEXITCODE 不重置 :连续调用外部命令后,第二次失败但 $LASTEXITCODE 仍可能是上次的值------严格脚本应在每次外部调用前清零或立即消费;
  3. PS 5.1 中 curl/wget 是别名 :老教程脚本在 5.1 与 7 中行为不同,跨版本脚本请写全 curl.exe;
  4. 执行策略是分发问题不是安全问题 :RemoteSigned 只要求"从网络下载的脚本有签名",本地写的脚本不受限;
  5. 非终止错误吞掉 catch :所有对外部世界有副作用的 Cmdlet 调用加 -ErrorAction Stop,或脚本头部统一 $ErrorActionPreference='Stop';
  6. PS 7.3+ 的外部参数传递语义变更 :新增 $PSNativeCommandArgumentPassing(Windows/Standard 模式),修复了 5.1 把内嵌引号传给原生 exe 时丢失的问题,但可能改变老脚本中"依赖旧怪癖"的行为;
  7. CMD 的 %~dp0 ↔ PS 的 $PSScriptRoot:等价能力,但后者只在脚本文件内有效,交互式为空;
  8. 管道输出编码 :CMD 工具输出 GBK 时,PS 7 需要 $OutputEncoding = [Console]::OutputEncoding = [Text.Encoding]::GetEncoding(936) 才能不乱码;
  9. ForEach-Object -Parallel 里看不到外层变量 :Runspace 隔离,必须 $using:;
  10. CMD 中在括号块内修改并读取同一变量 :忘记 EnableDelayedExpansion(本文 6.1 节),这是批处理 Stack Overflow 提问量最大的单一问题。

十四、结论与选型建议

把全文的工程判断收敛为一张决策表:

场景 选择 理由
新写任何自动化/管理脚本 PowerShell 7 类型化管道、结构化错误处理、模块生态、远程管理
维护存量 .bat 原样保留 重写收益低、回归风险高;增量功能用 PS 补
WinPE / 恢复环境 / 极小体积约束 CMD 环境内置且启动近乎零成本
调度任务每秒级高频拉起 CMD 或常驻 PS 进程 启动成本摊薄(见 10.3 节公式)
跨 Windows/Linux 混合运维 PowerShell 7 同一脚本双端运行(配合 #Requires 与路径抽象)
安全审计/等保要求环境 PowerShell 7 脚本块日志、转录、JEA、AMSI
面向他人的文档/教程命令 PowerShell 7 Get-Help 体系 + 可发现性,可维护性远超口口相传的 batch 咒语

最后给出三条可执行的迁移建议:

  1. 接口优先迁移:先迁移"与外部系统交互"的脚本(HTTP、JSON、WMI/CIM、远程执行),CMD 在这些场景几乎没有还手之力;纯本地文件批量操作若已稳定,可以最后再动;
  2. 用 $PSVersionTable 写版本守卫 :#Requires -Version 7.2 放在脚本首行,让版本不匹配在启动时就失败,而不是跑到一半在移除的 Cmdlet 上崩;
  3. 把 $ErrorActionPreference = 'Stop'、Set-StrictMode -Version Latest 写进你的脚本模板 :前者让错误可捕获,后者让未定义变量直接报错而非静默为 $null------这两行是 PowerShell 从"能跑"到"工程化"的分水岭。

CMD 不会消失,但它是历史。PowerShell 不是 CMD 的升级版,而是 Windows 从"图形管理 + 散装命令"走向"API 化管理自动化"的载体------理解了这一点,就理解了为什么微软敢把一个 33 岁的 Shell 送进维护模式,却把十年的投入押在一个基于 .NET 的对象管道引擎上。


参考:Microsoft Learn《Windows Commands》《about_Execution_Policies》《Differences between Windows PowerShell 5.1 and PowerShell 7.x》;PowerShell/PowerShell GitHub 仓库;Rich Turner 关于 cmd.exe 维护模式的公开说明(2020)。

相关推荐
长不大的杨瑞杰1 小时前
Windows 安装 OpenCode v2:原生、WSL、Desktop 怎么选?完整配置与避坑指南
人工智能·windows·ai agent·opencode
Layer3 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
Misnice3 小时前
Python 内置函数大全
开发语言·windows·python
闻缺陷则喜何志丹3 小时前
计算24点
c·测试·input·output
百慕大三角3 小时前
为了在 Linux CI 里跑小程序 E2E,我把微信官方基础库搬进了浏览器:wxa-runner 开源实践
微信小程序·agent·测试
vivo互联网技术4 小时前
域名埋点零侵入实时校验实践:基于代理转发与规则驱动的工具设计
前端·测试·监控·代理·埋点
运维踩坑日记5 小时前
【无标题】
linux·运维·windows