A.2 操作系统脚本编程入门
读者如果不学习脚本的编写,就无法让命令行发挥最大威力。编写脚本和编写C语言程序有几分相似,但也有一些不同。下面先来看一个常见任务:不停地随机生成测试数据,分别运行两个程序并对比其结果。这个任务被形象地称为“对拍”。
A.2.1 Windows下的批处理
Windows下的批处理程序如下:
@echo off
:again
r > input ;生成随机输入
a < input > output.a
b < input > output.b
fc output.a output.b > nul ;比较文件
if not errorlevel 1 goto again ;相同时继续循环
第1行表明接下来的各个命令本身并不会回显。如果不理解,试着把这一行去掉就明白了。第2行是一个标号,后面的goto语句用得上。接下来调用数据生成器r,把输入数据写到文件input中,然后分别执行a和b,得到相应的输出,然后用命令fc比较它们。注意,fc命令有输出,但我们对此不感兴趣,因此重定向到一个名为nul的设备中,它就好比一个黑洞。另一个有意思的设备是con,代表标准输入输出。例如,命令copy con con的含义是直接把标准输入复制到标准输出(尽管有些傻)。试一试,建立一个只包含一条语句的“C程序”:#include<con>,用命令行编译一下试试——很不幸,看上去编译器“死掉”了,尽管它其实是在读键盘。如果在设计一个基于Windows的在线评测系统,小心好事者用它来愚弄你的系统!另一方面,千万不要在正式比赛中使用这个伎俩——它很可能让你失去比赛资格。
最后一行是整个批处理程序的关键——只有当比较文件相同时才执行goto,否则立刻终止程序。这样,就有机会好好研究一下这个input文件,看看两个程序的输出到底为什么不同。读者也许会问,这个if not errorlevel 1到底是什么意思呢?它是在测试上一个程序(在本例中,就是fc程序)的返回码。if errorlevel num的意思是“如果返回码大于或者等于num”,因此if not errorlevel 1的意思是,“如果返回码小于1”。事实上,当且仅当文件相同时,fc程序返回0。如果不确定程序的返回码是多少,可以在程序执行完毕后用echo %errorlevel%命令输出返回码。
你自己编写的程序的返回码是多少呢?这要看在main函数的最后return的是多少。返回码0往往代表“正常结束”,因此本书的正文部分才建议用return 0。典型的评分程序将在执行选手程序之后判断它的返回码,如果非0,则直接认为程序非正常退出,根本不去理会输出是否正确。说到这里,你也许已经想到一种故意让返回码非0的情况了——输出检查器。对于答案不唯一的情况(例如,走迷宫时要求输出最短路径,但不必是字典序最小的),对拍时不能简单地用fc命令比较文本内容,而应该单独编写一个程序,这个程序应当在答案不一致时返回1,以便上面的批处理程序及时终止。
上面的程序应以.bat为扩展名保存,并且在执行时也可以省略扩展名。如果同时存在abc.bat和abc.exe,将执行abc.exe。但如果主文件名和系统命令重名,则连exe文件也无法执行,如path.exe。
A.2.2 Linux下的Bash脚本
下面是上述程序的Linux版:
#!/bin/bash
while true; do
./r > input #生成随机数据
./a < input > output.a
./b < input > output.b
diff output.a output.b #文件比较
if [ $? -ne 0 ] ; then break; fi #判断返回值
done
和Windows版没有太大的不同,但需要注意的是,Linux中的设备名和Windows有所不同,而且也没有必要执行类似@echo off的命令——命令本来就不会回显。需要注意的是,如果在Windows下编写Linux脚本,复制到Linux后需要去掉所有的\r字符,否则解释器会报错。
把上述程序保存成test.sh后,再执行chmod +x test.sh,即可用./test.sh来执行它。当然,扩展名也不是必需的,完全可以以不带扩展名的test命名。
上面的程序不是最简洁的(例如,可以直接把diff命令放在if语句中),但展示了bash脚本的一些其他用法。例如,while循环是“while <命令集>; do <命令集>; done”,而if语句的基本是“if <命令集>; do <命令集>;”。不管是while还是if,判断的都是命令集中最后一条语句的返回码(exit code)是否为0。例如,若把上面的脚本改成if diff output.a output.b; then break; fi,则当两个文件相同(diff返回码为0)时退出循环(这个不是我们所期望的)。如果忘记了命令格式,可以用help if和help while获取帮助。
上面的“true”和“[”都是程序。前者的作用是直接返回0;而后者的作用是计算表达式(该程序要求最后一个参数必须是“]”),其中“$?”是bash内部变量,表示“上一个程序的返回码”。
A.2.3 再谈随机数
如果做过测试,可能会发现上面的方法有一个问题:如果程序执行太快,随机数生成器在相邻两次执行时,time(NULL)函数返回值相同,因而产生出完全相同的输入文件。换句话说,每隔一秒才能产生出一个不同的随机数据。一个解决方案是利用系统自带的随机数发生器:在Windows下是环境变量%random%,而在bash中是$RANDOM。它们都是0~32767之间的随机整数。可以直接用脚本编写随机数生成器,也可以把它们传递到程序中。