Guide to Windows Batch Scripting
http://steve-jansen.github.io/guides/windows-batch-scripting/index.html
Overview
batch可以帮助配置 DevOps , 提高每天的工作效率.
Part 1 – Getting Started
Launching the Command Prompt
keyboard shortcut Windows Logo Key + R
输入cmd.exe
Editing Batch Files
Windows Logo Key + R
输入 notepad
或 notepad++
batch文件是 ASCII text, 差不多所有的编辑器都可以用来编辑它;
Viewing Batch Files
可以直接在编辑器中查看batch文件;
在 DOS cmmand中可以使用下列命令查看文件:
- TYPE myscript.cmd
- MORE myscript.cmd
- EDIT myscript.cmd
Batch File Names and File Extensions
建议的后缀名: .cmd
90年代的 Windows使用 .bat
区别:
avoid some rare side effects with .bat files
The differences between .CMD and .BAT as far as CMD.EXE is concerned are: With extensions enabled, PATH/APPEND/PROMPT/SET/ASSOC in .CMD files will set
ERRORLEVEL regardless of error. .BAT sets ERRORLEVEL only on errors.
使用 .cmd
后缀名, 你可以使用任何文件名; 建议不要在文件名中加入空格, 在 shell脚本中空格只会带来麻烦;
利用Pascal cast可以帮助避免空格;
e.g. 使用 HelloWorld.cmd
代替 Hello World.cmd
; 可以使用标点如 .
or -
or _
(e.g. Hello.World.cmd, Hello-World.cmd, Hello_World.cmd
)
另外要注意的是, 避免和 built-in的 command, 系统文件或者流行的程序重名.
e.g. 避免使用 ping.cmd
为已经有了一个广泛使用的系统文件 ping.exe
; 如果你无意间运行 ping
, 有可能是调用了 ping.cmd
而不是真正想要的 ping.exe
, 那么情况会变得很令人困惑.
建议使用 RemoteHeartbeat.cmd
或在脚本名字中添加一些细节, 这样还能避免和其他的可执行文件发生名字冲突.
当然, 也存在很特殊的情况: 就是你想把ping
的默认行为修改掉, 那么可以无视这里的名字规范.
Saving Batch Files in Windows
Notepad默认会尝试将所有文件当做 plain jane text[普通]文件格式保存;
要让 Notepad将文件保存为 .cmd
格式, 需要使用 “Save as type”下拉菜单选为 “All Files(.)”;
注意, Encoding选项, 英文一般为 ASCII.
e.g. 文件名设为 %USERPROFILE%\HelloWorld.cmd
%USERPROFILE%
关键字是 Windows环境变量, 代表 user profile文件夹的目录地址; 在较新的 Windows系统中, user profile文件夹一般是:C:\Users\<your username>
;这个shortcut可以帮你省点时间, 因为一个新的 command prompt通常是默认是在你的 user profile文件夹的 “working directory”目录下;
有了这个shortcut, 就可以不用预先修改当前目录或者指定脚本路径了.
Running your Batch File
运行batch文件的简单方式是双击文件; 不过, 在command prompt中才有机会看到更多的output和error.
当脚本退出的时候, command prompt的窗口会立即消失. ()参见 Part 10 – Advanced Tricks)
运行一个新脚本时, 你可能向要在打开的command窗口中运行batch文件; 对于初学者, 可以直接把脚本拖到command prompt窗口中; command prompt在command line显示出脚本的全路径, 将包含空格的路径用引号括起.
Tips:
- 可以按上下箭头键. 在command line历史中查找用过的命令.
- %COMPSPEC% /C /D "C:\Users\User\SomeScriptPath.cmd" Arg1 Arg2 Arg3
这个命令让脚本在一个新的command prompt 子进程中运行. /C
选项表示脚本结束时子进程退出; /D
选项将disable auto-run脚本 (可选);
这么做的原因是: 防止command prompt窗口被自动关闭 – 被EXIT
命令退出; EXIT
命令会自动关闭command prompt窗口, 除非它是在command prompt的子进程中调用的. 窗口被关闭挺烦人的, 因为你来不及看脚本所输出的信息.
Comments
官方定义了 REM
(Remark)关键字:
REM This is a comment!
power user方法是使用::
, 这是利用两个lable操作符:
的hack方式
多数人发现::
比起REM
来不那么容易分心; 不过要注意有几个地方’::’会造成error.
:: This is a comment too!! (usually!)
e.g. FOR
循环会对 ::
风格的注释输出error. 简单的回退方案是使用REM
Silencing Display of Commands in Batch Files
第一行非注释的batch命令一般是关闭打印(ECHO)
@ECHO OFF
@
是个特殊操作符, 用来抑制command line中的打印; 一旦将 ECHO设为off, 就不再需要 @
操作符了.
恢复命令行打印:
ECHO ON
在退出脚本前, command prompt会自动恢复ECHO的上个状态.
Debugging Your Scripts
batch有许多的 trial和 error coding. 可惜这里没有 WIndows batch script的 debugger(调试器). 更糟的是, 这里也没有将 command processor放入 verbose state来帮助troubleshoot 的方法 (这是Unix/Linux脚本的通用技术).
使用ECHO
打印自定义的ad-hoc(专门)信息可能是唯一的选择; 进阶的脚本开发者可以使用一些trickery来选择性地打印信息, 不过还是建议在脚本功能完善的时候把 debugging/instrumentation 代码移除掉.
Part 2 – Variables
变量, 在一个non-trivial batch程序中是必须的; 变量的语法会有些奇怪;
Variable Declaration
DOS无需声明变量. 未声明/未初始化的变量是个空的string, 或者""
. 多数人喜欢这个, 因为可以减少代码量; 不过这样也容易出傻bug, 比如变量名有typo(type error打印错误);
Variable Assignment
SET
命令将值赋给变量
SET foo=bar
NOTE:不要在名字和之间加入whitespace; SET foo = bar
无法工作;
/A
可以在赋值时打开arthimetic支持; 这个工具很有用, 可以检验用户输入是否是个数字值;
SET /A four=2+2
4
一般的约定是给变量使用小写名字; 系统级别的变量, 比如环境变量, 使用大写名字; 这些环境描述符指示了系统中某些东西的地址, e.g. %TEMP%
是临时文件的目录;
DOS是大小写敏感的, 因此约定虽然没有强制, 但最好是这么配置, 让阅读和调试简单些.
WARNING: SET
总是会覆盖(clobber)任何已有的变量; 在写脚本的时候最好先检验一下是否会将系统级别的变量覆盖; 快速方法是 ECHO %foo%
, 确认 foo
不是个已有变量;
e.g. 你可能想命名一个”temp”变量, 但并不想要改变环境变量“%TEMP%”; DOS包含一些”dynamic”环境变量, 它们更像是命令; 这些dynamic变量包含 %DATE%
, %RANDOM%
, %CD%
; 覆盖这些dynamic变量是个糟糕的主意;
Reading the Value of a Variable
大多数情况下你可以用 %<var>%
来读取变量的值; 下面的例子在console中打印出foo
变量的当前值:
C:\> SET foo=bar
C:\> ECHO %foo%
bar
有些特殊情况变量不使用%
语法;
Listing Existing Variables
不带参数的 SET
命令会将当前command prompt会话中的所有变量打印出来; 其中多数是系统级别的环境变量, 如 %PATH%
,%TEMP%
.
NOTE: 调用 SET
会列出当前对话中所有的 regular(static)变量; 列表中包含 dynamic环境变量如 %DATE%
, %CD%
; 可以通过SET的帮助文档来查看这些dynamic环境变量: 调用 SET /?
.
Variable Scope (Global vs Local)
默认情况下, 变量对于整个command prompt会话来说是global的; 调用 SETLOCAL
目录来使得变量对于脚本来变成local; 调用 SETLOCAL
后, 变量的赋值会在调用 `ENDLOCAL’, ‘EXIT’或者执行到达脚本的EOF(end of file)之后被revert(恢复);
下例演示了改变一个已有变量名foo
, 脚本为 HelloWorld.cmd
; shell在HelloWorld,cmd
退出时恢复变量%foo%
的原始值;
>TYPE HelloWorld.cmd
SETLOCAL
SET v=Local Value
ECHO v=%v%
>SET v=Global Value
>ECHO v=%v%
v=Global Value
>HelloWorld.cmd
>SETLOCAL
>SET v=Local Value
>ECHO v=Local Value
v=Local Value
>ECHO v=%v%
v=Global Value
>
真实世界中的例子可能会是一个修改系统级别%PATH%
环境变量的脚本,
>TYPE LocalPath.cmd
SETLOCAL
SET PATH=%SystemRoot%\system32
ECHO %PATH%
>ECHO %PATH%
REM Original %PATH%
"C:\Windows\system32;C:\Windows\System32\WindowsPowerShell\v1.0\"
>LocalPath.cmd
>SETLOCAL
>SET PATH=%SystemRoot%\system32
>ECHO %PATH%
REM %PATH% modified locally
"C:\Windows\system32"
>ECHO %PATH%
REM Original %PATH% restored
"C:\Windows\system32;C:\Windows\System32\WindowsPowerShell\v1.0\"
Special Variables
有少数特殊情况下变量工作起来有些不同; 在command line上传递给脚本的参数也是变量, 然而, 却不使用 %var%
语法;
代替的是: 使用单个%
带数字0-9来读取每个参数, 每个数字依次代表参数的位置; 在随后的bacth脚本中可以看到一个创建function/subroutine的hack方式有一样的风格;
还有些变量语法使用 !
, !var
; 这是个调用delayed expansion(延迟扩展)的特殊情况; 在后面讨论 condition(if/then)和looping的时候可以了解更多细节;
Command Line Arguments to Your Script
通过特殊语法可以读取传递给脚本的command line参数;
语法: 单个%
字符后面跟参数的位置0-9
; zero顺位参数是batch文件的名称; 因此脚本HelloWorld.cmd
中变量%0
代表”HelloWorld.cmd”;
command line参数:
- %0
脚本/程序名, 总是个非空值
- %1
第一个command line参数, 如果参数没提供, 就为空;
- %2
第二个command line参数, 如果参数没提供, 就为空;
- …
- %9
第九个参数
NOTE: DOS支持超过9个command line参数, 不过你无法直接读取第10个或更多的参数; 因为特殊变量语法不支持 %10
或更多; 事实上, shell会将 %10
读作 %0
的后缀 –> sting “0”;
使用 SHIFT
目录将第一个参数从参数列表中pop出去, 这样就把所有的参数左移一格; e.g. 第二个参数从位置 %2
移到 %1
, 这样第10个参数就成了 %9
;
后面会看到如何在循环中处理大量的参数;
Tricks with Command Line Arguments
Command Line Arguments也支持一些有用的可选语法, 在command line参数是文件名的时候, 对其运行quasi-macros(类似宏)的操作;
这些 marco被称为 变量substitution support, 可以从command line参数中解析path, timestamp, 或者file size;
对于这个超有用的特性来说文档有些难找 – 运行 FOR /?
来查看.
%~1
对第一个command line参数移除quotes(引号), 这个在参数作为文件路径时很有用; 你会需要在文件路径上加上引号, 但是, 在一个文件路径上两次加上引号会造成 file not found error.SET myvar=%~1
%~f1
是第一个参数的folder(文件夹)的全路径%~fs1
和上面的一样, 但是多出来的s
选项 yield出 DOS 8.3 short name path到第一个参数中; (e.g.C:\PROGRA~1
是常用的8.3 short name变体 – 对于C:\Program Files
)在使用第三方脚本或程序的时候这很有用, 这样就无需处理文件路径中的空格了;
%~dp1
是第一个参数的 parent folder的全路径; 在每个测试脚本文件自身位置的batch文件中都可以用这个trick;语法
SET parent=%~dp0
会将脚本的 folder的路径放入变量 ‘%parent%’.%~nx1
代表第一个参数中的文件名以及文件后缀名; 可以用来在运行时检测脚本的名字; 如果需要将信息打印出来, 可以给消息加上脚本名称的前缀: e.g.ECHO %~n0: some message
代替ECHO some message
;这里的前缀可以帮到终端用户, 可以知道输出是从脚本打印而不是另一个脚本调用的程序所打印的; 要是花几个小时来追踪一个脚本生成的obtuse error message的话, 其实是挺蠢的. 这是从 Unix/Linux世界中学到的一招改进;
Some Final Polish
在batch脚本顶部加入
SETLOCAL ENABLEEXTENSIONS
SET me=%~n0
SET parent=%~dp0
SETLOCAL
命令确保在脚本退出时就不会再clobber已有变量; ENABLEEXTENSIONS
参数启用一个很有用的特性, 称为 command processor extension;
%me%
中保存的是脚本名(不包括文件后缀); 可以给打印的message加上前缀 (e.g. ECHO %me%: some message
)
%parent%
中存储的是脚本的parent path(父路径); 可以用来给当前脚本相同目录下的文件提供 fully qualified filepath.
Part 3 – Return Codes
return code是和脚本外部的执行者进行交流的正确方法; 可惜的是很多Windows开发者都忽视了它.
Return Code Conventions
根据约定, 当执行成功, command line execution应该返回 zero, 而执行失败则返回 non-zero; Warning message则不影响return code.
Checking Return Codes In Your Script Commands
环境变量%ERRORLEVEL%
包含了最后执行的程序或脚本的 return code; 一个有用的特性: built-in的 DOS命令如 ECHO
, IF
, SET
会保留现有的 %ERRORLEVEL%
的值;
检查 non-zero return code的conventional technique(传统方法)是在 IF
命令中使用 NEQ
(Not-Equal-To)操作符;
IF %ERRORLEVEL% NEQ 0 (
REM do something here to address the error
)
另一个通用的方法:
IF ERRORLEVEL 1 (
REM do something here to address the error
)
当return code是任何数字–等于1或大于1时, ERRORLEVEL 1
语句为true;
不过程序返回正数的同时也有可能返回负数, 这时候这个语句就会有问题;
大多程序不会描述每个可能的return code, 因此最好是显式地检查 non-zero: NEQ 0
风格, 而不要假设 return code在error时会是1或大于1;
一些特殊的error code. e.g. 测试一个可执行程序或脚本是在你的PATH中, 调用程序然后检查 return code 9009;
SomeFile.exe
IF %ERRORLEVEL% EQU 9009 (
ECHO error - SomeFile.exe not found in your PATH
)
不是多查看trial和尝试error的话很难预先了解到这些特殊用法; 记住, 这是 duct tape programming. 它不总是很漂亮, 但可以完成工作;
Conditional Execution Using the Return Code
这里有个超赞的shorthand(便利方法)用来基于第一个命令的成功或失败, 执行第二个命令; 第一个程序/脚本必须遵从成功返回0, 失败返回non-0的规则;
要在成功后执行一个 follow-on(跟随)命令, 使用 &&
操作符;
SomeCommand.exe && ECHO SomeCommand.exe succeeded!
要在失败后执行一个 follow-on命令, 使用 ||
操作符;
SomeCommand.exe || ECHO SomeCommand.exe failed with return code %ERRORLEVEL%
和面向对象中的 &&, || 操作符不同, 0 && xxx 竟然是执行xxx, non-0 || xxx 竟然是执行 xxx; 而且 0 || xxx不执行后面的, non-0 && xxx也不执行后面的… 简直是反过来的短路规则
这项技术可以用来在发生错误的时候halt(叫停)脚本; 默认情况下, command processor会在error出现的时候继续执行; 为了实现 halt on error(错误时中止), 不得不要进行编码;
一个halt on error的简单方案是使用 带 /B
switch(开关)的 EXIT
命令(退出当前的batch脚本内容, 而不是command prompt process);
如果是从外部执行batch脚本, 则还是会退出CMD.exe;
还要从失败的命令中返回一个特定的 non-zero return code来通知调用者:
SomeCommand.exe || EXIT /B 1
类似的方法还有使用 隐式的 GOTO lable :EOF
(End-Of-File); 这样跳到EOF可以退出当前脚本, 并且return code为 1;
SomeCommand.exe || GOTO :EOF
Tips and Tricks for Return Codes
推荐成功的 return code都为 zero, 对于 DOS batch文件都返回正值; 正值是因为调用者可能会使用 IF ERRORLEVEL 1
语法来检测脚本;
另外建议给可能的return code都加上文档, 在脚本头上使用便于阅读的 SET
语句:
SET /A ERROR_HELP_SCREEN=1
SET /A ERROR_FILE_NOT_FOUND=2
注意这里打破了之前的约定, 使用了大写的变量名 – 这是想表示这些变量是常量, 而且在任何地方都不该被修改; DOS不像 Unix/Linux shell那样可以支持常量值, 这挺糟糕的.
Some Final Polish
一个小小的优化: 按2的幂次定义 return code;
SET /A ERROR_HELP_SCREEN=1
SET /A ERROR_FILE_NOT_FOUND=2
SET /A ERROR_FILE_READ_ONLY=4
SET /A ERROR_UNKNOWN=8
这样就可以有更多灵活性: 使用 bitwise OR(按位或)多个 error number, 可以在一个error code中记录多个问题; 这在interactive(交互式)的情况下很少用到, 但在编写脚本时, 如果你缺乏对目标系统的权限, 就会非常有用;
@ECHO OFF
SETLOCAL ENABLEEXTENSIONS
SET /A errno=0
SET /A ERROR_HELP_SCREEN=1
SET /A ERROR_SOMECOMMAND_NOT_FOUND=2
SET /A ERROR_OTHERCOMMAND_FAILED=4
SomeCommand.exe
IF %ERRORLEVEL% NEQ 0 SET /A errno^|=%ERROR_SOMECOMMAND_NOT_FOUND%
OtherCommand.exe
IF %ERRORLEVEL% NEQ 0 (
SET /A errno^|=%ERROR_OTHERCOMMAND_FAILED%
)
EXIT /B %errno%
如果 和 都失败了, return code会是 0x2和 0x4的bitwise combination(按位组合), 或者是数字6; 这个 return code告知我们两个 error都出现了; 更进一步, 可以用相同的 error code反复地调用 bitwise OR, 仍然可以解读到哪些 error出现过.
Part 4 – stdin, stdout, stderr
DOS, 就像 Unix/Linux, 使用三个universal “”files” - keyboard input(键盘输入), printing text on screen(屏幕字符输出), printing errors on screen(屏幕错误输出);
“Standard In(标准输入)”文件–stdin, 包含程序/脚本的输入;
“Standard Out(标准输出)”文件–stdout, 用来将输入写到屏幕显示;
“Standard Err(标准错误)”文件–stderr, 包含显示到屏幕上的错误消息;
File Numbers
对于这三个标准文件, 作为standard stream(标准流), 使用数字 0,1,2来引用; stdin是 file 0, stdout是 file 1, stderr是 file 2;
Redirection
batch文件的一个任务是将程序的输出传到 log文件; >
操作符send(传送), 或者 redirect(重定向) stdout或 stderr到另一个文件;
e.g. 可以将当前目录结构列表写入文件:
DIR > temp.txt
>
操作符会使用从 DIR命令返回的 stdout覆盖 temp.txt的内容; >>
操作符是个slight variant(轻量级变量), 它把输出 append到目标文件, 而不是覆盖目标文件.
一个通用的技术是使用 >
来创建/覆盖 log文件, 然后使用 >>
在之后append到 log文件中.
SomeCommand.exe > temp.txt
OtherCommand.exe >> temp.txt
默认情况下, >
和 >>
操作符重定向 stdout. 可以在操作符前面使用文件编号 2
来重定向 stderr:
DIR SomeFile.txt 2>> error.txt
甚至还可以用文件编号和 &
前缀来结合stdout和stderr流:
DIR SomeFile.txt 2>&1
如果想要把 stdout和 stderr都写入同一个文件时这很有用.
DIR SomeFile.txt > output.txt 2>&1
要将文件内容作为一个程序的输入, 代替手动一个个字地从键盘输入, 可以用 <
操作符:
SORT < SomeFile.txt
—TBC—
—YCR—