Shell 技巧 - 理解退出码
上个月,我们探讨了信号,Linux 系统上的进程可以使用这种基本机制来传递事件和状态变化。我们讨论了如何使用 kill 命令手动向运行中的进程发送各种信号,以及 shell 脚本如何捕获并响应特定的信号(但并非所有信号都能捕获——有些信号实际上由操作系统本身处理)。
与信号类似,退出码也是进程向调用它的父进程传递状态的一种简单方式,但大多数 Linux 用户都忽略了这一点。现在不一样了——本月,我们将更仔细地研究一下。
让我们从一个简单的 Linux 命令开始,可能每个人都已经掌握了:mv,它可以将文件或目录从文件系统中的一个位置移动到另一个位置(和/或重命名)。
如您所知,如果目标文件丢失、目标位置丢失等等,可能会产生错误。这是一个快速示例
$ mv missing ~/missing2 mv: cannot move `missing' to `.../missing2': No such file or directory
您看到了一个错误;显然,它没有工作。啊,但是在幕后,shell 中也设置了一个数字“返回码”变量,您可以在 shell 脚本中测试并响应它。请看以下序列
$ echo $? 0 $ mv missing ~/missing2 mv: cannot move `missing' to `.../missing2': No such file or directory $ echo $? 1 $ echo test me test me $ echo $? 0
如果在执行命令时没有错误发生,则退出码(我们在 shell 中用简写$?)的值将为 0:没有错误。现在,如果我运行一个失败的命令,退出码将有一个非零值。在上面失败的 mv 命令的情况下,错误码的值将为 1。并且,如果我现在运行另一个命令,一个没有错误运行的命令,错误码将被重置为零。
现在,让我们看一下 mv 命令的手册页,特别注意文档的后半部分。仔细检查后发现:“mv 实用程序在成功时退出码为 0,如果发生错误则退出码 >0。”
这真的没什么意思。实际上,grep 命令有更有趣的诊断信息:“通常,如果找到选定的行,则退出状态为 0,否则为 1。但是,如果发生错误,则退出状态为 2,除非使用 -q 或 --quiet 或 --silent 选项并且找到了选定的行。”
有一组已定义的系统退出码,尽管您可能永远不需要这些信息。以下是代码及其含义的列表
1:通用错误
2:错误使用 shell 内建命令(非常罕见)
126:无法调用请求的命令
127:命令未找到错误
128:“exit” 命令的无效参数
128+n:致命错误信号 “n”(例如,kill -9 = 137)。
130:脚本被 Ctrl-C 终止
在我开始深入研究退出码问题之前,我从未真正见过这个列表,所以您可以继续愉快地进行 shell 脚本编写,而无需担心上面的数据。
您分析和响应退出码最常见的情况是在脚本中的错误处理中。
这是一个简单的代码片段,您想在其中创建一个目录。如果失败,您想输出一条错误消息并退出
#!/bin/sh mkdir /usr echo \$? = $? if [ $? -ne 0 ] ; then echo "mkdir /usr failed: we have an exit code of $?" exit 1 fi echo "made the requested directory. Why is '/' world writable?" exit 0
事实证明,使用 $? 有一个细微之处,我已经暗示过了——这使得像第一个 “echo” 这样的输出语句非常成问题。您可以在输出中看到原因
$ ./test.sh mkdir: /usr: File exists $? = 1 made the requested directory. Why is '/' world writable?
您能看到发生了什么吗?在 mkdir 之后立即退出码 = 1,这很有道理,因为 /usr 已经存在,但是当我们再次在条件语句中测试退出码时,它不是一个非零值!
为什么?因为在那个时候,它指示的是 echo 语句的退出码,而不是 mkdir 命令的退出码。糟糕。
您可以通过注释掉第一个 echo 语句来简单地验证这一点,在这种情况下,您现在看到的是这样的命令输出
$ !. ./test.sh mkdir: /usr: File exists mkdir /usr failed: we have an exit code of 0
这更有道理,不是吗?
因为这可能会很棘手,所以我在具有大量错误处理的真正可靠的 shell 脚本中看到的一种常见做法是这样的
#!/bin/sh mkdir /usr error=$? if [ $error -ne 0 ] ; then echo "mkdir /usr failed: we have an exit code of $error" exit 1 fi
这是使用局部变量来保存系统或全局变量的一个很有意义的例子,它还允许您执行诸如在屏幕上显示错误消息并将其传递给像 syslog() 这样的东西,以确保管理员在某个时候看到它。
当然,错误处理并不总是只需要打印一条消息并退出。另一种情况可能是以下这样
alternates=' http://www.example.com/test.pdf http://www.example2.com/test.pdf http://www.example3.com/test.pdf ' gotit=0 for file in $alternates do wget $file if [ $? -ne 0 ]; then echo "Unable to get $file else gotit=1 break fi done ...
在这里,我们尝试从多个备用位置之一检索文件,并且仅在成功时(或当我们用尽所有可能性时)退出循环。
现在您已经知道如何捕获和分析系统命令的退出码,如果您希望错误消息来自您的脚本,而不是来自命令本身,该怎么办呢?
这可以通过另一种新的简写符号来完成>&,它重定向 stderr/错误输出流。以下是我如何使用它来隐藏示例脚本中使用的 mkdir 命令的所有错误消息
mkdir /usr >& /dev/null
您也可以使用&>或者2>&1而不是>&.
当然,如果您不测试命令的结果,您可能会严重搞砸事情,但这肯定会使输出更优雅
$ ./test.sh mkdir /usr failed: we have an exit code of 0
嗯...我仍然得到那个错误的 0。哦!我还没有添加代码将退出码值保存为 “error”。稍作调整后
$ ./test.sh mkdir /usr failed: we have an exit code of 1
这就对了!
我将本月的内容到此结束。下个月,我将演示 exit 命令如何让您从过程和函数向调用程序发送退出码,就像它们是单独的 Linux 命令而不是同一 shell 脚本的一部分一样。
Dave Taylor 从事 shell 脚本编程已经很长时间了,30 年。他是广受欢迎的《Wicked Cool Shell Scripts》的作者,您可以在 Twitter 上找到他 @DaveTaylor,或者访问他的网站 www.DaveTaylorOnline.com。