一天学会 Windows 批处理

概述

Windows 批处理文件的扩展名为 .bat,它是由 Windows 命令解释器 cmd.exe 负责解释和运行的。一个批处理文件就是一个批处理程序。Windows 上的批处理程序相当于 Linux 上的 Shell 脚本,常用于实现重复性工作的自动化。

第一个批处理程序

批处理文件是文本文件,内容主要是一些可以直接在控制台(命令提示符)执行的 Windows 命令,比如:

1
2
@echo off
echo Hello, World!

将以上内容保存在扩展名为 .bat 的文本文件就得到一个批处理程序。要运行该批处理程序,只需打开控制台,输入批处理程序的路径,再按回车即可,比如:

1
2
C:\Users\John>a.bat
Hello, World!

基本 Windows 命令

Windows 命令及其选项都不区分大小写。命令的选项以 / 开头,比如 /? 通常是显示帮助信息的选项。下列是 3 个最基本的 Windows 命令:

  • echo,用于显示消息
  • cls,用于清除屏幕
  • chcp,用于切换代码页

echo 命令会把紧随其后的字符串显示在屏幕中:

1
2
C:\Users\John>echo Hello, World!
Hello, World!

字符串两边无需添加引号。

cls 命令会把屏幕上的内容清空,再将光标移动到屏幕顶部。

不同的代码页对应不同的字符集。控制台只能显示当前代码页中的字符,显示其他字符时会显示成乱码。代码页还决定控制台使用的语言。不带参数执行 chcp 命令时将显示当前代码页的编号:

1
2
C:\Users\John>chcp
Active code page: 437

要切换到其他代码页则需要提供目标代码页的编号。常用的代码页有下列 3 个:

  • 936 GBK - 简体中文
  • 437 ASCII - 美国英语
  • 65001 Unicode

控制台的默认代码页是 936。默认代码页只能通过注册表修改:

1
HKEY_CURRENT_USER\Console\%SystemRoot%_system32_cmd.exe

将属性 CodePage 的值由 936 修改为 65001。

命令回显

批处理程序在运行时会显示执行的每个命令,即命令回显。如果把 rd /? 命令保存在一个名为 a.bat 的批处理程序中,运行该批处理程序时就会看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\Users\John>a.bat

C:\Users\John>rd /?
Removes (deletes) a directory.

RMDIR [/S] [/Q] [drive:]path
RD [/S] [/Q] [drive:]path

/S Removes all directories and files in the specified directory
in addition to the directory itself. Used to remove a directory
tree.

/Q Quiet mode, do not ask if ok to remove a directory tree with /S

要禁用或启用命令回显,可以在批处理程序中使用 echo offecho on 命令。

1
2
echo off
rd /?

虽然 echo off 命令用于禁用命令回显,但是它只对后续的命令有效。因此,在执行包含上述命令的批处理程序时,会看到 echo off 命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\Users\John>a.bat

C:\Users\John>echo off
Removes (deletes) a directory.

RMDIR [/S] [/Q] [drive:]path
RD [/S] [/Q] [drive:]path

/S Removes all directories and files in the specified directory
in addition to the directory itself. Used to remove a directory
tree.

/Q Quiet mode, do not ask if ok to remove a directory tree with /S

另一种禁用命令回显的方法是在命令的开头添加 @ 符号,它只对当前命令有效。

1
@rd /?

在实际应用中,经常把 @echo off 结合起来使用:

1
2
@echo off
rd /?

语法

语句

在批处理程序中,一条命令就是一条语句。语句也可以由多条命令复合而成,比如:

1
cls & chcp & echo

以下 3 种逻辑运算符可以用于组合语句:

  • && 只有前一条语句执行成功,后一条语句才会执行
  • || 只有前一条语句执行失败,后一条语句才会执行
  • & 无论前一条语句是否执行成功,后一条语句都会执行

可以在语句中间换行,但是换行时必须以 ^ 结尾,比如:

1
2
3
cmake -S .     ^
-B build ^
-A Win32

注释

批处理程序只支持两种形式的注释:单行注释和行内注释。单行注释必须独占一行,而行内注释可以跟在命令结尾,甚至可以嵌入命令。这两种注释都不能包含换行符。

添加单行注释的方法是用 rem 命令或者将注释防止两个 :: 之后;添加行内注释的方法是将注释放在两个 % 中间:

1
2
3
4
5
:: 这是单行注释
rem 这是单行注释

cd .. %这是行内注释%
cd %这是行内注释% ..

添加注释通常用 rem 命令。与其他命令一样,如批处理在执行时没有禁用命令回显,就会看到用 rem 命令添加的单行注释。

变量

变量用 set 命令设置。比如设置名为 message 值为 "Hello, World!" 的变量:

1
set message=Hello, World!

批处理中的变量都是字符串变量,并且在设置变量时无需把等号右边的字符串放在双引号中。

等号两边不应该有多余的空格,因为多余的空格会被视为变量名和字符串的一部分。下面的命令实际上设置了一个名为 "message " 值为 " Hello, World!" 的变量:

1
set message = Hello, World!

访问变量的方法是将变量名放在两个 % 中间,比如打印变量 message 的值:

1
2
echo %message%
rem output "Hello, World!"

变量的值也可以由用户输入。带 /p 选项执行 set 命令时,等号右边的字符串将用作提示。此时用户需要输入一个字符串并按下回车键。输入的字符串会被设为变量的值。

1
set /p number=Please enter a number: 

set 命令的 /p 选项只有在命令解释器的命令扩展启用时才有效。命令解释器的命令扩展默认启用。

环境变量

实际上,用 set 命令设置的变量都是环境变量,因此访问环境变量的方法也是使用两个 %

1
echo %PATH%

环境变量的变量名不区分大小写,因此 PATHpath 是同一个变量。

常用的环境变量有:

  • DATE 当前日期
  • TIME 当前时间
  • RANDOM 随机数,范围 0 - 32767
  • USERNAME 用户名
  • USERPROFILE 用户文件夹
  • TEMP 临时文件夹
  • CD 当前目录
  • PATH 搜索路径列表
  • OS 操作系统的名称
  • PROCESSOR_ARCHITECTURE 处理器的架构
  • NUMBER_OF_PROCESSORS 逻辑处理器个数

命令行参数

批处理程序的第 n 个参数用 %n 表示,其中 n 可以是 0 到 9 中的任何一个数字,%0 是批处理程序的路径,%* 相当于 %0 %1 %2 %3 ...

1
2
3
4
5
6
@echo off
echo %0
echo %1
echo %2
echo %3
echo %*

运行以上批处理时可以看到如下输出:

1
2
3
4
5
6
C:\Users\John>a.bat 1 2 3
a.bat
1
2
3
1 2 3

如果参数是一个文件的路径,还可以在 %n 之间可以插入 ~fd 等字符来获取该文件的完整路径和属性信息,例如:

语法 作用 结果
%1 - "a.txt"
%~1 去除参数两边的引号 a.txt
%~f1 绝对路径 C:\Users\John\a.txt
%~d1 盘符 C:
%~p1 路径 \Users\John\
%~n1 文件名 a
%~x1 扩展名 .txt
%~t1 修改日期 2022/11/08 22:30
%~z1 文件大小 386

延迟展开

每条语句中的变量在语句执行前就会被展开成相应的值,因此下面的程序会输出 1 而不是 2

1
2
set a=1
set a=2 & echo %a%

这是因为命令解释器在执行第二条语句前就会展开其中的 %a%,而变量 a 此时的值为 1

如果用 ! 代替 % 访问变量,解释器就会在执行语句时再展开变量,即延迟展开,例如:

1
2
3
setlocal EnableDelayedExpansion
set a=1
set a=2 & echo !a!

延迟展开默认是禁用的,需要用 setlocal EnableDelayedExpansion 命令开启。

运算符

批处理中的算术运算符、位运算符和复合运算符是由 set 命令提供的。使用 set 命令时,需要添加 /a 选项才能使用这些运算符,它表示将命令的参数视为表达式进行求解。例如,要计算 1 + 1 的值并赋给变量 sum

1
2
3
set /a sum = 1 + 1
echo %sum%
rem output "2"

在表达式中可以添加多余的空格并且引用环境变量时无需在变量名两边添加 %

1
2
3
4
set a=2
set /a b = a + 1
echo %b%
rem output "3"

set 命令支持 3 种进制:以 0x 开头的数字视为 16 进制,以 0 开头的数字视为 8 进制,其他数字视为 10 进制。set 命令不支持 2 进制。

1
2
3
4
5
set /a a = 0x12
set /a b = 18
set /a c = 022
echo %a% %b% %c%
rem output "18 18 18"

set 命令支持下列运算符:

  • 算术运算符:加 +、减 -、乘 *、除 /、取余 % 和取相反数 -
  • 位运算符
    • 按位与 &
    • 按位或 |
    • 按位异 ^
    • 按位取反 ~
    • 左移 <<
    • 右移 >>
  • 逻辑运算符:取反 !
  • 复合运算符:+=-=*=/=%=

表达式中包含位运算符或取余运算符时,需要将整个表达式放在双引号中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rem 0x03 = 0xb0011
rem 0x05 = 0xb0101

set /a "a = 0x03 & 0x05"
set /a "b = 0x03 | 0x05"
set /a "c = 0x03 ^ 0x05"
echo %a% %b% %c%
rem output "1 7 6"

set /a "d = 0xff<<32"
set /a "e = 0x08>>2"
set /a "f = ~0x01"
echo %d% %e% %f%
rem output "0 2 -2"

运算符的操作数是用补码表示的 32 位有符号数,因此任何数左移 32 位都将变成 0。16 进制数 0x01 转换成二进制数是 0...1,按位取反是 1...10,刚好是 2 的补码,因此 ~0x01 等于 -2

取反运算符 ! 的规则很简单:任何非 0 数取反都等于 0,只有 0 取反等于 1。

1
2
3
4
5
set /a g = !255
set /a h = !0
set /a i = !-128
echo %g% %h% %i%
rem output "0 1 0"

流程控制

条件语句

条件语句由 if 命令实现,完整形式如下:

1
2
3
4
5
if condition (
command
) else (
command
)

else 子句是可选的。if 语句的条件 condition 有以下几种形式。

判断文件是否存在

形如 exist filename 的条件用于判断文件 filename 是否存在; not exist filename 则用于判断文件 filename 是否不存在。文件的路径 filename 应该放在双引号中。

1
2
3
4
5
if exist "C:\Users\John\a.bat" (
echo true
) else (
echo false
)
判断变量是否有定义

形如 defined variable 的条件用于判断变量 variable 是否有定义; not defined variable 则用于判断变量 variable 是否未定义。变量名 variable 无需放在两个 % 中。

1
2
3
4
5
if defined message (
echo true
) else (
echo false
)
比较两个字符串或数字

可以用下列关系运算符比较两个字符串:

  • == 等于
  • equ 等于
  • neq 不等于
  • lss 小于
  • leq 小于等于
  • gtr 大于
  • geq 大于等于
1
2
3
4
5
if "%message%" == "aaa" (
echo "%message%" equal "aaa"
) else (
echo "%message%" not equal "aaa"
)

不同于 set 命令,用 if 命令比较两个字符串时,应该把字符串放在双引号中。如果比较两个字符串时不需要区分大小写,添加 /i 选项。

除了 == 之外,以上关系运算符还可用于比较两个数字。

1
2
3
4
5
if %number% lss 3 (
echo %number% less than 3
) else (
echo %number% greater than or equal 3
)
检查退出码

形如 errorlevel number 的条件用于判断上一个程序或上一条命令的退出码是否大于等于 number

1
2
3
if errorlevel 1 (
echo an error occurred
)

环境变量 errorlevel 的初始值为 0。当一个程序或一条命令遇到错误时,该变量就会被设置为一个大于 0 的数字。

循环语句

循环语句由 for 命令实现,基本形式如下:

1
2
3
for %%v in (set) do (
command
)

其中,%%v 是循环变量;小括号中的 set 是需要遍历的元素的集合,元素之间用空格或逗号隔开;do 之后是循环体。

循环变量的变量名只能由一个字母构成并且区分大小写。

通过搭配 for 命令的不同选项,set 部分可以有以下几种形式。

C 语言风格

带有 /l 选项时,set 由 3 个用逗号 , 或空格隔开的数字 start, step, end 组成。它表示循环变量从 start 递增到 end,每次递增 step

1
2
3
for /l %%i in (0, 1, 3) do (
echo %%i
)
遍历文件名

不带任何选项时,for 语句用于遍历文件名,此时 set 是文件名的列表,文件名可以包含通配符 *。例如,遍历当前目录下的所有 .bat 文件,以及 C:\Windows\SysWOW64 目录下所有以 api- 开头的动态库。

1
2
3
for %%f in ("*.bat", "C:\Windows\SysWOW64\api-*.dll") do (
echo %%f
)
遍历目录名

带上 /d 选项时,for 语句用于遍历目录名。例如,遍历 C 盘根目录和用户文件夹下的所有文件夹。

1
2
3
for /d %%d in ("C:\*", "%userprofile%\*") do (
echo %%d
)
遍历目录树

带上 r 选项时,for 语句用于遍历目录树,树根在 /r 选项之后给出,缺省时为当前目录,此时 set 是每个目录中需要遍历的文件名的列表。例如,遍历图片文件夹中所有 PNG 和 GIF 格式的图片:

1
2
3
for /r "%userprofile%\Pictures" %%f in ("*.png", "*.gif") do (
echo %%f
)
按行按列遍历文本

带上 /f 选项时,for 语句可以按行按列遍历文本。文本支持 3 种来源:

  • 文件的内容,此时 set 是文件名的列表,例如 a.txt
  • 字符串,此时 set 是字符串的列表,每个字符串必须放在双引号中,例如 "a b c"
  • 命令的输出,此时 set 是命令的列表,每个命令必须放在单引号中,例如 'dir'

默认情况下,for 语句只遍历每一行的第一列。

1
2
3
for /f %%i in (a.txt) do (
echo %%i
)

/f 选项之后可以插入下列参数来改变 for 语句的行为:

  • eol=c 将字符 c 视为行结束符。
  • skip=n 跳过前 n 行。
  • delims=xxxxxx 这些字符作为列的定界符。比如 delims=,; 表示用 ,; 作为定界符。
  • tokens=x,y,m-n 提取每一行的第 xym-n 列。

tokens 参数提取多列时,for 语句会按字母表顺序自动定义额外的循环变量,每个循环变量对应一列,以便在循环体中可以访问每一列。

1
2
3
for /f "tokens=1,3,5-7 delims=;" %%a in (a.txt) do (
echo %%a, %%b, %%c, %%d, %%e
)

函数

批处理程序并不支持真正意义上的函数。批处理程序中的函数更像是汇编程序中的子程序,它是通过 call 命令和 goto 命令的结合实现的。

跳转命令与标号

跳转命令 goto 要配合标号使用。标号是以 : 开头且独占一行的标识符,比如 :eof。用标号作为参数执行 goto 命令,标号下方的命令就会成为下一条被执行的命令。例如,在下面的批处理程序中,命令的执行顺序是:先执行 goto :output 命令,再执行 echo Hello, World! 命令。

1
2
3
4
goto :output
set message=Hello, World!
:output
echo Hello, World!

:eof 其实是预定义的标号,它表示批处理程序的末尾。如果跳转到批处理程序的末尾,批处理程序就会退出。例如,下面的批处理程序中的 echo 语句不会被执行,因为批处理程序在它之前就已经退出了。

1
2
goto :eof
echo Hello, World!

函数的定义和调用

函数的定义以一个标号开始,以一条 goto :eof 语句结束。

1
2
3
4
:add
set /a sum = %1 + %2
echo %1 + %2 = %sum%
goto :eof

函数要用 call 命令调用。使用 call 命令时,先给出函数的标号,再列出需要传递给函数的参数。

1
call :add 1 1

实际上,call 命令是用于调用其他批处理程序的命令,用 call 命令调用函数时,相当于再创建一个进程来执行当前批处理程序。新的进程会从函数的第一条命令开始依次执行批处理程序中的命令,因此,每个函数的末尾都要包含一条 goto :eof 命令用来结束新的进程。新的进程结束后,原来的进程就会继续执行 call 命令之后的命令。

标号本身并不会影响程序的走向。如果一个函数的标号前没有任何 goto 命令,解释器就会跳过标号执行这个函数中的命令。在批处理程序中定义和调用函数的完整示例如下:

1
2
3
4
5
6
7
call :add 1 1
goto :eof

:add
set /a sum = %1 + %2
echo %1 + %2 = %sum%
goto :eof

局部作用域

默认情况下,用 set 命令定义的变量都是全局变量,包括函数中的 set 命令。因此,在函数中可以读写函数外部定义的变量,在函数外部也可以读写函数中定义的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
set a=1
call :fn
echo a=%a%
echo b=%b%
goto :eof

:fn
set a=0
set b=2
goto :eof

rem output:
rem a=0
rem b=2

要定义局部变量,需要借助 setlocalendlocal 两个命令。这两个命令必须成对使用,它们中间会形成一个局部作用域,在局部作用域中定义的局部变量只在局部作用域中有效,在局部作用域中给全局变量重新赋值也不会影响原来的全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
set a=1

setlocal
set a=0
set b=2
endlocal

echo a=%a%
echo b=%b%

rem output:
rem a=1
rem b=

在启用命令扩展的情况下,setlocal 命令还支持下列参数:

  • EnableExtensions 在局部作用域中启用命令扩展
  • DisableExtensions 在局部作用域中禁用命令扩展
  • EnableDelayedExpansion 在局部作用域中启用环境变量延迟展开
  • DisableDelayedexpansion 在局部作用域中禁用环境变量延迟展开

函数的参数

访问函数的参数的方法与访问命令行参数的方法一致。

专题