Contents

理解和忽略Bash中的错误

1. 简介

**错误是 Linux 管理和 Bash 脚本的正常组成部分。**它们表示特殊且可能重要的系统状态。尽管如此,我们可能希望跳过处理一些错误。

在本教程中,我们将讨论错误以及如何在 Bash 中忽略它们。特别是,我们定义了什么是错误并对其进行分类。接下来,我们看看它们在一般情况下和在 Bash 中是如何被识别的。之后,我们展示了一些忽略和抑制错误的方法。最后,一些评论展示了具体案例。

我们使用 GNU Bash 5.1.4 在 Debian 11 (Bullseye) 上测试了本教程中的代码。它是 POSIX 兼容的,应该可以在任何这样的环境中工作。

2. 错误

尽管我们经常遇到错误,但它们是一种异常状态。因此,错误的另一个名称是异常异常表示软件未达到其预期目标的情况

由于情况不同,我们可以粗略地按来源对错误进行排序。

2.1. 硬件错误

任何软件产品的最低级别是运行它的硬件。虽然电气和机械问题不在本文的讨论范围内,但它们通常具有软件表达式这一事实很重要。 考虑以下场景:

  • 出现故障的硬盘会产生读取或写入错误
  • 物理内存已损坏并导致应用程序由于多种原因而崩溃
  • 处理器过热,强制重启

重要的是,硬件错误通常是通过软件不可避免的。在某种程度上,这同样适用于以下错误类别。

2.2. 操作系统错误

由于操作系统 (OS) 是所有应用程序的基础,因此操作系统错误很容易影响它们。在更广泛的意义上,我们认为操作系统是内核 、设备驱动程序、API、用户界面和文件系统的组合。因此,这些组件中的任何错误都是操作系统错误。

事实上,操作系统本身就是软件产品。也就是说,与硬件一样,它们的错误很难被忽视。

例如,损坏的操作系统文件甚至会阻止应用程序运行。另一种情况可能涉及缺少或配置错误的驱动程序,这会导致设备访问期间出现错误。由于这种情况,应用程序可能会抛出错误,尽管最初并没有引起它们。

2.3. 应用程序错误

当然,即使操作系统正常运行,应用程序也可能滥用或错误配置它。

这通常是由于用户输入错误造成的。例如,尝试访问不存在的文件会导致应用程序异常。有时操作系统本身可能会损坏,从而阻止其他有效操作。

另一方面,我们错误地处理了用户输入。操作系统可能会阻止进程执行非法操作,而不是应用程序报告问题。想象一个用户试图删除其他人的文件的场景。文件系统权限将由操作系统强制执行,而不管应用程序中的任何检查。

此外,应用程序可能会故意行为不端,但也可能是意外环境的结果。也就是说,应用程序本身可能有错误或不完整的代码。

2.4. 编程错误

虽然到目前为止讨论的错误主要不在我们的控制范围内,但访问源代码可以改变这一点。特别是,我们可以在开发过程中实施检查和边界,以防止运行时出现问题。 当然,发展也有其自身的挑战

  • 语法错误,编程语言规则被破坏
  • 逻辑错误,例如无限循环、错误的布尔条件和不正确的流
  • 运行时错误,例如内存分配错误

这些错误的可预防性、可避免性和可忽略性取决于它们的具体情况以及编程语言。

要处理错误,我们必须首先知道它确实发生了。如何?那要看产地。

3. 错误代码

问题诊断并不简单。操作系统错误可以在启动期间自行报告。难闻的气味甚至可能表明硬件错误。 另一方面,应用程序具有状态或退出代码退出代码允许应用程序的用户知道其最终状态。重要的是,被广泛接受的无错误状态编号是0。 尽管错误通常伴随着文本说明,但情况并非总是如此。以下是一些常见的 POSIX 错误代码及其描述:

  • EBADF=9,错误的文件描述符
  • EFAULT=14,错误地址
  • EIO=5,I/O 错误

应用程序可以省略描述,只返回错误(代码)

本质上,开发人员可以考虑和计划不同的条件和情况。但是,它们很少能涵盖所有可能的情况。重要的是,操作系统可以终止恶意进程,这也会导致退出代码。

Bash 中的错误也可能导致退出代码,我们将在下面看到。

4. Bash 错误

在运行之前,必须解释 Bash 脚本行。因此,每个命令构造都有一个返回码。 基于此返回码和其他条件 ,Bash 确定给定的命令构造是否失败。 这是cd (更改目录)的示例:

$ cd DoesNotExist
-bash: cd: DoesNotExist: No such file or directory
$ echo $?
1

如果命令以非零状态退出,则它失败。这通常没有直接后果,但如果没有发现错误,则可能是有害的。为了避免隐藏的问题,我们使用带有-e标志的set ,失败的命令构造会导致立即退出,并带有非零代码**。由于在生产环境中通常建议使用set -e,因此突然退出似乎是不可避免的。但是,有一些方法可以避免它们,我们将在下面讨论。

5. Bash 错误处理

我们可以将操作系统和硬件想象成一个骨架,其中包含应用程序。如果骨架受到破坏,这些应用程序可能无法正常工作。他们没有必要的工具来纠正这种情况。应用程序只能检查问题。因此,我们不会处理这些情况,而只会处理它们的后果。

作为开发人员,我们主要对我们可以处理的问题感兴趣。我们处理我们调用的软件的执行状态代码,同时控制我们自己的退出代码和编程错误。以下面的脚本为例:

read input
if [[ "$input" == 'hello' ]]; then
exit 0
fi
exit 1

我们使用read 来获取用户的输入。之后,将输入与预定义的字符串进行比较。只要脚本运行它,我们就会返回我们自己的状态码。但是,如果脚本被过早地杀死,这个事实将在 POSIX 下由状态137指示。

如果我们的脚本被称为greet。sh,这是在管道 输入“hello”输入后如何检查退出代码的方法:

$ echo hello | bash greet.sh
$ echo $?
0

在 Bash 中,$? 变量存储最后一条命令的状态码。事实上,这对于我们脚本中的任何应用程序调用都是有效的。 **由于零表示成功作为 Bash 中的退出代码,我们可以使用逻辑运算符 **链接调用:

$ echo bye | bash greet.sh && echo Success. || echo Failure.
Failure.

在将“bye”到我们的脚本之后,它返回一个非零(失败)错误代码。这意味着*&&结构被跳过,而||* (或)被触发。

6. 忽略 Bash 中的错误

最重要的是,整行的退出代码为零(成功),无论其中的路径如何:

$ echo bye | bash greet.sh && echo Success. || echo Failure.
Failure.
$ echo $?
0

实际上,这在set -e起作用时会派上用场,因为它可以防止特定命令的脚本突然终止。请注意,该方法仅适用于*&&||之后的命令。返回成功。我们使用echo* ,但静默成功的另一种选择是: (null 实用程序)。

另一种实现相同目的的方法是使用感叹号语法:

set -e
! echo bye | bash greet.sh
echo Success.

尽管在set -e环境中出现错误,但执行上面的脚本不会强制退出。 另一个选项是管道到任何成功执行的命令:

set -e
echo bye | bash greet.sh | echo $?
echo Success.

虽然echo $? 显示 1,我们仍然成功,因为它返回0

最后,我们还可以在运行命令之前关闭*-e*设置,然后重新启用它:

set -e
# code here
set +e
echo bye | bash greet.sh
set -e
echo Success.

此外,我们还可以抑制失败命令可能打印的任何stdout 和 stderr 错误消息:

$ badcommand
-bash: badcommand: command not found
$ badcommand >/dev/null 2>&1

使用退出代码处理和忽略输出的组合,我们可以实现一个静默失败的命令。

所有讨论的方法都可以通过用大括号括起来应用于整个代码块

set -e
! {
echo First command.
badcommand
echo Third command.
}
echo Success.

接下来,我们将看到一些例外情况。

7. 备注

在大多数情况下,我们可以很确定地预测何时可能出现错误。但是,某些命令具有内置的固有异常。

7.1. heredoc

一种构造是使用带有heredocread

$ read -d '' var << EOI
Line one.
Line two.
EOI
$ echo $?
1

错误代码 1?有什么错误?事实证明,当read没有遇到 delimiter 时,它会返回一个非零错误代码。在这种情况下,分隔符 ( -d ) 是一个空字符串 ( ),即NUL。但是,heredoc 不以NUL结尾,因此它返回非零(失败)。

7.2. 管道故障

另一个值得一提的问题是setpipefail选项。启用后,管道的返回码是最后一个以非零状态退出的命令的值。这意味着管道上的任何故障都会导致管道链和命令立即终止

set -e
badcommand | echo
# script does not exit
set -o pipefail
badcommand | echo
# script exits

最初,将非零退出代码传递给成功的命令会导致整体成功。设置pipefail后,同一行会生成错误并强制脚本退出。

7.3. 未定义的变量

另一个设置标志是*-u*。一旦我们设置 -u,它会强制对未定义变量的任何引用导致错误**:

set -e
set -u
echo $undefined
# script exits

除了通常的方法外,还有一个特殊的结构可以让我们绕过这个错误:${undefined:-SOME_VALUE}。当我们像这样取消引用变量时,它要么返回变量值,要么如果未定义,则返回SOME_VALUE。因此,我们不使用未定义的值,因此不会使用set -u生成错误。

7.4. 编译和解释

在编程语法错误方面,如果在处理源代码文件时遇到错误,编译器和解释器会自行返回错误代码:

$ touch empty.c
$ gcc empty.c
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/10/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x20): undefined reference to `main'
collect2: error: ld returned 1 exit status
$ echo $?
1
$ echo [ > bad.sh
$ bash bad.sh
empty: line 1: [: missing `]'
$ echo $?
2

7.5. 陷阱

虽然不是专门为忽略错误而设计的,但我们可以使用trap 来避免突然退出。在某种程度上,陷阱可以防止或添加到错误的默认处理中。没有太多陷阱,这里有一个简单的示例脚本:

set -e
trap 'echo "Inside trap"; echo "Line $LINENO."' ERR
echo 'Before bad command.'
badcommand
echo 'Unreachable.'

正如该脚本的输出所示,我们可以在脚本退出或出错之前执行多个命令:

$ bash script.sh
Before bad command.
./script.sh: line 5: badcommand: command not found
Inside trap.
Line 5.

为了规避本节中的任何错误,我们可以利用我们在上一节中讨论的方法