windows bat批处理脚本由于低成本、高效益,从某种角度上说更像是一门艺术,人们用其可以以更简单的方式完成复杂的任务。遗憾的是,随着c、java、python、golang、javascript等高级语言的蓬勃发展,选择使用传统脚本方式解决问题的人员越来越少,甚至很多类脚本任务也通过perl或python等高级语言变通实现。
纵然脚本编程已是“老古董”,但不可否认它仍是运维人员解决问题的首要工具。因此对于运维人员来说,脚本编程仍是“酒中茅台”,简单、高效。同理,对于监控来说,脚本也是不二选择,得益于脚本与操作系统的浑然天成,意味着脚本只要写完即可工作,无需依赖任何运行环境。
之所以写本文的一个原因就是笔者最近在用zabbix进行应用监控时(监控指定进程的存活性、内存占用大小、CPU利用率等),发现zabbix自带功能不能实现,需要自行实现,考虑过用python、golang等实现,后来考虑到python、golang等需要zabbix agent机器安装相应运行环境,实施成本较高,最终选择使用脚本编程实现。由于linux shell脚本编程,教程相对较多、功能相对强大、实现也相对容易;而windows bat批处理编程不仅教程少、不好上手,实现起来相对麻烦。因此整理本文,希望能给广大windows bat脚本攻城狮一点点帮助。
我们学java、python等高级语言,恐怕第一个DEMO都是输出“Hello World”字符串,学习bat批处理脚本,不妨也来个“Hello World”。
可以用任何文本编辑器,当然使用editplus、notepad等编辑器效率会更高,输入下述代码并保存为D:\cmdtest\helloworld.bat。
- @echo off
- rem This is a "Hello World" program.
- echo Hello World!
- echo=
-
打开cmd命令窗口,切换到D:\cmdtest目录,运行helloworld 或 helloworld.bat
- D:\cmdtest>helloworld
- Hello World!
-
-
为了提高bat批处理脚本开发、测试效率,建议:
变量无需声明可直接引用,其值为空字符串,并且大小写不敏感。可使用defined关键字或是否为空字符串""判断变量是否为空,如下所示:
- rem 将代码保存为bat文件执行
- @echo off
- rem set var2="var2"
- if not defined var2 (
- echo var2 is not defined, the value is: %var2%
- ) else (
- echo var2 is defined, the value is: %var2%
- )
-
- if "%var2%"=="" (
- echo var2 is not defined, the value is: %var2%
- ) else (
- echo var2 is defined, the value is: %var2%
- )
-
说明:
- @echo off
- set var1=2+2
- set /a var2=2+2
- set /p var3=Please input a number:
- set /p md5=<file_info.md5
- echo var1: %var1%
- echo var2: %var2%
- echo var3: %var3%
- echo md5: %md5%
-
- D:\cmdtest>var
- Please input a number:100
- var1: 2+2
- var2: 4
- var3: 100
- md5: 76adfafs76776...
-
说明:
默认为全局变量(Global),可使用setlocal命令将变量作用域设置为local,直到endlocal或exit命令,或bat文件执行结束,变量local作用域也结束并恢复到global作用域,看下述DEMO。
var_scope.bat
- @echo off
- setlocal
- set v=Local Variable
- echo v=%v%
-
cmd命令框
- D:\cmdtest>set v=Global Variable
- D:\cmdtest>var_scope
- v=Local Variable
- D:\cmdtest>echo v=%v%
- v=Global Variable
- D:\cmdtest>
-
读者朋友们可以尝试将var_scope.bat文件中的setlocal命令注释掉,然后执行上述cmd命令框中的代码,我们将发现变量v最终输出的是“Local Variable”,即外面设置的变量v被bat文件中的变量v玷污了。
var_normal.bat
- @echo off
- set a=1
- set /a a+=1 > nul & echo %a%
-
var_normal.bat运行后将输出1,而不是2,原因如下:
当我们准备执行一条命令的时候,命令解释器会先将命令读取,如果命令中有环境变量,那么就会将变量的值先读取来出,然后在运行这条命令,如:echo %a%,当我们执行这条命令的时候,命令解释器会先读出%a%的值,即1,然后执行echo,所以输出1。
然而,上述脚本本意是输出a+=1运算后的a值,即2。bat脚本提供了变量延迟,即变量在使用时再读取,上述代码修改如下:
var_delay.bat
- @echo off
- setlocal EnableDelayedExpansion
- set a=1
- set /a a+=1 > nul & echo !a!
-
var_delay.bat 运行后会输出2,有2个注意事项:
上文已经提及很多内置变量或命令,此处的特殊变量指命令行参数,比如运行var_arg.bat arg1 arg2,带了2个参数arg1,arg2,那么如何表示脚本文件本身,参数1、参数2如何获取呢?
var_arg.bat
- @echo off & setlocal
- echo arg0=%0
- echo arg1=%1
- echo arg1 no quotes=%~1
- echo batfile fullpath=%~f0
- echo batfile=%~n0
- echo batfolder=%~dp0
-
- D:\cmdtest>var_arg "marcus"
- arg0=var_arg
- arg1="marcus"
- arg1 no quotes=marcus
- batfile fullpath=D:\cmdtest\var_arg.bat
- batfile=var_arg
- batfolder=D:\cmdtest\
-
说明:
通常来说一条命令的执行结果返回的值只有两个,0 表示"成功",1 表示"失败",实际上,errorlevel 返回值可以是一个任何整型值,一般只定义在0~255之间。
- @echo off
- rem return code demo
- exit /b %1
-
- D:\cmdtest>returncode 0
- D:\cmdtest>echo %errorlevel%
- 0
- D:\cmdtest>returncode 1
- D:\cmdtest>echo %errorlevel%
- 1
- D:\cmdtest>returncode -1
- D:\cmdtest>echo %errorlevel%
- -1
-
bat脚本文件中exit指定的code即返回码,就是下一行获取到的errorlevel值,从demo可以看出errorlevel甚至可以是负值。
如果bat脚本文件中没有exit code命令,bat文件执行结束后,会不会有返回码?没有,有点类似void函数,因此errorlevel仍然是上次的-1。
通常来说,可以根据errorlevel是否等于0来判断脚本是否成功执行(0表示成功,>0值表示失败),若明确脚本返回码的情况下,也可以根据具体返回码值做具体处理,DEMO如下:假设执行脚本后,errorlevel=0,则
- D:\cmdtest>if errorlevel 1 (echo fail) else (echo success)
- success
- D:\cmdtest>if %errorlevel% EQU 0 (echo success) else (echo fail)
- success
-
说明:
stdin:标准输入,重定向时也用数字0表示
stdout:标准输出,重定向时也用数字1表示
stderr:错误输出,重定向时也用数字2表示
标准输出重定向
- dir > dir.txt //dir文件、目录列表输出到dir.txt, dir.txt文件重新生成
- dir >> dir.txt //dir文件、目录列表添加到dir.txt, dir.txt存在则添加,否则新建
- echo line1 > line.txt //覆盖line.txt,内容为line1
- type con > line.txt //响应键盘输入,直到按ctrl+z结束,输出到line.txt文件
-
错误输出重定向
- d:\cmdtest\stdout>dir aaa 2>error.txt
- d:\cmdtest\stdout>type error.txt
- 找不到文件
-
标准、错误输出合并
通常我们会将标准输出和错误输出合并到一个文件,如下所示:
- d:\cmdtest\stdout>DIR SomeFile.txt > output.txt 2>&1
- d:\cmdtest\stdout>type output.txt
- 驱动器 D 中的卷是 软件
- 卷的序列号是 65F3-3762
-
- d:\cmdtest\stdout 的目录
-
- 找不到文件
-
说明:遍历SomeFile.txt,先将遍历结果输出到output.txt,如果出错则将错误信息添加到
output.txt(此处的“找不到文件”)。
标准输入
将某个文件作为内容输入,用 < 表示,如下所示:
- D:\cmdtest\stdout>sort < countries.txt
- America
- Australia
- China
- England
-
- D:\cmdtest\stdout>type countries.txt
- China
- America
- England
- Australia
- D:\cmdtest\stdout>
-
将countries.txt文件中的内容进行排序显示。
用NUL表示丢弃任何程序输出,2个经典应用:
- @echo off & setlocal
- set str1=The most severe place of New SARS is Wuhan.
- set str2=%~1
- echo %str1% | findstr /i "%str2%" > nul && (echo "found") || (echo "not found")
-
- D:\cmdtest>findstrex.bat wuhan
- "found"
-
- @echo off
- echo "program sleep 5 seconds, start..."
- ping /n 5 127.1>nul
- echo "program sleep 5 seconds, end..."
- exit /b 0
-
先输出"program sleep 5 seconds, start…",5秒后再输出"program sleep 5 seconds, end…"
管道符 | 通常用于一个命令的输出作为另一个命令的输入,如:
- DIR /B | SORT
-
DIR /B,/B 使用空格式(没有标题信息或摘要)。
DIR /B | SORT,将dir /b结果进行字符串排序
顺序、选择和循环是编程语言的常见3种语句,bat脚本也是如此,bat脚本if选择语句语法如下:
- if 条件 (do...)
- if 条件 (do...) else (do ...)
-
注意:
条件判断比较常见应用场景如下:
- @echo off
- IF EXIST "temp.txt" (
- ECHO found
- ) ELSE (
- ECHO not found
- )
-
- IF "%var%"=="" (TODO)
- IF NOT DEFINED var (TODO)
-
- @echo off & setlocal
- set /p arg1="please input a string:"
- set /p arg2="please input another string:"
- if %arg1%==%arg2% (echo %arg1% equals %arg2%) else (echo %arg1% not equals %arg2%)
- if not %arg1%==%arg2% (echo %arg1% not equals %arg2%) else (echo %arg1% equals %arg2%)
- if %arg1% equ %arg2% (echo %arg1% equals %arg2%) else (echo %arg1% not equals %arg2%)
- if %arg1% neq %arg2% (echo %arg1% not equals %arg2%) else (echo %arg1% equals %arg2%)
-
- set /p name="please input your name: "
- if /i "%name%"=="marcus" ( echo You are Marcus! ) else ( echo You are not Marcus! )
-
- D:\cmdtest\lianxi>str
- please input a string:aa
- please input another string:aa
- aa equals aa
- aa equals aa
- aa equals aa
- aa equals aa
- please input your name: aaa
- You are not Marcus!
-
说明:
- @echo off & setlocal
- set num1=1
- set num2=2
- if %num1% EQU %num2% (echo %num1% == %num2%) else (echo echo %num1% != %num2%)
-
- EQU,等于
- NEQ,不等于
- LSS,小于
- LEQ,小于等于
- GTR,大于
- GEQ,大于或等于
-
本文前面部分已经提及程序返回码处理,简单demo如下:
- if errorlevel 1 (TODO)
- if %errorlevel% equ 0 (TODO)
-
- set str="Apple,Huawei,Xiaomi,Oppo,Vivo"
- echo %str% | findstr /i "asus" > nul && (FOUND,TOTO) || (NOTFOUND, TODO)
-
bat脚本实现循环有2种方式:使用goto或for循环,简单demo如下:
goto实现方式,1-10的循环:
- @echo off
- set var=0
- rem ************loop start.
- :continue
- set /a var+=1
- echo loop time: %var%
- if %var% lss 10 goto continue
- rem ************loop end.
- echo loop execution finished.
-
for /L in (start, step, end) do ():
- @echo off
- set var=0
- rem ************loop start.
- for /L %%i in (1,1,10) do (echo loop time: %%i)
- rem ************loop end.
- echo loop execution finished.
-
说明:
对一组文件中的每一个文件执行某个特定命令。
FOR %variable IN (set) DO command [command-parameters]
%variable 指定一个单一字母可替换的参数。
(set) 指定一个或一组文件。可以使用通配符。
command 指定对每个文件执行的命令。
command-parameters
为特定命令指定参数或命令行开关。
在批处理程序中使用 FOR 命令时,指定变量请使用 %%variable
而不要用 %variable。变量名称是区分大小写的,所以 %i 不同于 %I.
如果启用命令扩展,则会支持下列 FOR 命令的其他格式:
FOR /D %variable IN (set) DO command [command-parameters]
如果集中包含通配符,则指定与目录名匹配,而不与文件名匹配。
FOR /R [[drive:]path] %variable IN (set) DO command [command-parameters]
检查以 [drive:]path 为根的目录树,指向每个目录中的 FOR 语句。
如果在 /R 后没有指定目录规范,则使用当前目录。如果集仅为一个单点(.)字符,
则枚举该目录树。
FOR /L %variable IN (start,step,end) DO command [command-parameters]
该集表示以增量形式从开始到结束的一个数字序列。因此,(1,1,5)将产生序列
1 2 3 4 5,(5,-1,1)将产生序列(5 4 3 2 1)
FOR /F [“options”] %variable IN (file-set) DO command [command-parameters]
FOR /F [“options”] %variable IN (“string”) DO command [command-parameters]
FOR /F [“options”] %variable IN (‘command’) DO command [command-parameters]
或者,如果有 usebackq 选项:
FOR /F [“options”] %variable IN (file-set) DO command [command-parameters]
FOR /F [“options”] %variable IN (“string”) DO command [command-parameters]
FOR /F [“options”] %variable IN (‘command’) DO command [command-parameters]
…
另外,FOR 变量参照的替换已被增强。您现在可以使用下列
选项语法:
%~I - 删除任何引号("),扩展 %I
%~fI - 将 %I 扩展到一个完全合格的路径名
%~dI - 仅将 %I 扩展到一个驱动器号
%~pI - 仅将 %I 扩展到一个路径
%~nI - 仅将 %I 扩展到一个文件名
%~xI - 仅将 %I 扩展到一个文件扩展名
%~sI - 扩展的路径只含有短名
%~aI - 将 %I 扩展到文件的文件属性
%~tI - 将 %I 扩展到文件的日期/时间
%~zI - 将 %I 扩展到文件的大小
%~P A T H : I − 查 找 列 在 路 径 环 境 变 量 的 目 录 , 并 将 到 找 到 的 第 一 个 完 全 合 格 的 名 称 。 如 果 环 境 变 量 名 未 被 定 义 , 或 者 没 有 找 到 文 件 , 此 组 合 键 会 扩 展 到 空 字 符 串 可 以 组 合 修 饰 符 来 得 到 多 重 结 果 : PATH:I - 查找列在路径环境变量的目录,并将 %I 扩展 到找到的第一个完全合格的名称。如果环境变量名 未被定义,或者没有找到文件,此组合键会扩展到 空字符串 可以组合修饰符来得到多重结果: %~dpI - 仅将 %I 扩展到一个驱动器号和路径 %~nxI - 仅将 %I 扩展到一个文件名和扩展名 %~fsI - 仅将 %I 扩展到一个带有短名的完整路径名 %~dpPATH:I−查找列在路径环境变量的目录,并将到找到的第一个完全合格的名称。如果环境变量名未被定义,或者没有找到文件,此组合键会扩展到空字符串可以组合修饰符来得到多重结果:PATH:I - 搜索列在路径环境变量的目录,并将 %I 扩展到找到的第一个驱动器号和路径。
%~ftzaI - 将 %I 扩展到类似输出线路的 DIR
在以上例子中,%I 和 PATH 可用其他有效数值代替。%~ 语法
用一个有效的 FOR 变量名终止。选取类似 %I 的大写变量名
比较易读,而且避免与不分大小写的组合键混淆。
下述代码请复制到bat文件执行
- @echo off
- rem 遍历字符串
- for %%i in (Hangzhou Ningbo Wenzhou Shaoxin) do echo %%i
-
- rem 遍历USERPROFILE下的文件
- FOR %%i IN (%USERPROFILE%\*) DO ECHO %%i
-
代码请在cmd命令框中运行
- 遍历目录
- 语法:FOR /D %variable IN (set) DO command [command-parameters]
- rem 遍历文件目录
- FOR /D %I IN (%USERPROFILE%\*) DO @ECHO %I
-
- 递归遍历
- 语法:FOR /R [[drive:]path] %variable IN (set) DO command [command-parameters]
- rem 递归遍历文件
- FOR /R "%TEMP%" %I IN (*) DO @ECHO %I
- rem 递归遍历文件目录
- FOR /R "%TEMP%" /D %I IN (*) DO @ECHO %I
-
下述代码请在cmd命令框中运行,@echo 表示关闭echo回显
- FOR /L %variable IN (start,step,end) DO command [command-parameters]
- 该集表示以增量形式从开始到结束的一个数字序列。
- rem (1,1,5)将产生序列 1 2 3 4 5
- FOR /L %i in (1,1,5) do @echo %i
-
- rem (5,-1,1)将产生序列(5 4 3 2 1)
- FOR /L %i in (5,-1,1) do @echo %i
-
语法说明:
- FOR /F ["options"] %variable IN (file-set) DO command [command-parameters]
- FOR /F ["options"] %variable IN ("string") DO command [command-parameters]
- FOR /F ["options"] %variable IN ('command') DO command [command-parameters]
-
- fileset 为一个或多个文件名。继续到 fileset 中的下一个文件之前,
- 每份文件都被打开、读取并经过处理。处理包括读取文件,将其分成一行行的文字,
- 然后将每行解析成零或更多的符号。然后用已找到的符号字符串变量值调用 For 循环。
- 以默认方式,/F 通过每个文件的每一行中分开的第一个空白符号。跳过空白行。
- 您可通过指定可选 "options" 参数替代默认解析操作。这个带引号的字符串包括一个
- 或多个指定不同解析选项的关键字。这些关键字为:
-
- eol=c - 指一个行注释字符的结尾(就一个)
- skip=n - 指在文件开始时忽略的行数。
- delims=xxx - 指分隔符集。这个替换了空格和跳格键的
- 默认分隔符集。
- tokens=x,y,m-n - 指每行的哪一个符号被传递到每个迭代
- 的 for 本身。这会导致额外变量名称的分配。m-n
- 格式为一个范围。通过 nth 符号指定 mth。如果
- 符号字符串中的最后一个字符星号,
- 那么额外的变量将在最后一个符号解析之后
- 分配并接受行的保留文本。
-
下述代码请保存为bat文件执行
- @echo off
- rem 分割字符串
- for /f "delims=, tokens=1,2,*" %%j in ("Hangzhou,Ningbo,Wenzhou") do echo %%j,%%k,%%l
-
- rem 逐行读取文件
- for /f %%j in (d:\cmdtest\stdout\line.txt) do echo %%j
-
- rem 执行命令,并将执行结果逐行读取
- for /f %%j in ('wmic process get caption') do echo %%j
-
delims,tokens,skip 选项说明
bat脚本没有显性的function、sub等关键字,我们可以通过goto变相实现函数,局部代码重用。
calc.bat
- @echo off & setlocal
- rem This is a simple calculator.
- rem usage: calc + 3 5, result is 3+5=8.
-
- set m=%~1
- set a=%~2
- set b=%~3
- set result=0
- if "%m%"=="+" goto add
- if "%m%"=="-" goto sub
- if "%m%"=="*" goto mul
- if "%m%"=="/" goto div
- echo "invalid arguments, usage: calc + 3 5, result is 3+5=8."
- goto :eof
-
- :add
- set /a result=a+b
- goto result
-
- :sub
- set /a result=a-b
- goto result
-
- :mul
- set /a result=a*b
- goto result
-
- :div
- set /a result=a/b
- goto result
-
- :result
- echo %a% %m% %b% = %result%
-
- D:\cmdtest\lianxi>calc s 1 2
- "invalid arguments, usage: calc + 3 5, result is 3+5=8."
- D:\cmdtest\lianxi>calc + 1 2
- 1 + 2 = 3
- D:\cmdtest\lianxi>calc - 1 2
- 1 - 2 = -1
-
说明:
至此,windows bat 批处理脚本编写指南告一段落,知识点包括:
基于上述知识点,基本上可以完成绝大多数bat批处理脚本。如要深究BAT脚本不妨参考下述博文:
BAT脚本知识点深究:
BAT脚本案例: