前四篇讲的是单条命令。这一篇讲怎么把它们组织起来干活:批处理(.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 0PowerShell 里判断外部程序的退出码要用 $LASTEXITCODE(它只反映最近一次外部可执行文件的退出码),而不是 $?——$? 反映的是「上一条 PowerShell 语句是否成功」,两者不是一回事。
跑之前记得看一眼执行策略:Get-ExecutionPolicy。策略是 Restricted 时按上一节的办法给当前用户放宽;脚本是从别处下载来的,先 Unblock-File 再跑。
六、这个系列的其它几篇
- Windows 命令详解(一):文件与目录操作常用命令速查表
- Windows 命令详解(二):系统信息、进程与服务管理命令速查表
- Windows 命令详解(三):网络诊断与配置命令速查表
- Windows 命令详解(四):磁盘与系统维护命令速查表
七、出处
批处理部分:微软官方 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。
几条重点页面:
if(含 errorlevel 的「大于等于」语义):https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/iffor(含/d、/r、/l、/f与解析关键字表、变量修饰符表):https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/forcall(含参数修饰符表):https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/callchoice:https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/choice
PowerShell 部分:微软官方 PowerShell 文档(https://learn.microsoft.com/en-us/powershell/module/),版本口径为 PowerShell 7.5:
about_Execution_Policies(执行策略取值与作用域优先级):https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policiesabout_Pipelines:https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pipelinesabout_Comparison_Operators(比较运算符表):https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operatorsTest-Connection(含-TcpPort、-Traceroute):https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/test-connection
文中的 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。