Windows 命令详解(五):批处理与 PowerShell 常用写法速查表

2026-08-10 Windows

前四篇讲的是单条命令。这一篇讲怎么把它们组织起来干活:批处理(.bat)的变量、条件、循环、参数与返回码,以及 PowerShell 里那批日常要用的 cmdlet、管道、比较运算符和执行策略。

两边的定位区别一句话说清:批处理够轻——双击就能跑,不需要任何策略设置,适合「固定几步的自动化」;PowerShell 强在对象与管道,处理「筛选、汇总、格式化输出」这类活儿比批处理省力得多。语法与行为照旧取自微软官方文档(Windows Commands 参考 + PowerShell 官方文档),出处列在文末。

一、批处理的骨架

写法 作用 示例
@echo off 关掉命令回显(@ 让它自己那行也不显示) @echo off
echo on / echo off 开关回显;echo 不带参数显示当前回显设置 echo off
echo. 输出一个空行 echo.
echo <文字> 输出文字 echo 开始备份
rem <注释> 注释;不带内容时相当于垂直空行 rem 下面是主流程
set 变量=值 设置环境变量(只在当前窗口/脚本生命周期内有效) set LOG=report.txt
set /p 变量=提示语 把用户输入读进变量 set /p name=请输入名称:
set /a 变量=表达式 把数值表达式的结果赋给变量 set /a n=n+1
setlocal 开始变量本地化,到 endlocal 或脚本结束为止 setlocal enableextensions
endlocal 结束本地化,丢弃本次 setlocal 之后的改动 endlocal
if 条件判断(多种形式见下表) if exist D:\data echo 存在
goto <标签> 跳到脚本里的某个标签 goto end
call <脚本或标签> 调用另一个批处理或本脚本里的标签,返回后继续执行 call checknew
for 对一组文件/值逐个执行命令 for %f in (*.log) do echo %f
shift 把批处理参数整体前移一位(处理不定个数的参数) shift
choice 让用户按一个字符做选择,返回所选序号 choice /c ync /m "继续吗"
timeout 暂停指定秒数(/nobreak 后按键也无法跳过) timeout /t 10
pause 暂停并提示「按任意键继续」 pause
exit /b <n> 结束当前批处理并返回退出码 exit /b 1
verify 开关「写入磁盘后校验」 verify on

setlocal 有一个很实用的开关:setlocal enabledelayedexpansion 打开延迟展开。批处理在解析一整行(或一整个 for/if 块)时就把 %变量% 替换成了当时的值,所以块里现改现用的变量拿不到新值;开了延迟展开后可以用 !变量! 取运行时的当前值。写循环里累加、拼接字符串这类逻辑时几乎必然会用到它。

if 的几种形式

形式 含义
if [not] ERRORLEVEL <number> <命令> 上一条程序返回的退出码大于或等于 number 时为真(注意是「大于等于」,不是「等于」)
if [not] <字符串1>==<字符串2> <命令> 两个字符串相同为真;字面字符串不需要加引号
if [not] exist <文件名> <命令> 文件存在为真
if [/i] <字符串1> <比较符> <字符串2> <命令> 按指定比较符比较,/i 忽略大小写
if cmdextversion <number> <命令> 命令扩展版本匹配时为真
if defined <变量> <命令> 变量已定义时为真
... [else <表达式>] 条件不成立时执行 else 分支

if errorlevel 的「大于等于」语义是最容易踩的坑:if errorlevel 1 在退出码是 2、3 时同样成立。要精确判等就用 %errorlevel% 配合比较符,例如 if %errorlevel% equ 2 ...。另外 if defined <变量> 成立时,环境里会多出 %errorlevel%、%cmdcmdline%、%cmdextversion% 这几个变量——这是官方 Remarks 里写明的。

判断外部工具的退出码还要知道各自的约定。比如第一篇里讲过 robocopy 返回 0~7 都算正常、只有 ≥ 8 才是失败,所以在批处理里写 if errorlevel 8 才是「真出错」;而 findstr 找不到匹配时返回 1,用 if errorlevel 1 判断就会把「没找到」和「出错」混在一起。

for 的四种扩展形式

命令扩展开启时(默认开启),for 还有这几种形态:

形式 作用
for /d %%变量 in (<集合>) do <命令> 只遍历目录(集合里带通配符时,对每个匹配的目录执行)
for /r [[<盘>:]<路径>] %%变量 in (<集合>) do <命令> 递归遍历目录树;不写路径就以当前目录为根,集合写 . 表示只枚举目录树
for /l %%变量 in (<起始>,<步长>,<结束>) do <命令> 按数字区间循环,(1,1,5) 得到 1 2 3 4 5,(5,-1,1) 倒着来
for /f [<解析关键字>] %%变量 in (<集合>) do <命令> 逐行解析文件内容、字符串或某条命令的输出

for /f 的「集合」有三种写法,对应三种数据来源:文件(in (file.txt))、字面字符串(in ("字符串"))、命令输出(in ('命令'))。用 usebackq 之后引号规则会反转:反引号里的当命令、单引号里的当字面字符串、双引号里的当含空格的文件名。

for /f 的解析关键字(官方表):

关键字 作用
eol=<c> 指定行结束字符(只能一个字符)
skip=<n> 跳过文件开头的 n 行
delims=<xxx> 指定分隔符集合,会替换掉默认的空格与制表符
tokens=<x,y,m-n> 指定把每行的哪些字段(token)传给循环变量;m-n 表示范围,末尾写 * 表示「剩下的全部文本」也分配一个变量
usebackq 反引号内容按命令执行、单引号内容按字面字符串、双引号文件名按含空格处理

默认情况下 for /f 只取每行第一个以空格分隔的字段,跳过空行。所以处理 CSV 这类数据时 delims=, 和 tokens= 是最常写的两个。

参数与文件的修饰符

批处理参数(%1、%2……)和 for 的循环变量都能加 %~ 修饰符,官方 call 与 for 页面都给了一整表:

修饰符 展开成什么
%~1 去掉两侧引号的参数值
%~f1 完整路径
%~d1 只保留盘符
%~p1 只保留路径
%~n1 只保留文件名
%~x1 只保留扩展名
%~s1 含短名称的完整路径
%~a1 文件的属性
%~t1 文件的日期和时间
%~z1 文件大小
%~dp1 盘符 + 路径
%~nx1 文件名 + 扩展名
%~fs1 全路径 + 短名称
%~$PATH:1 在 PATH 里查找该文件,展开成找到的第一个完整路径(找不到展开为空串)
%~dp$PATH:1 在 PATH 里查找,展开成第一个找到的文件的盘符 + 路径
%~ftza1 展开成类似 dir 的一整行输出

写脚本时最常用的一个组合是 %~dp0:%~d0 是脚本所在盘符、%~p0 是脚本所在路径,合起来就是「脚本自己所在的目录」——用它拼路径,脚本被移动到哪儿都能找到自己的配套文件。注意 %~ 修饰符不能和 %* 一起用。

二、PowerShell 常用 cmdlet

用途 cmdlet 要点 示例
找命令 Get-Command 按名称/动词/名词/模块搜;带 -Syntax 只列语法 Get-Command -Noun Service
看帮助 Get-Help -Examples 看例子、-Full 看全部、-Online 打开网页文档 Get-Help Get-ChildItem -Examples
切目录 Set-Location 支持 -Path、-LiteralPath(路径含通配符字符时用后者) Set-Location D:\data
列目录 Get-ChildItem -Recurse 递归、-Filter 用提供程序过滤、-File/-Directory 只取文件或目录 Get-ChildItem D:\logs -Filter *.log -Recurse
读文件 Get-Content -Tail n 取末尾 n 行、-Raw 一次性读成一个字符串、-Wait 持续跟读 Get-Content app.log -Tail 50
写文件 Set-Content -Encoding 指定编码、-NoNewline 不在末尾补换行 Set-Content -Path out.txt -Value $text
复制 Copy-Item -Recurse 递归、-Force 覆盖 Copy-Item D:\data E:\bak -Recurse -Force
删除 Remove-Item -Recurse、-Force;支持 -WhatIf 预演 Remove-Item D:\tmp -Recurse -Force -WhatIf
文本搜索 Select-String -Pattern 支持正则、-CaseSensitive、-Context n 带上下文 Select-String -Path *.log -Pattern "ERROR"
过滤 Where-Object 脚本块写法 { $_.属性 -gt 值 },或属性比较简写 -Property -GT -Value Get-Process | Where-Object { $_.CPU -gt 10 }
逐项处理 ForEach-Object -Begin/-Process/-End 三段式;-Parallel 并行 1..5 | ForEach-Object { $_ * 2 }
挑列 Select-Object 取属性、-First/-Last 取头尾、-Unique 去重 Get-Process | Select-Object -First 5 Name,CPU
排序 Sort-Object -Descending 倒序、-Unique 去重、-Top n 取前 n Get-Process | Sort-Object CPU -Descending
统计 Measure-Object 计数、求和、均值、最大最小 Get-ChildItem D:\logs | Measure-Object Length -Sum
看对象结构 Get-Member 查一个对象有哪些属性和方法——不知道能筛什么时先跑它 Get-Service | Get-Member
看进程 Get-Process -Name 按进程名(可通配)、-Id 按 PID Get-Process -Name chrome
结束进程 Stop-Process -Id 或 -Name、-Force 强杀 Stop-Process -Name notepad -Force
看服务 Get-Service -Name、-DisplayName、-DependentServices 看依赖 Get-Service -Name wuauserv
启停服务 Start-Service / Stop-Service 按 -Name 或管道传入的服务对象 Stop-Service -Name Spooler
改服务配置 Set-Service -StartupType 改启动类型、-Status 启停 Set-Service -Name Spooler -StartupType Disabled
查事件日志 Get-WinEvent -LogName 指定日志、-MaxEvents 限条数、-FilterXPath 精筛 Get-WinEvent -LogName System -MaxEvents 50
测连通性 Test-Connection -TargetName 目标、-TcpPort 测指定 TCP 端口、-Traceroute 走路径跟踪、-Quiet 只返回布尔值 Test-Connection -TargetName 192.0.2.10 -TcpPort 443 -Quiet
发 HTTP 请求 Invoke-WebRequest -Uri、-Method、-Body、-Headers Invoke-WebRequest -Uri https://example.com
看执行策略 Get-ExecutionPolicy 不带参数看当前生效值,-List 看各作用域 Get-ExecutionPolicy -List
设执行策略 Set-ExecutionPolicy 需要指定 -Scope Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

两个使用上的要点:

Test-Connection 是新版 PowerShell 里替代老 ping/Test-NetConnection 的那一条,-TcpPort 正是第三篇里「telnet 手工测端口」的替代方案,不用装 telnet 客户端也能验端口是否开放。-Quiet 只回 True/False,写脚本判断时最省事。

Remove-Item、Stop-Process、Set-Service 这些会改东西的 cmdlet 都支持 -WhatIf 和 -Confirm,-WhatIf 只打印「将会做什么」而不动手,是批量清理前最便宜的保险。

三、管道与比较运算符

管道把上一条命令的输出直接交给下一条处理(官方 about_Pipelines 的写法是 Command-1 | Command-2 | Command-3),最典型的一行是 Get-Process notepad | Stop-Process——查出记事本进程直接结束掉,中间不需要复制粘贴 PID。

比较运算符(官方 about_Comparison_Operators 收录)在 PowerShell 里是字母形式,不是 > <:

运算符 含义
-eq / -ne 等于 / 不等于
-gt / -ge 大于 / 大于等于
-lt / -le 小于 / 小于等于
-like / -notlike 通配符匹配 / 不匹配
-match / -notmatch 正则匹配 / 不匹配
-contains / -notcontains 集合里包含 / 不包含某个值
-in / -notin 某个值在 / 不在集合里
-replace 用正则替换

为什么 PowerShell 要另起一套:在它里面 > 是重定向输出到文件,如果 > 同时还是「大于」,Get-Process > out.txt 这种写法就会变得没法解释。所以比较一律用字母形式。

一行上下文的例子:

# 找出最近一天修改过、体积超过 1MB 的日志文件,按大小倒序取前 10 个
Get-ChildItem D:\logs -Filter *.log -Recurse |
    Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-1) -and $_.Length -gt 1MB } |
    Sort-Object Length -Descending |
    Select-Object -First 10 FullName,Length,LastWriteTime

四、执行策略(脚本跑不起来多半是它)

PowerShell 默认不允许随便运行脚本文件,这就是「明明双击能跑但脚本报错不让运行」的原因。官方文档给出的取值:

取值 含义
Restricted 允许单条命令,不允许运行任何脚本文件(含 .ps1xml、.psm1 与配置文件 .ps1)
RemoteSigned Windows 电脑的默认值:本地写的脚本可以直接跑;从互联网下载的脚本需要有可信发布者的数字签名,或先用 Unblock-File 解除标记
AllSigned 所有脚本都需要签名
Unrestricted 未签名脚本也能跑(非 Windows 系统的默认值且不可更改)
Undefined 当前作用域没有设置策略;若所有作用域都是 Undefined,则 Windows 客户端上等效于 Restricted、Windows Server 上等效于 RemoteSigned

作用域(-Scope)与优先级,官方给的是从高到低:

作用域 影响范围
Process 只在当前 PowerShell 会话内有效(存在 $Env:PSExecutionPolicyPreference 里,会话关掉就没了)
CurrentUser 只影响当前用户(存在用户级的 powershell.config.json)
LocalMachine 影响本机所有用户(默认作用域,存在 $PSHOME/powershell.config.json)

MachinePolicy 与 UserPolicy 是由组策略下发的,手动去设这两个作用域会被拒绝——这也是为什么有时候「改了策略还是被拦」:组策略的优先级比你手动设的高。

日常最省事的做法是只给当前用户放宽:Set-ExecutionPolicy RemoteSigned -Scope CurrentUser,不要去动 LocalMachine。跑一个下载来的脚本被拦时,先确认来源可靠,再用 Unblock-File 解除「来自互联网」的标记,比直接把策略改成 Unrestricted 安全得多。

五、两个能直接改的模板

把前四篇的命令组织成脚本,大致就是这个样子。

批处理版:把某目录整体备份到另一块盘,失败时返回非零并留下日志。

@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
rem 用法:backup.bat "源目录" "目标目录"
if "%~1"=="" goto usage
if "%~2"=="" goto usage

set "SRC=%~1"
set "DST=%~2"
rem 日志名里的日期按本机区域设置截取 %DATE%;%DATE% 格式不同的系统上如果名字不对,改成固定文件名即可
set "LOG=%~dp0backup-%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"

echo [%DATE% %TIME%] 开始备份 %SRC% -> %DST%
robocopy "%SRC%" "%DST%" /E /R:2 /W:5 /NP /LOG:"%LOG%"

rem robocopy 的 0~7 都算正常,只有 >=8 才是失败
if errorlevel 8 (
    echo [%DATE% %TIME%] 备份失败,详见 %LOG%
    exit /b 8
)
echo [%DATE% %TIME%] 备份完成,日志:%LOG%
exit /b 0

:usage
echo 用法:%~nx0 "源目录" "目标目录"
exit /b 2

几个细节:%~dp0 让日志文件默认落在脚本所在目录;if errorlevel 8 用的是「大于等于」语义,正好匹配第一篇里讲过的 robocopy 返回码口径;不同的退出码(0 成功 / 8 备份失败 / 2 参数错)让上层调度程序能区分失败原因。

PowerShell 版:做同一件事,外加「跑之前先看有没有空间」。

# 用法:.\backup.ps1 -Source D:\data -Destination E:\bak
[CmdletBinding()]
param(
    [Parameter(Mandatory)][string]$Source,
    [Parameter(Mandatory)][string]$Destination
)

$ErrorActionPreference = 'Stop'
$log = Join-Path $PSScriptRoot ('backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log')

if (-not (Test-Path -LiteralPath $Source))                      { Write-Error "源目录不存在:$Source";               exit 2 }
if (-not (Test-Path -LiteralPath $Destination))                 { Write-Error "目标目录不存在:$Destination";         exit 2 }

$need = (Get-ChildItem -LiteralPath $Source -Recurse -File | Measure-Object -Property Length -Sum).Sum
$free = (Get-PSDrive -Name ([System.IO.Path]::GetPathRoot($Destination).TrimEnd(':\'))).Free
if ($free -lt $need) { Write-Error ('空间不足:需要 ' + [math]::Round($need/1MB) + ' MB,可用 ' + [math]::Round($free/1MB) + ' MB'); exit 3 }

"$(Get-Date -Format 'HH:mm:ss') 开始备份 $Source -> $Destination" | Tee-Object -FilePath $log
robocopy $Source $Destination /E /R:2 /W:5 /NP /LOG+:$log
if ($LASTEXITCODE -ge 8) { Write-Error "备份失败,返回码 $LASTEXITCODE,详见 $log"; exit 8 }

"$(Get-Date -Format 'HH:mm:ss') 备份完成" | Tee-Object -FilePath $log
exit 0

PowerShell 里判断外部程序的退出码要用 $LASTEXITCODE(它只反映最近一次外部可执行文件的退出码),而不是 $?——$? 反映的是「上一条 PowerShell 语句是否成功」,两者不是一回事。

跑之前记得看一眼执行策略:Get-ExecutionPolicy。策略是 Restricted 时按上一节的办法给当前用户放宽;脚本是从别处下载来的,先 Unblock-File 再跑。

六、这个系列的其它几篇

七、出处

批处理部分:微软官方 Windows Commands 参考(https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/)下的 if、for、call、goto、shift、choice、timeout、setlocal、endlocal、rem、echo、set、verify、exit、pause、start、doskey、findstr、find、forfiles、robocopy。

几条重点页面:

PowerShell 部分:微软官方 PowerShell 文档(https://learn.microsoft.com/en-us/powershell/module/),版本口径为 PowerShell 7.5:

文中的 cmdlet 语法与参数名逐条核对自对应的官方 cmdlet 页面:Get-Command、Get-Help、Get-ChildItem、Set-Location、Get-Content、Set-Content、Copy-Item、Remove-Item、Select-String、Where-Object、ForEach-Object、Select-Object、Sort-Object、Measure-Object、Get-Member、Get-Process、Stop-Process、Get-Service、Start-Service、Set-Service、Get-WinEvent、Invoke-WebRequest、Set-ExecutionPolicy。