Shell 技巧 - 理解退出码

作者:Dave Taylor

上个月,我们探讨了信号,Linux 系统上的进程可以使用这种基本机制来传递事件和状态变化。我们讨论了如何使用 kill 命令手动向运行中的进程发送各种信号,以及 shell 脚本如何捕获并响应特定的信号(但并非所有信号都能捕获——有些信号实际上由操作系统本身处理)。

与信号类似,退出码也是进程向调用它的父进程传递状态的一种简单方式,但大多数 Linux 用户都忽略了这一点。现在不一样了——本月,我们将更仔细地研究一下。

mv 退出码

让我们从一个简单的 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

加载 Disqus 评论