理解和忽略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
一种构造是使用带有heredoc 的read:
$ read -d '' var << EOI
Line one.
Line two.
EOI
$ echo $?
1
错误代码 1?有什么错误?事实证明,当read没有遇到 delimiter 时,它会返回一个非零错误代码。在这种情况下,分隔符 ( -d ) 是一个空字符串 ( ” ),即NUL。但是,heredoc 不以NUL结尾,因此它返回非零(失败)。
7.2. 管道故障
另一个值得一提的问题是set的pipefail选项。启用后,管道的返回码是最后一个以非零状态退出的命令的值。这意味着管道上的任何故障都会导致管道链和命令立即终止:
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.
为了规避本节中的任何错误,我们可以利用我们在上一节中讨论的方法。