前言

batch 也就是批处理, 是 Windows 中的一种程序, 可以认为 batch 实际上就相当于 Linux/Unix 系统中的 shell 程序, 其特有的使用灵活, 容易开发的特点, 让这种程序在操作文件系统, 实现工作流程自动化的领域具有很重要的作用, 本文以两个脚本作为例子, 对 bat 编程进行简单的讲解.

本文默认你具有基本的编程知识, 并具有一定的 Windows 基础.

此外, 在 python, cpp 或者其他语言中, 可以使用一些命令直接调用 batch 指令. 例如 cpp 中比较常见的 system("pause"); 指令.

常用符号与指令

首先是常用符号:

  • @: 回显屏蔽, 参考 echo 指令.
  • > 与 >>: 重定向1, 可以理解为将本来应该出现在一个地方的信息转移至另一个地方, 一般用来写入文件, 例如 echo 123 > 123.txt 就是将本来应该出现在屏幕上的 123 写入 123.txt 中, > 和 >> 的区别在于, 前者是当文件存在时, 清空文件再写入, 后者是当文件存在时, 在文件尾部添加新的信息; 当文件不存在时, 二者都是新建文件并写入信息.
  • <: 重定向2, 可以理解为重定向1的逆操作, 可以理解为读取文件, 执行 set /p a=<123.txt 之后, 再执行 echo %a% 即可输出 123.txt 文件中的内容.
  • |: 管道符号, 将管道符号前面命令的输出结果重定向输出到管道符号后面的命令中去,作为后面命令的输入。使用格式为:command_1|command_2, 例如 echo y|del /p 123.txt 就是直接在当 del 命令是否决定删除时, 将 y 作为输入, 确定删除文件.
  • ^: 转义字符
  • &, &&, ||: 逻辑操作符, 区别在于 & 无条件执行符号后面的命令, && 只在前面的命令执行成功后才会执行后面的命令, || 只在前面的命令执行失败时才执行后面的命令, 否则不执行.

常用指令:

  • echo: 回显, 也就是在控制台显示这一语句后面的内容, 例如 echo 123 的运行结果就是 123; echo on|off 为开关回显功能.
  • rem: 注释命令
  • pause: 暂停命令
  • call: 从一个批处理中调用另一个批处理命令, 并不会终止当前批处理命令.
  • start: 调用外部程序, 例如 start calc.exe 可以打开 Windows 计算机程序, 如果不使用 start 命令的话, 在你运行外部程序的时候, 桌面上会保留一个 cmd 窗口.
  • goto: 跳转命令, 需要注意的是, 批处理中并没有 if-else 语句或者其他类似的语法, 而要用 goto 语句来实现, 实际上在计算机底层, if-else 也是通过这样的方式实现的, 只是编译器隐藏了这些步骤而已. 用法为 goto LABEL, 其中 LABEL 是标签, 在批处理中, 行首以 : 开头的单词就是标签, 具体可以参考本文的案例.
  • set: 设置, 显示, 或者清除变量, set 或者 set s 显示当前系统中的所有环境变量, set 变量名=值 即可赋值给变量, set 变量名= 即可设置变量值为空, 也就是删除变量, 一些交互功能需要使用到这个命令.

其他:

  • %1 ~ %9: 这些是命令行传递给批处理的参数, 例如执行命令 cd workspace\ 那么 %1 就是 workspace\, 利用这个功能这样可以很方便地在程序之间交换参数. 特别的, %0 就是命令本身, 例如上面的例子中, %0 就是 cd 本身, %* 是从第一个(而不是第 0 个)参数开始的全部参数.
  • 命令: find 查找命令, more 逐屏显示, tree 列出目录结构…
  • 当你不知道一个命令的作用时, 可以在 cmd 中运行 命令 /? 或者是 命令 -h 来获取帮助, 后者是 Linux/Unix 系统中的帮助参数.

案例

特别说明: 由于这种实现工作自动化的脚本我一直认为都是能用就行, 因此很多地方都使用的简单粗暴且丑陋的方法, 你当然可以写出更好看的程序, 但花费更多时间来优化一个使用场景单一且几乎不会修改的(因为如果有必要那么基本都是直接重写)程序我认为是没什么必要的.

加密脚本

以下的代码实际上是一个简单的加密脚本, 利用 Windows 保留文件名来保护需要隐藏的文件, 这种方法对付君子是毫无问题的了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
rem 运行前清理屏幕
cls
rem 关闭回显
@echo off
echo Choose directory:
rem 向用户获取文件目录名称, 使用 set /p 指令获取用户输入实现交互
set /p name=
rem 当存在 name 目录时, 跳转到 :encrypt 标签
rem 如果不存在, 继续执行下一行命令
if exist "%name%" goto encrypt

rem 输出文件不存在
echo File not exist!
rem 利用 set /p 来实现暂停, 同时获取用户输入
set /p var=
rem 判断用户输入密码是否正确 (没错, 密码是明文储存的, 所以不要指望
rem 这个加密脚本能防到非小白, 当然你可以通过将 bat 封装为 exe 来避
rem 免一般人获取密码), 如果不正确, 跳转至 :end 标签
if NOT "%var%"=="password" goto end
rem 执行到这一步说明用户第一次输入的 name 不存在且第二次输入的密码
rem 正确, 当存在 private 文件夹, 也就是公开未加密文件夹时, 跳转到
rem :lock 标签对其加密
if exist private goto lock
rem 当存在某个文件夹的名字是 Windows 保留的回收站文件夹名时, 跳转
rem 到 :unlock 标签解锁文件夹
rem 显然, 这就是我们隐藏文件的方法: 利用保留文件夹名称来隐藏文件
rem 此时这个文件夹看起来和用起来就和垃圾桶一模一样了
if exist .{645ff040-5081-101b-9f08-00aa002f954e} goto unlock

rem :lock 标签
:lock
rem 当已经存在加密文件夹时, 跳转至 :conflict 冲突标签
if exist .{645ff040-5081-101b-9f08-00aa002f954e} goto conflict
rem 当文件不冲突时, 将 private 文件夹重命名为 Windows 回收站保留名
ren private .{645ff040-5081-101b-9f08-00aa002f954e}
rem 新增 hide(隐藏) system(系统) 两个属性
rem 这样在资源管理器中要分别打开显示隐藏文件和显示系统文件两个选项,
rem 文件才会出现, 而即使这两个选项都被开启, 用户双击这个文件夹,
rem Windows 也会直接跳转到回收站, 当然, 有一些可以绕开跳转的方法,
rem 但我想大部分人不是特地搞事的人是不知道这样的方法的
attrib +h +s .{645ff040-5081-101b-9f08-00aa002f954e}
echo successfully locked !!
rem 加密完成, 跳转到 :end 结束标签
goto end

rem 解锁标签
:unlock
rem 当已经存在 private 文件夹, 跳转至 :conflict 冲突标签
if exist private goto conflict
rem 去掉系统文件和隐藏文件的属性
attrib -h -s .{645ff040-5081-101b-9f08-00aa002f954e}
rem 重命名回 private 文件夹
ren .{645ff040-5081-101b-9f08-00aa002f954e} private
echo successfully unlocked !!
goto end

rem 当用户第一次输入的 name 文件(夹)存在时, 进入加密/解密过程
rem 注意: 这里的加密仅仅是增加系统文件属性和隐藏文件属性,
rem 并不会修改文件名, 这也是这个脚本的一个特点, 即脚本本身可以正常
rem 使用普通加密功能, 如果不知道密码是不会进行保留名加密/解密的
:encrypt
echo "%name%" does exist, encrypt or decrypt?(e/d/esc):
rem 要求用户输入是否加密, e 则进入 :yes 标签, d 则进入 :no 标签,
rem esc 放弃此次加密
set /p operation=
if "%operation%"=="e" goto yes
if "%operation%"=="d" goto no
if "%operation%"=="esc" goto end
rem 三者都未命中的话, 输出参数错误, 并返回 :encrypt 标签
echo Invalid argument!
goto encrypt

:yes
attrib +h +s "%name%"
echo successfully locked "%name%" !!
goto end

:no
attrib -h -s "%name%"
echo successfully unlocked "%name%" !!
goto end

:conflict
echo conflict!

:end
rem 需要注意的是, 由于在 cmd 中直接使用命令调用脚本, 脚本结束时
rem 并不会自动清除已经设定的变量, 因此要在这里进行一下清理
set name=
set var=
set operation=
pause

工作自动化脚本

以下是我在使用 hexo 编译静态博客时, 为实现在发布博客的同时把源文件推送至远程仓库另一分支的功能, 所编写的一个简单脚本. 与上一个脚本不一样的地方是, 这里我并没有使用交互式的方式来操作脚本, 而是使用参数的方式来改变脚本行为. 此外由于没有用 @echo off 指令关闭回显, 在不需要回显的语句前插入 @ 符号以关闭回显.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
rem 根据第一个参数的不同, 跳转到不同的标签
@if "%1"=="-h" goto bat_help
@if "%1"=="-c" goto clean_and_deploy
@if "%1"=="-l" goto local
rem 当没有参数时或者参数错误时, 运行默认(也是最常用的)标签中的语句,
rem 这样虽然不是很符合标准, 但比较方便, 需要注意的是, 由于参数错误
rem 时也会跳转到默认标签, 因此要**特别注意**默认标签中的操作
@goto only_deploy

:clean_and_deploy
rem 添加当前目录中的所有文件到仓库中, 这里涉及一些 git 和 hexo 知识
git add .
rem 提交修改
git commit -m "update blog"
rem 当第二个参数是 `-l` 时, 跳转到 :only_clean 标签
@if "%2"=="-l" goto only_clean
rem 否则将本地仓库提交到远程仓库的 hexo 分支
git push origin hexo
rem 清理 public\ 目录, 生成文件并发布
hexo clean && hexo g -d
@goto end

rem 只生成文件和提交到本地仓库而不发布到网络
:local
git add .
git commit -m "update blog"
hexo g
@goto end

rem 只清理和生成文件, 不提交到仓库也不发布
:only_clean
hexo clean && hexo g
@goto end

rem 只发布而不清理 public\ 文件夹, 这也是默认运行的指令
:only_deploy
git add .
git commit -m "update blog"
git push origin hexo
hexo g -d
@goto end

rem 提供帮助信息, 如果输入 go -h 则会显示帮助信息
rem go 是我给这个脚本起的名字
:bat_help
@echo Usage: go [-h] [-c] [-l]
@echo go generate and deploy
@echo -c clean public/
@echo -l update local file and don't deploy
@echo -h help

:end

其他说明

从上文两个案例中可以看出, 批处理有时能帮助我们减轻很多不必要的工作量, 也能为我们提供一些有意思的功能, 这些功能可能用 “正式” 的编程语言实现起来非常复杂, 这个时候用批处理可以说是术业有专攻了.(当然 Python 也能做到很多类似的功能, 而且语法更加简单, 如果是更复杂的任务的话就可以寻求 Python 的帮助啦)

对于某些重复性的劳动, 我们应该想办法让计算机来帮我们完成, 打个比方, 如果你想复制某个目录中的 .jpg 文件和 .png 文件等等类型文件到一个新的目录, 手工操作会显得十分愚蠢, 尤其是当文件数量和种类比较多, 而又需要复制很多次时, 此时我们需要做的事情就是打开搜索引擎, 搜索相关的关键词, 最后写出一个下面这样的脚本:

1
2
3
@echo off
copy *.jpg C:\test\
copy *.png C:\test\

其中 test\ 文件夹就是目标文件夹, 这样一来, 花费 3 分钟, 省下以后的 30 分钟, 岂不美哉?