Work the Shell - 理解 Shell 脚本速记

作者:Dave Taylor

哦,快乐的一天!我收到了一位读者的电子邮件,其中包含一个 shell 脚本问题,这个问题看起来不像编程课的作业,也与破解密码无关。这位读者写道:

我正在阅读 /etc/init.d 目录中的脚本。我对这类脚本非常陌生,不明白它们是如何编写的。在每个脚本中,都有类似这样的语句:

[ -x /usr/sbin/halt ] || exit 0

这是什么意思?为什么这里使用 || ?

此外,在 halt 守护进程 init 脚本的“stop” case 中,有这样一句话:

[ $RETVAL -eq 0 ] && touch /var/lock/subsys/$sname

我不明白这些是做什么的。你能解释一下吗?

向我的老朋友 Larry Wall 道歉,这就是我所说的“Perl 综合症”(尽管如果我们真的要追溯历史,我在 Algol-68 和 PL/I 等语言中也看到了同样的问题,甚至在 Ada 中更糟)——由于程序员能够缩写他们的代码以使其更短,有时也更有效率,导致代码变得晦涩难懂。

查看文件系统可以解释其中一个结构。看看这个:

$ ls -l /bin/[

-r-xr-xr-x  2 root  wheel  46704 Sep 23 20:35 /bin/[*

$ ls -l /bin/test

-r-xr-xr-x  2 root  wheel  46704 Sep 23 20:35 /bin/test* 

这可能看起来很奇怪,但实际上在 Linux 的 /bin 目录中有一个名为 [ 的文件,它与 test 实用程序是同义的。您可以通过输入以下命令来了解它:man test在终端窗口中,但实际上它比这更复杂,因为现代 shell(如 Bash)为了性能原因,将 test 内置到 shell 代码本身中。因此,实际上有三个不同版本的 test。

如果您选择使用 [ 版本,则程序要求您有一个匹配的 ] 以保持语法简洁(e-hygiene?)。如果您省略它,您将收到-bash: [: missing `]'作为错误。

所以,第一个语句,[ -x /usr/sbin/halt ] || exit 0,最初可以展开为一个测试,快速浏览一下man test就会发现 -x 测试是用来检查指定的文件是否存在且可执行。基本上,此语句确保在执行 /usr/sbin/halt 脚本之前存在该脚本,以避免任何错误。这是一个可移植性测试。如果您缺少该脚本,您会遇到一些严重的问题,但是很多系统脚本都是这样编写的。

现在,我们来谈谈 || 符号。与它的伙伴 && 一起,这两个符号给深入研究脚本的人们带来了很多困惑,所以让我们首先阅读 Bash 手册页关于它们的说明(man bash):

command1 && command2 

command2 is executed if, and only if, command1 returns 
an exit status of zero. 

command1 || command2 

command2 is executed if and only if command1 returns 
a non-zero exit status.  

The return status of AND and OR lists is the exit 
status of the last command executed in the list. 

像泥一样浑浊,对吧?当我们回到 test 手册页并发现“test 实用程序以以下值之一退出:0 = 表达式评估为真,1 = 表达式评估为假或表达式缺失。”时,这将变得更加清晰。

所以,这里的逻辑是执行 [] 测试以查看脚本是否存在且可执行,如果测试失败,则执行exit 0。您如何知道它是否失败?test语句将返回退出值 1。

现在,让我们结合这一点来看第二个语句。您询问了这个语句

[ $RETVAL -eq 0 ] && touch /var/lock/subsys/$sname 

同样,[ 是 test 应用程序的速记符号。RETVAL 是一个系统变量,-eq 是用于相等性的数字测试。在这种情况下,返回值再次决定测试是真还是假。如果为真(零返回值),则 touch 命令用于设置所谓的信号量——一个锁文件,用于向其他脚本指示 $sname 子系统被锁定且不可修改。

这实际上是一种非常草率的设置信号量的方法,因为它不是原子性的。在第一次 RETVAL 测试和 touch 命令之间的间隙中,很可能脚本会被换出几毫秒并运行另一个脚本。这意味着两个脚本可能都认为它们已经锁定了该文件——这在计算机科学理论中被称为竞争条件,显然这不是一件好事。

无论如何,我不应该调试系统脚本。所以,只需说一下,该语句的目的是测试前一个命令的返回值(可能有一行像这样的语句RETVAL=$?在前一行,因为 $? 是前一个 shell 命令的返回值的速记)。如果测试为真,则临时文件被“touch”(即,它被创建并赋予当前日期和时间的创建时间戳)。

在脚本的后面,毫无疑问会有像这样的语句rm -f /var/lock/subsys/$sname,事实上,更简洁的写法是捕获退出条件,并确保即使脚本出错,锁文件也不会被留下。这是通过 trap shell 命令完成的。错误条件 0 是标准终止,所以一种干净的写法如下:

trap "/bin/rm -f /var/lock/subsys/$sname" 0 

这提供了很大的灵活性,因为您可以捕获数十个可能的信号,如 SIGINT(中断)或 SIGHUP(挂起)。

无论如何,您不是第一个被系统脚本搞糊涂的人,但正如您所看到的,一点坚持不懈就能揭示一切。

Dave Taylor 是一位拥有 26 年 UNIX 经验的资深人士,Elm 邮件系统的创建者,也是畅销书 Wicked Cool Shell ScriptsTeach Yourself Unix in 24 Hours 等 16 本技术书籍的作者。他的主要网站是 www.intuitive.com,他还提供技术支持,网址为 AskDaveTaylor.com

加载 Disqus 评论