Skip to content

Latest commit

 

History

History
939 lines (714 loc) · 53.7 KB

File metadata and controls

939 lines (714 loc) · 53.7 KB

九、错误检查和处理

在本章中,我们将描述如何检查错误并优雅地处理它们。我们将首先解释退出状态的概念,然后用test命令进行一些功能检查。之后,我们将开始使用test命令的速记符号。本章的下一部分专门讨论错误处理:我们将使用if-then-exitif-then-else来处理简单的错误。在本章的最后一部分,我们将首先介绍一些防止错误发生的方法,因为预防胜于补救。

本章将介绍以下命令:mktemptruefalse

本章将涵盖以下主题:

  • 错误检查
  • 错误处理
  • 错误预防

技术要求

本章只需要 Ubuntu 虚拟机。如果你从来没有更新过你的机器,现在可能是个好时机!sudo apt update && sudo apt upgrade -y命令完全升级您的机器和所有工具。如果您选择这样做,请确保您重新启动机器,以便加载升级的内核。在 Ubuntu 上,如果 /var/log/reboot-required文件存在,你可以确定需要重启*。*

*本章所有脚本均可在 GitHub:https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter09 上找到。

错误检查

在前一章中,我们花了一些时间解释如何在脚本中捕获和使用用户输入。虽然这使得我们的脚本更加动态,并且,通过扩展,更加实用,但是我们也引入了一个新概念:**人为错误。**假设您正在编写一个脚本,想要向用户提出是/否问题。您可能期望一个合理的用户使用以下任何一项作为答案:

  • y
  • n
  • Y
  • 普通

虽然 Bash 允许我们检查我们能想到的所有值,但有时用户仍然能够通过提供您意想不到的输入来破坏脚本。例如,用户用他们的母语回答是/否问题:jasinei,或无数其他可能性中的任何一种。在实践中,你会发现你可以永远不会想到用户会提供的每一个可能的输入。既然如此,最好的解决方案是处理最常见的预期输入,并使用通用错误消息捕获所有其他输入,该消息告诉用户如何正确提供答案。我们将在本章后面看到如何做到这一点,但首先,我们将从查看如何通过检查命令的退出状态来确定是否发生了错误开始。

退出状态

退出状态,通常也称为退出代码返回代码,是 Bash 向其父进程传达进程成功或失败终止的方式。在 Bash 中,所有进程都是从调用它们的 Shell 中分叉出来的*。下图说明了这一点:*

*

当一个命令运行时,比如上图中的ps -f,复制当前 shell(包括环境变量!),该命令在副本中运行,称为分叉。命令/进程完成后,它终止分叉,并将退出状态返回到最初分叉的 shell(在交互会话的情况下,这将是您的用户会话)。此时,您可以通过查看退出代码来确定流程是否成功执行。如前一章所述,退出代码 0 被视为正常,而所有其他代码应被视为不正常。因为分叉被终止了,我们需要返回代码,否则我们将无法将状态反馈给我们的会话!

因为我们在上一章已经看到了如何在交互会话中抓取退出状态(提示:我们看了$?变量的内容!),让我们看看如何在脚本中做到这一点:

reader@ubuntu:~/scripts/chapter_09$ vim return-code.sh
reader@ubuntu:~/scripts/chapter_09$ cat return-code.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-29
# Description: Teaches us how to grab a return code.
# Usage: ./return-code.sh
#####################################

# Run a command that should always work:
mktemp
mktemp_rc=$?

# Run a command that should always fail:
mkdir /home/
mkdir_rc=$?

echo "mktemp returned ${mktemp_rc}, while mkdir returned ${mkdir_rc}!"

reader@ubuntu:~/scripts/chapter_09$ bash return-code.sh 
/tmp/tmp.DbxKK1s4aV
mkdir: cannot create directory ‘/home’: File exists
mktemp returned 0, while mkdir returned 1!

看完剧本,我们从标题开始。因为我们在这个脚本中不使用用户输入,所以用法只是脚本名称。我们运行的第一个命令是mktemp。这个命令用来创建一个临时的文件,它有一个随机的名字,如果我们需要在磁盘上有一个位置来存放一些临时数据的话,它可能会很有用。或者,如果我们将-d标志提供给mktemp,我们将创建一个临时的目录,并随机命名。因为随机名称足够长,并且我们应该始终在/tmp/中拥有写权限,所以我们期望mktemp命令几乎总是成功,从而返回退出状态 0。我们通过在命令之后直接运行变量赋值将返回代码保存到mktemp_rc变量中。这就是返回代码的最大弱点:我们只有在命令完成后才能直接使用它们。如果我们在之后做任何其他事情,返回代码将被设置为该操作,覆盖以前的退出状态!

接下来,我们运行一个我们认为总是会失败的命令:mkdir /home/。我们预计失败的原因是因为在我们的系统上(以及几乎每个 Linux 系统上),已经存在/home/目录。在这种情况下,无法再次创建它,这就是命令失败且退出状态为 1 的原因。再次,直接在mkdir命令后,我们将退出状态保存到mkdir_rc变量中。

最后,我们需要检查一下我们的假设是否正确。使用echo,我们打印两个变量的值以及一些文本,这样我们就知道在哪里打印了哪个值。最后要注意的是:我们在包含变量的句子中使用了双引号。如果我们使用单引号,变量就不会被扩展(用变量名替换变量名的 Bash 术语)。或者,我们可以完全省略引号,echo也可以按照期望运行,但是当我们开始使用重定向时,这可能会出现问题,这就是为什么我们认为在处理包含变量的字符串时总是使用双引号是一种好的形式。

功能检查

现在,我们知道如何检查进程的退出状态,以确定它是否成功。然而,这不是我们验证命令成功/失败的唯一方法。对于我们运行的大多数命令,我们还可以执行功能检查,看看我们是否成功。在之前的脚本中,我们尝试创建/home/目录。但是如果我们更关心的是/home/目录的存在,而不是进程的退出状态呢?

以下脚本显示了我们如何对系统状态执行功能检查:

reader@ubuntu:~/scripts/chapter_09$ vim functional-check.sh
reader@ubuntu:~/scripts/chapter_09$ cat functional-check.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-29
# Description: Introduces functional checks.
# Usage: ./functional-check.sh
#####################################

# Create a directory.
mkdir /tmp/temp_dir
mkdir_rc=$?

# Use test to check if the directory was created.
test -d /tmp/temp_dir
test_rc=$?

# Check out the return codes:
echo "mkdir resulted in ${mkdir_rc}, test resulted in ${test_rc}."

reader@ubuntu:~/scripts/chapter_09$ bash functional-check.sh 
mkdir resulted in 0, test resulted in 0.
reader@ubuntu:~/scripts/chapter_09$ bash functional-check.sh 
mkdir: cannot create directory ‘/tmp/temp_dir’: File exists
mkdir resulted in 1, test resulted in 0.

我们从通常的管道开始前面的脚本。接下来,我们要用mkdir创建一个目录。我们获取退出状态并将其存储在变量中。接下来,我们使用test命令(我们在上一章中简要讨论过)来验证/tmp/temp_dir/是否是一个目录(因此,如果它是在某个时候创建的)。然后我们用echo打印返回代码,与我们打印返回代码的方式相同。

接下来,我们运行脚本两次。这里发生了一些有趣的事情。我们第一次运行脚本时,/tmp/temp_dir/目录不存在于文件系统中,而是被创建的。因此,mkdir命令的退出代码为 0。自从成功创建后,test -d也成功了,并如预期的那样给了我们一个退出状态 0。

现在,在脚本的第二次运行中,mkdir命令没有成功完成。这是意料之中的,因为脚本的第一次运行已经创建了目录。由于我们没有在两次运行之间删除它,第二次运行mkdir不成功。然而,test -d仍然运行良好:目录存在,尽管它不是在脚本运行中创建的。

When creating scripts, make sure you think long and hard about how you want to check for errors. Sometimes, return codes will be what you need: this is the case when you need to be sure that the command has been run successfully. Other times, however, a functional check might be a better fit. This is often the case when it is the end result that matters (for example, a directory must exist), but it does not matter so much what caused the desired state.

测试速记

test命令是我们 shell 脚本库中最重要的命令之一。因为 shell 脚本通常是脆弱的,尤其是在涉及到用户输入的地方,所以我们希望尽可能地使它们健壮。虽然解释test命令的每个方面需要一整章,但是test可以做以下事情:

  • 检查文件是否存在
  • 检查目录是否存在
  • 检查变量是否不为空
  • 检查两个变量是否具有相同的值
  • 检查文件 1 是否比文件 2 旧
  • 检查 INTEGER1 是否大于 INTEGER2

诸如此类——这至少应该给你一个印象,你可以用test来检查。在进一步阅读部分,我们包含了大量关于测试的资料。确保给它一个外观,因为它肯定会有助于你的 shell 脚本冒险!

对于大多数脚本和编程语言来说,没有test命令这种东西。显然,测试在这些语言中同样重要,但是与 Bash 不同,测试通常直接与if-then-else逻辑集成(我们将在本章的下一部分讨论)。对我们来说幸运的是,Bash 有一个test命令的简写,这使得它更接近其他语言的语法:[[[

查看下面的代码,更好地了解如何用这种简写方式替换test命令:

reader@ubuntu:~/scripts/chapter_09$ vim test-shorthand.sh
reader@ubuntu:~/scripts/chapter_09$ cat test-shorthand.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-29
# Description: Write faster tests with the shorthand!
# Usage: ./test-shorthand.sh
#####################################

# Test if the /tmp/ directory exists using the full command:
test -d /tmp/
test_rc=$?

# Test if the /tmp/ directory exists using the simple shorthand:
[ -d /tmp/ ]
simple_rc=$?

# Test if the /tmp/ directory exists using the extended shorthand:
[[ -d /tmp/ ]]
extended_rc=$?

# Print the results.
echo "The return codes are: ${test_rc}, ${simple_rc}, ${extended_rc}."

reader@ubuntu:~/scripts/chapter_09$ bash test-shorthand.sh 
The return codes are: 0, 0, 0.

如您所见,在管道化之后,我们从之前介绍的test语法开始。接下来,我们用[代替了单词 test,用]结束了这一行。这是 Bash 与其他脚本/编程语言的共同点。请注意,与大多数语言不同的是,Bash 需要在之后和之前有一个空格!最后,我们使用了扩展的速记语法,以[[开始,以]]结束。当我们打印返回代码时,它们都返回0,这意味着所有测试都成功了,即使语法不同。

The difference between [ ] and [[ ]] is minor, but can be very important. Simply said, the simple shorthand syntax of [ ] can introduce problems when variables or paths have whitespace in them. In this case, the test considers the whitespace the delimiter, which means the string hello there becomes two arguments instead of one (hello + there). There are other differences, but in the end our advice is really simple: use the extended shorthand syntax of [[ ]]. For more information, see the Further reading section on test.

可变复习

作为一点小奖励,我们对test-shorthand.sh脚本有一点小改进。在前一章中,我们解释了,如果我们必须在一个脚本中多次使用同一个值,我们最好将其作为一个变量。如果变量的值在脚本执行过程中没有变化,并且不受用户输入的影响,我们使用一个常量。看看我们将如何在之前的脚本中融入这一点:

reader@ubuntu:~/scripts/chapter_09$ cp test-shorthand.sh test-shorthand-variable.sh
reader@ubuntu:~/scripts/chapter_09$ vim test-shorthand-variable.sh 
reader@ubuntu:~/scripts/chapter_09$ cat test-shorthand-variable.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-29
# Description: Write faster tests with the shorthand, now even better 
# with a CONSTANT!
# Usage: ./test-shorthand-variable.sh
#####################################

DIRECTORY=/tmp/

# Test if the /tmp/ directory exists using the full command:
test -d ${DIRECTORY}
test_rc=$?

# Test if the /tmp/ directory exists using the simple shorthand:
[ -d ${DIRECTORY} ]
simple_rc=$?

# Test if the /tmp/ directory exists using the extended shorthand:
[[ -d ${DIRECTORY} ]]
extended_rc=$?

# Print the results.
echo "The return codes are: ${test_rc}, ${simple_rc}, ${extended_rc}."

reader@ubuntu:~/scripts/chapter_09$ bash test-shorthand-variable.sh 
The return codes are: 0, 0, 0.

虽然最终结果是一样的,但是如果我们想要改变它,这个脚本会更加健壮。此外,它向我们展示了我们可以在test速记中使用变量,这将由 Bash 自动扩展。

Bash 调试

我们还有一个锦囊妙计来证明值被适当地扩展了:运行带有调试日志记录的 Bash 脚本**。请看下面的执行:**

reader@ubuntu:~/scripts/chapter_09$ bash -x test-shorthand-variable.sh 
+ DIRECTORY=/tmp/
+ test -d /tmp/
+ test_rc=0
+ '[' -d /tmp/ ']'
+ simple_rc=0
+ [[ -d /tmp/ ]]
+ extended_rc=0
+ echo 'The return codes are: 0, 0, 0.'
The return codes are: 0, 0, 0.

如果将此与实际脚本进行比较,您将看到脚本文本test -d ${DIRECTORY}在运行时解析为test -d /tmp/。这是因为,我们不是在跑bash test-shorthand-variable.sh,而是在跑bash -x test-shorthand-variable.sh。在这种情况下,-x标志告诉 Bash 在执行命令时打印命令及其参数— 如果您正在构建脚本,并且不确定脚本为什么没有按照您期望的那样运行,这是一件非常容易记住的事情!

错误处理

到目前为止,我们已经研究了如何检查错误。然而,除了检查错误之外,还有一个同样重要的方面:处理错误。在我们继续介绍处理错误的更聪明的方法之前,我们将首先结合我们以前的经验和iftest退出错误!

如果-那么-退出

正如您可能从上一章中回忆的那样,Bash 使用的if-then构造对(几乎)所有编程语言都是通用的。在它的基本形式中,想法是你测试一个条件(如果),如果那个条件是真的,你做一些事情(然后)。

这里有一个非常基本的例子:如果name长于或等于 2 个字符,那么echo "hello ${name}"。在这种情况下,我们假设一个名字至少要有 2 个字符。如果不是,输入无效,我们不会给它一个“你好”。

在下面的脚本if-then-exit.sh中,我们将看到我们的目标是使用cat打印一个文件的内容。但是,在此之前,我们会检查文件是否存在,如果不存在,我们会退出脚本,并向调用者发送一条消息,指明发生了什么错误:

reader@ubuntu:~/scripts/chapter_09$ vim if-then-exit.sh 
reader@ubuntu:~/scripts/chapter_09$ cat if-then-exit.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-30
# Description: Use the if-then-exit construct.
# Usage: ./if-then-exit.sh
#####################################

FILE=/tmp/random_file.txt

# Check if the file exists.
if [[ ! -f ${FILE} ]]; then 
  echo "File does not exist, stopping the script!"
  exit 1
fi

# Print the file content.
cat ${FILE}

reader@ubuntu:~/scripts/chapter_09$ bash -x if-then-exit.sh
+ FILE=/tmp/random_file.txt
+ [[ ! -f /tmp/random_file.txt ]]
+ echo 'File does not exist, stopping the script!'
File does not exist, stopping the script!
+ exit 1

这个剧本的大部分现在应该都清楚了。我们在测试中使用了扩展速记语法,我们将在本书的其余部分中进行测试。-f标志在test的手册页中描述为文件存在,并且是常规文件。但是,我们在这里遇到了一个小问题:我们想打印文件(用cat,但前提是文件存在;否则,我们要用echo打印消息。在本章的后面,当我们介绍if-then-else时,我们将看到如何通过阳性检测来做到这一点。不过目前,如果我们正在检查的文件不是现有文件,我们希望测试给出一个真值。在这种情况下,从语义上讲,我们正在做以下事情:如果文件不存在,那么打印一条消息并退出。Bash 中的测试语法没有这个标志。幸运的是,我们可以使用一个强大的构造:感叹号!,否定/颠倒了测试!

这方面的一些例子如下:

  • if[[-f/tmp/file]];如果文件/tmp/file 存在,则执行做某事->-做某事
  • if [!-f/tmp/file]];如果文件/tmp/文件不存在,则执行做某事 - > 做某事
  • if[[-n $ { variable }];如果变量${variable}不为空,则执行做某事->-做某事
  • if [!-n $ { variable }]];然后做某事 - > 做某事在变量${variable}为而非不为空的情况下执行(因此,双负数表示只有变量实际为空时才执行做某事)
  • if[[-z $ { variable }];如果变量${variable}为空,则执行做某事->-做某事
  • if [!-z $ { variable }]];如果变量${variable}为而非为空,则执行做某事 - > 做某事

你应该知道,最后四个例子是重叠的。这是因为旗帜-n(非零)和-z(零)已经是彼此的对立面。既然我们可以用否定测试!,这意味着-z等于! -n! -z等于-n。在这种情况下,你用-n还是没关系!-z。我们建议您使用特定的标志,如果它是可用的,在使用另一个标志的否定之前。

让我们回到我们的剧本。当我们通过使用否定的文件存在测试发现文件不存在时,我们向调用者打印出有用的消息并退出脚本。在这种情况下,我们从未到达cat命令,但由于文件无论如何都不存在,cat永远不会成功。如果我们继续执行死刑,我们将会看到cat的错误信息。在cat的情况下,这个消息并不比我们自己的消息差,但是对于其他一些命令来说,错误消息肯定不总是像我们希望的那样清晰;在这种情况下,用明确的信息检查我们自己并不是一件坏事!

这里还有一个例子,我们使用 if 和 test 来查看我们将在变量中捕获的状态代码:

reader@ubuntu:~/scripts/chapter_09$ vim if-then-exit-rc.sh
reader@ubuntu:~/scripts/chapter_09$ cat if-then-exit-rc.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-30
# Description: Use return codes to stop script flow.
# Usage: ./if-then-exit-rc.sh
#####################################

# Create a new top-level directory.
mkdir /temporary_dir
mkdir_rc=$?

# Test if the directory was created successfully.
if [[ ${mkdir_rc} -ne 0 ]]; then
  echo "mkdir did not successfully complete, stop script execution!"
  exit 1
fi

# Create a new file in our temporary directory.
touch /temporary_dir/tempfile.txt

reader@ubuntu:~/scripts/chapter_09$ bash if-then-exit-rc.sh
mkdir: cannot create directory ‘/temporary_dir’: Permission denied
mkdir did not successfully complete, stop script execution!

在这个脚本的第一个功能部分,我们试图创建顶层目录/temporary_dir/。由于只有 root 用户拥有这些权限,并且我们既没有以 root 用户的身份也没有以sudo的身份运行这些权限,因此mkdir失败了。当我们在mkdir_rc变量中捕捉到退出状态时,我们不知道确切的值(如果我们想要的话,我们可以打印它),但是我们可以确定一件事:它不是0,它是为成功执行而保留的。所以,我们这里有两个选项:我们可以检查退出状态是否不等于 0 ,或者状态代码是否等于 1 (这实际上是mkdir在这种情况下向母壳报告的内容)。我们通常更喜欢检查没有成功,而不是检查特定类型的失败(由不同的返回代码表示,如 1、113、127、255 等)。如果我们只停留在退出代码 1 上,我们将在所有没有得到 1 的情况下继续脚本:这有希望是 0,但是我们不确定。而且,一般来说,任何不成功的事情都应该停止脚本!

对于这种情况,检查返回代码是否不是0,我们使用一个整数(记住,一个花哨的单词表示数字)进行比较。如果我们查看man test,我们可以看到-ne旗被描述为INTEGER1 -ne INTEGER2: INTEGER1 is not equal to INTEGER2。因此,对于我们的逻辑,这意味着,如果变量中捕获的返回代码是notequal to0,则命令没有成功,我们应该停止。请记住,我们也可以使用-eq ( eq ual to)标志,并用!否定它,以获得相同的效果。

按照目前的形式,脚本比严格要求的要长一点。我们首先将返回代码存储在一个变量中,然后比较该变量。我们还可以直接使用if-test构造中的退出状态,比如:

reader@ubuntu:~/scripts/chapter_09$ cp if-then-exit-rc.sh if-then-exit-rc-improved.sh
reader@ubuntu:~/scripts/chapter_09$ vim if-then-exit-rc-improved.sh
reader@ubuntu:~/scripts/chapter_09$ cat if-then-exit-rc-improved.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-30
# Description: Use return codes to stop script flow.
# Usage: ./if-then-exit-rc-improved.sh
#####################################

# Create a new top-level directory.
mkdir /temporary_dir

# Test if the directory was created successfully.
if [[ $? -ne 0 ]]; then
  echo "mkdir did not successfully complete, stop script execution!"
  exit 1
fi

# Create a new file in our temporary directory.
touch /temporary_dir/tempfile.txt

reader@ubuntu:~/scripts/chapter_09$ bash if-then-exit-rc-improved.sh 
mkdir: cannot create directory ‘/temporary_dir’: Permission denied
mkdir did not successfully complete, stop script execution!

虽然这个只为我们节省了一行(变量赋值),但它也为我们节省了一个不必要的变量。您可以看到,我们将测试更改为将 0 与$进行比较?。我们知道无论如何都要检查执行情况,所以我们不妨马上就做。如果我们以后需要这样做,我们仍然需要将它保存在一个变量中,因为请记住:退出状态只有在运行命令后才直接可用。此后,它被后面命令的退出状态覆盖。

如果-那么-否则

到现在,你有希望感受到if-then逻辑有多有用。然而,你可能会觉得仍然缺少一些东西。如果是这样,你就对了!没有 ELSE 语句,一个if-then构造是不完整的。if-then-else结构允许我们指定如果 if 子句中的测试等于真,会发生什么。从语义上讲,它可以翻译为:

IF condition, THEN do-something, ELSE (otherwise) do-something-else

我们可以很容易地说明这一点,方法是使用我们早期的脚本之一if-then-exit.sh,并优化脚本和代码的流程:

reader@ubuntu:~/scripts/chapter_09$ cp if-then-exit.sh if-then-else.sh
reader@ubuntu:~/scripts/chapter_09$ vim if-then-else.sh 
reader@ubuntu:~/scripts/chapter_09$ cat if-then-else.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-30
# Description: Use the if-then-else construct.
# Usage: ./if-then-else.sh
#####################################

FILE=/tmp/random_file.txt

# Check if the file exists.
if [[ ! -f ${FILE} ]]; then 
  echo "File does not exist, stopping the script!"
  exit 1
else
  cat ${FILE} # Print the file content.
fi

reader@ubuntu:~/scripts/chapter_09$ bash if-then-else.sh 
File does not exist, stopping the script!
reader@ubuntu:~/scripts/chapter_09$ touch /tmp/random_file.txt
reader@ubuntu:~/scripts/chapter_09$ bash -x if-then-else.sh 
+ FILE=/tmp/random_file.txt
+ [[ ! -f /tmp/random_file.txt ]]
+ cat /tmp/random_file.txt

现在,这开始看起来像什么了!我们将cat命令移入if-then-else逻辑块。现在,感觉(而且是!)就像一个命令:如果文件不存在,打印一条错误消息并退出,否则打印其内容。不过,我们使用 then 块来处理错误情况有点奇怪;按照惯例,这是留给成功的条件。我们可以通过交换 then 和 else 块使我们的脚本更加直观;然而,我们还需要反转我们的测试条件。让我们来看看:

reader@ubuntu:~/scripts/chapter_09$ cp if-then-else.sh if-then-else-proper.sh
reader@ubuntu:~/scripts/chapter_09$ vim if-then-else-proper.sh 
reader@ubuntu:~/scripts/chapter_09$ cat if-then-else-proper.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-30
# Description: Use the if-then-else construct, now properly.
# Usage: ./if-then-else-proper.sh file-name
#####################################

file_name=$1

# Check if the file exists.
if [[ -f ${file_name} ]]; then 
  cat ${file_name} # Print the file content.
else
  echo "File does not exist, stopping the script!"
  exit 1
fi

reader@ubuntu:~/scripts/chapter_09$ bash -x if-then-else-proper.sh /home/reader/textfile.txt 
+ FILE=/home/reader/textfile.txt
+ [[ -f /home/reader/textfile.txt ]]
+ cat /home/reader/textfile.txt
Hi, this is some text.

我们在此脚本中所做的更改如下:

  • 我们用用户输入变量file_name替换了硬编码的文件常数
  • 我们移除了!这颠倒了test
  • 我们交换了“然后”和“否则”执行块

现在,脚本首先检查文件是否存在,如果存在,它将打印其内容(成功场景)。如果文件不存在,脚本将打印一条错误消息,并以退出代码 1 退出(失败场景)。在实践中,else往往是为失败场景预留的,then是为成功场景预留的。然而,这些不是黄金规则,可能会有所不同,这取决于您可用的测试类型。如果你曾经写过一个脚本,并且你想使用 else 块来实现成功的场景,那就去做吧:只要你确定这是适合你的情况的正确选择,这绝对没有什么丢人的!

You might have noticed that within an if-then-else block, the commands we execute in then or else are always preceded by two whitespaces. In scripting/programming, this is called indenting. It serves only a single function in Bash: to improve readability. By indenting those commands with two spaces, we know they're part of the then-else logic. In that same manner, it is much easier to see where the then ends and the else begins. Note that, in some languages, notably Python, whitespace is part of the programming language syntax and cannot be omitted!

在此之前,我们只使用if-then-else逻辑进行错误检测,然后是退出1。然而,在某些情况下,然后否则都可以用来完成脚本的目标,而不是其中一个用于错误处理。看看下面的脚本:

reader@ubuntu:~/scripts/chapter_09$ vim empty-file.sh 
reader@ubuntu:~/scripts/chapter_09$ cat empty-file.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-02
# Description: Make sure the file given as an argument is empty.
# Usage: ./empty-file.sh <file-name>
#####################################

# Grab the first argument.
file_name=$1

# If the file exists, overwrite it with the always empty file 
# /dev/null; otherwise, touch it.
if [[ -f ${file_name} ]]; then
  cp /dev/null ${file_name}
else
  touch ${file_name}
fi

# Check if either the cp or touch worked correctly.
if [[ $? -ne 0 ]]; then
  echo "Something went wrong, please check ${file_name}!"
  exit 1
else
  echo "Succes, file ${file_name} is now empty."
fi

reader@ubuntu:~/scripts/chapter_09$ bash -x empty-file.sh /tmp/emptyfile
+ file_name=/tmp/emptyfile
+ [[ -f /tmp/emptyfile ]]
+ touch /tmp/emptyfile
+ [[ 0 -ne 0 ]]
+ echo 'Succes, file /tmp/emptyfile is now empty.'
Succes, file /tmp/emptyfile is now empty.
reader@ubuntu:~/scripts/chapter_09$ bash -x empty-file.sh /tmp/emptyfile
+ file_name=/tmp/emptyfile
+ [[ -f /tmp/emptyfile ]]
+ cp /dev/null /tmp/emptyfile
+ [[ 0 -ne 0 ]]
+ echo 'Succes, file /tmp/emptyfile is now empty.'
Succes, file /tmp/emptyfile is now empty.

我们使用这个脚本来确保文件存在并且是空的。基本上有两种情况:文件存在(并且可能不是空的)或者不存在。在我们的 if 测试中,我们检查文件是否存在。如果是,我们通过将/dev/null(始终为空)复制到用户给定的位置,用空文件替换。否则,如果文件不存在,我们只需使用touch创建即可。

在脚本的执行中可以看到,我们第一次运行这个脚本的时候,文件是不存在的,是用touch创建的。在接下来的脚本运行中,文件确实存在(因为它是在第一次运行中创建的)。这次在调试中可以看到使用了cp。因为我们想确定这两个动作是否成功,所以我们包括了一个额外的 if 块,它处理退出状态检查,正如我们之前看到的。

速记语法

到目前为止,我们已经看到了 if 块的一些用法,以查看我们之前的命令是否成功运行。虽然功能很棒,但是在您怀疑可能发生错误的每个命令后使用 5-7 行确实增加了脚本的总长度!更大的问题将是可读性:如果一半的脚本是错误检查,可能很难触及代码的底部。幸运的是,有一种方法可以让我们在命令后直接检查错误。我们可以用||命令来实现这一点,这是逻辑 OR 的 Bash 版本。它的对应物&&是逻辑“与”的实现。为了说明这一点,我们将引入两个新命令:truefalse。如果你看一下各自的手册页,你会发现你能得到的最清楚的答案:

  • 真:什么都不做,成功了
  • 错误:什么都不做,没有成功

以下脚本说明了我们如何使用||和&&来创建逻辑应用流。如果逻辑运算符不熟悉地形,请先查看逻辑运算符下的进一步阅读部分中的链接:

reader@ubuntu:~/scripts/chapter_09$ vim true-false.sh 
reader@ubuntu:~/scripts/chapter_09$ cat true-false.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-02
# Description: Shows the logical AND and OR (&& and ||).
# Usage: ./true-false.sh
#####################################

# Check out how an exit status of 0 affects the logical operators:
true && echo "We get here because the first part is true!"
true || echo "We never see this because the first part is true :("

# Check out how an exit status of 1 affects the logical operators:
false && echo "Since we only continue after && with an exit status of 0, this is never printed."
false || echo "Because we only continue after || with a return code that is not 0, we see this!"

reader@ubuntu:~/scripts/chapter_09$ bash -x true-false.sh 
+ true
+ echo 'We get here because the first part is true!'
We get here because the first part is true!
+ true
+ false
+ false
+ echo 'Because we only continue after || with a return code that is not 0, we see this!'
Because we only continue after || with a return code that is not 0, we see this!

正如我们所期望的,只有当 before 命令返回退出代码为 0 时,才会执行 before 之后的代码,而 before 之后的代码只有在退出代码为而不是 0(通常为 1)时才会执行。如果您仔细观察,您实际上可以在脚本的调试中看到这种情况。可以看到true被执行了两次,还有false。然而,我们最终看到的第一个echo是在第一个真之后,而我们看到的第二个echo是在第二个假之后!为了方便起见,我们在前面的代码中强调了这一点。

现在,我们如何使用它来处理错误?一个错误将给出一个不是 0 的退出状态,所以这相当于false命令。在我们的示例中,逻辑运算符||之后的代码是在 false 之后打印的。这是有道理的,因为要么false要么echo应该成功。这种情况下,由于false(默认)失败,执行echo。在下面的简单示例中,我们将向您展示如何在脚本中使用||运算符:

reader@ubuntu:~/scripts/chapter_09$ vim logical-or.sh
reader@ubuntu:~/scripts/chapter_09$ cat logical-or.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-02
# Description: Use the logical OR for error handling.
# Usage: ./logical-or.sh
#####################################

# This command will surely fail because we don't have the permissions needed:
cat /etc/shadow || exit 123

reader@ubuntu:~/scripts/chapter_09$ cat /etc/shadow
cat: /etc/shadow: Permission denied
reader@ubuntu:~/scripts/chapter_09$ echo $?
1
reader@ubuntu:~/scripts/chapter_09$ bash logical-or.sh 
cat: /etc/shadow: Permission denied
reader@ubuntu:~/scripts/chapter_09$ echo $?
123

我们尝试cat一个我们没有权限的文件(这是一件好事,因为/etc/shadow包含系统上所有用户的哈希密码)。当我们正常这样做的时候,我们收到的退出状态为 1,从我们的手册cat中可以看到。然而,在我们的脚本中,我们使用exit 123。如果我们的逻辑运算符完成了它的工作,我们将不会以默认的1退出,而是以退出状态 123 退出。当我们调用脚本时,我们会得到同样的Permission denied错误,但是这次当我们打印返回代码时,我们看到了预期的123

If you really want to confirm that the code after || is only executed if the first part fails, run the script with sudo. In this case, you will see the contents of /etc/shadow, since root has those permissions and the exit code will be 0 instead of the earlier 1 and 123.

同样,如果您只想在完全确定第一个命令已成功完成时执行代码,也可以使用&。为了以一种非常优雅的方式处理潜在的错误,最好在||之后组合echoexit。在下一个示例中,在接下来的几页中,您将看到这是如何实现的!在本书的剩余部分,我们将使用这种方式处理错误,所以不要担心语法——在本书结束之前,您将会遇到更多次。

错误预防

此时,您应该对我们如何处理(用户输入)错误有一个坚定的把握。显然,上下文是这里的一切:根据情况,一些错误以不同的方式处理。本章还有一个比较重要的主题,那就是防错。虽然知道如何处理错误是一回事,但如果我们能在脚本执行期间完全防止错误,那就更好了。

检查参数

正如我们在前一章中提到的,当您处理传递给脚本的位置参数时,有几件事非常重要。其中一个是空白,表示参数之间的边界。如果我们需要将一个包含空格的参数传递给我们的脚本,我们需要用单引号或双引号将该参数括起来,否则它将被解释为多个参数。位置参数的另一个重要方面是获得正确的参数数量:不要太少,但也绝对不要太多。

通过检查传递的参数数量来启动我们的脚本(使用位置参数),我们可以验证用户是否正确调用了脚本。否则,我们可以指导用户如何正确调用它!以下示例向您展示了我们如何做到这一点:

reader@ubuntu:~/scripts/chapter_09$ vim file-create.sh 
reader@ubuntu:~/scripts/chapter_09$ cat file-create.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-01
# Description: Create a file with contents with this script.
# Usage: ./file-create.sh <directory_name> <file_name> <file_content>
#####################################

# We need exactly three arguments, check how many have been passed to 
# the script.
if [[ $# -ne 3 ]]; then
  echo "Incorrect usage!"
  echo "Usage: $0 <directory_name> <file_name> <file_content>"
  exit 1
fi
# Arguments are correct, lets continue.

# Save the arguments into variables.
directory_name=$1
file_name=$2
file_content=$3

# Create the absolute path for the file.
absolute_file_path=${directory_name}/${file_name}

# Check if the directory exists; otherwise, try to create it.
if [[ ! -d ${directory_name} ]]; then
  mkdir ${directory_name} || { echo "Cannot create directory, exiting script!"; exit 1; }
fi

# Try to create the file, if it does not exist.
if [[ ! -f ${absolute_file_path} ]]; then
  touch ${absolute_file_path} || { echo "Cannot create file, exiting script!"; exit 1; }
fi

# File has been created, echo the content to it.
echo ${file_content} > ${absolute_file_path}

reader@ubuntu:~/scripts/chapter_09$ bash -x file-create.sh /tmp/directory/ newfile "Hello this is my file"
+ [[ 3 -ne 3 ]]
+ directory_name=/tmp/directory/
+ file_name=newfile
+ file_content='Hello this is my file'
+ absolute_file_path=/tmp/directory//newfile
+ [[ ! -d /tmp/directory/ ]]
+ mkdir /tmp/directory/
+ [[ ! -f /tmp/directory//newfile ]]
+ touch /tmp/directory//newfile
+ echo Hello this is my file
reader@ubuntu:~/scripts/chapter_09$ cat /tmp/directory/newfile 
Hello this is my file

为了恰当地说明这个原则和我们之前看到的其他一些原则,我们创建了一个相当大而复杂的脚本(与您之前看到的相比)。为了便于理解,我们将把它分成几部分,并依次讨论每一部分。我们将从标题开始:

#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-01
# Description: Create a file with contents with this script.
# Usage: ./file-create.sh <directory_name> <file_name> <file_content>
#####################################
...

舍邦和大部分地区现在应该感觉很自然。然而,当指定位置参数时,如果它们是必需的,我们喜欢将它们包含在 < > 中,如果它们是可选的,我们喜欢将它们包含在 [] 中(例如,如果它们有默认值,我们将在本章末尾看到)。这是脚本中常见的模式,您最好遵循它!脚本的下一部分是对参数数量的实际检查:

...
# We need exactly three arguments, check how many have been passed to the script.
if [[ $# -ne 3 ]]; then
  echo "Incorrect usage!"
  echo "Usage: $0 <directory_name> <file_name> <file_content>"
  exit 1
fi
# Arguments are correct, lets continue.
...

这一部分的魔力来自$#组合。类似于$?exit status 构造,$#被解析为已传递给脚本的参数数。因为这是一个整数,我们可以使用test-ne-eq标志,将其与我们需要的参数数量进行比较:三个。任何不是三的都不适用于这个脚本,这就是为什么我们以这种方式构建检查。如果测试为阳性*(表示结果为阴性!),我们执行then-logic,告知用户调用脚本不正确。为了防止这种情况再次发生,还传递了使用脚本的正确方法。我们在这里再使用一个技巧,即$0标志。这将解析为脚本名称,这就是为什么在错误调用的情况下,脚本名称会很好地打印在实际预期参数的旁边,如下所示:*

reader@ubuntu:~/scripts/chapter_09$ bash file-create.sh 1 2 3 4 5
Incorrect usage!
Usage: file-create.sh <directory_name> <file_name> <file_content>

由于这个检查和给用户的提示,我们期望用户只错误地调用这个脚本一次。因为我们还没有开始处理脚本的功能,所以我们不会出现脚本中一半任务已经完成的情况,即使我们在脚本开始时知道它永远不会完成,因为它缺少脚本需要的信息。让我们进入脚本的下一部分:

...
# Save the arguments into variables.
directory_name=$1
file_name=$2
file_content=$3

# Create the absolute path for the file.
absolute_file_path=${directory_name}/${file_name}
...

作为总结,我们可以看到,我们将位置用户输入分配给了一个变量名,我们选择这个变量名来表示它正在保存的东西。因为我们需要不止一次地使用最终文件的绝对路径,所以我们根据用户输入组合两个变量来形成文件的绝对路径。脚本的下一部分包含实际功能:

...
# Check if the directory exists; otherwise, try to create it.
if [[ ! -d ${directory_name} ]]; then
  mkdir ${directory_name} || { echo "Cannot create directory, exiting script!"; exit 1; }
fi

# Try to create the file, if it does not exist.
if [[ ! -f ${absolute_file_path} ]]; then
  touch ${absolute_file_path} || { echo "Cannot create file, exiting script!"; exit 1; }
fi

# File has been created, echo the content to it.
echo ${file_content} > ${absolute_file_path}

对于文件和目录,我们都进行类似的检查:我们检查目录/文件是否已经存在,或者我们是否需要创建它。通过使用||带有echoexit的速记,我们检查mkdirtouch是否返回退出状态 0。记住,如果他们返回除 0 之外的任何东西*,那么||之后和大括号内的所有内容都将被执行,在这种情况下退出脚本!*

最后一部分包含对文件的回显的重定向。简单地说,echo 的输出被重定向到一个文件中。重定向将在第 12 章脚本中使用管道和重定向中进行深入讨论。现在,接受我们用于${file_content}的文本将被写入文件(您可以自己检查)。

管理绝对和相对路径

有一个问题我们还没有讨论:用绝对路径和相对路径运行脚本。这看似微不足道,但绝对不是。您运行的大多数命令,尽管是直接交互的,或者是从您调用的脚本中运行的,都使用您当前的工作目录作为它们当前的工作目录。您可能希望脚本中的命令默认为脚本所在的目录,但是由于脚本只不过是当前 shell 的一个分叉(如本章开头所述),所以它也继承了当前的工作目录。我们可以通过创建一个将文件复制到相对路径的脚本来最好地说明这一点:

reader@ubuntu:~/scripts/chapter_09$ vim log-copy.sh 
reader@ubuntu:~/scripts/chapter_09$ cat log-copy.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-02
# Description: Copy dpkg.log to a local directory.
# Usage: ./log-copy.sh
#####################################

# Create the directory in which we'll store the file.
if [[ ! -d dpkg ]]; then
  mkdir dpkg || { echo "Cannot create the directory, stopping script."; exit 1; }
fi

# Copy the log file to our new directory.
cp /var/log/dpkg.log dpkg || { echo "Cannot copy dpkg.log to the new directory."; exit 1; }

reader@ubuntu:~/scripts/chapter_09$ ls -l dpkg
ls: cannot access 'dpkg': No such file or directory
reader@ubuntu:~/scripts/chapter_09$ bash log-copy.sh 
reader@ubuntu:~/scripts/chapter_09$ ls -l dpkg
total 632
-rw-r--r-- 1 reader reader 643245 Oct  2 19:39 dpkg.log
reader@ubuntu:~/scripts/chapter_09$ cd /tmp
reader@ubuntu:/tmp$ ls -l dpkg
ls: cannot access 'dpkg': No such file or directory
reader@ubuntu:/tmp$ bash /home/reader/scripts/chapter_09/log-copy.sh 
reader@ubuntu:/tmp$ ls -l dpkg
total 632
-rw-r--r-- 1 reader reader 643245 Oct  2 19:39 dpkg.log

脚本本身非常简单——检查目录是否存在,否则创建它。您可以使用我们的速记错误处理来检查mkdir上的错误。接下来,将一个已知文件(/var/log/dpkg.log)复制到dpkg目录。第一次运行时,我们和脚本在同一个目录中。我们可以看到在那里创建的dpkg目录以及里面复制的文件。然后,我们将当前工作目录移动到/tmp/并再次运行脚本,这次使用绝对路径而不是第一次调用的相对路径。现在可以看到dpkg目录是在/tmp/dpkg/创建的!不是真的想不到,但是我们怎么会avoid这个呢?脚本开头的一行就可以解决这个问题:

reader@ubuntu:~/scripts/chapter_09$ cp log-copy.sh log-copy-improved.sh
reader@ubuntu:~/scripts/chapter_09$ vim log-copy-improved.sh 
reader@ubuntu:~/scripts/chapter_09$ cat log-copy-improved.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-02
# Description: Copy dpkg.log to a local directory.
# Usage: ./log-copy-improved.sh
#####################################

# Change directory to the script location.
cd $(dirname $0)

# Create the directory in which we'll store the file.
if [[ ! -d dpkg ]]; then
  mkdir dpkg || { echo "Cannot create the directory, stopping script."; exit 1; }
fi

# Copy the log file to our new directory.
cp /var/log/dpkg.log dpkg || { echo "Cannot copy dpkg.log to the new directory."; exit 1; }

reader@ubuntu:~/scripts/chapter_09$ cd /tmp/
reader@ubuntu:/tmp$ rm -rf /tmp/dpkg/
reader@ubuntu:/tmp$ rm -rf /home/reader/scripts/chapter_09/dpkg/
reader@ubuntu:/tmp$ bash -x /home/reader/scripts/chapter_09/log-copy-improved.sh 
++ dirname /home/reader/scripts/chapter_09/log-copy-improved.sh
+ cd /home/reader/scripts/chapter_09
+ [[ ! -d dpkg ]]
+ mkdir dpkg
+ cp /var/log/dpkg.log dpkg
reader@ubuntu:/tmp$ ls -l dpkg
ls: cannot access 'dpkg': No such file or directory

正如代码执行应该显示的,我们现在做所有与脚本位置相关的事情。这是通过一点点 Bash 魔法结合dirname命令实现的。这个命令也很简单:它打印我们传递的目录名,在本例中是$0。您可能还记得,$0 解析为脚本名。From /tmp/,这是绝对路径;如果我们从另一个目录调用它,它可能是一个相对路径。如果我们和脚本在同一个目录下,dirname,$0 将导致.,这意味着我们cd到当前目录。这并不是真正需要的,但也没有任何坏处。对于一个更加健壮的脚本来说,这似乎是一个小小的回报,我们现在可以在任何地方调用它!

For now, we won't go into details regarding the $(...) syntax. We will further discuss this in Chapter 12, Using Pipes and Redirection in Scripts. At this point, remember that this allows us to get a value which we can pass to cd in a single line.

处理 y/n

在这一章的开始,我们向您展示了一些需要思考的问题:通过陈述是或否来要求用户同意或不同意某件事。正如我们所讨论的,我们可以期望用户给出许多可能的答案。实际上,有五种方式用户可以给我们一个 yes : y,Y,yes,yes,YES。

也是如此。让我们看看如何在不使用任何技巧的情况下检查这一点:

reader@ubuntu:~/scripts/chapter_09$ vim yes-no.sh 
reader@ubuntu:~/scripts/chapter_09$ cat yes-no.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-01
# Description: Dealing with yes/no answers.
# Usage: ./yes-no.sh
#####################################

read -p "Do you like this question? " reply_variable

# See if the user responded positively.
if [[ ${reply_variable} = 'y' || ${reply_variable} = 'Y' || ${reply_variable} = 'yes' || ${reply_variable} = 'YES' || ${reply_variable} = 'Yes' ]]; then
  echo "Great, I worked really hard on it!"
  exit 0
fi

# Maybe the user responded negatively?
if [[ ${reply_variable} = 'n' || ${reply_variable} = 'N' || ${reply_variable} = 'no' || ${reply_variable} = 'NO' || ${reply_variable} = 'No' ]]; then
  echo "You did not? But I worked so hard on it!"
  exit 0
fi

# If we get here, the user did not give a proper response.
echo "Please use yes/no!"
exit 1

reader@ubuntu:~/scripts/chapter_09$ bash yes-no.sh 
Do you like this question? Yes
Great, I worked really hard on it!
reader@ubuntu:~/scripts/chapter_09$ bash yes-no.sh 
Do you like this question? n
You did not? But I worked so hard on it!
reader@ubuntu:~/scripts/chapter_09$ bash yes-no.sh 
Do you like this question? maybe 
Please use yes/no!

虽然这是可行的,但它并不是一个真正可行的解决方案。更糟糕的是,如果用户在尝试键入 Yes 时碰巧打开了 Caps Lock,我们最终将得到 yES !我们需要把它也包括进去吗?答案当然是否定的,Bash 有一个俏皮的小功能叫做参数扩展。我们将在第 16 章Bash 参数替换和扩展中对此进行更深入的解释,但现在,我们可以给你预览一下它的功能:

reader@ubuntu:~/scripts/chapter_09$ cp yes-no.sh yes-no-optimized.sh
reader@ubuntu:~/scripts/chapter_09$ vim yes-no-optimized.sh 
reader@ubuntu:~/scripts/chapter_09$ cat yes-no-optimized.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-10-01
# Description: Dealing with yes/no answers, smarter this time!
# Usage: ./yes-no-optimized.sh
#####################################

read -p "Do you like this question? " reply_variable

# See if the user responded positively.
if [[ ${reply_variable,,} = 'y' || ${reply_variable,,} = 'yes' ]]; then
  echo "Great, I worked really hard on it!"
  exit 0
fi

# Maybe the user responded negatively?
if [[ ${reply_variable^^} = 'N' || ${reply_variable^^} = 'NO' ]]; then
  echo "You did not? But I worked so hard on it!"
  exit 0
fi

# If we get here, the user did not give a proper response.
echo "Please use yes/no!"
exit 1

reader@ubuntu:~/scripts/chapter_09$ bash yes-no-optimized.sh 
Do you like this question? YES
Great, I worked really hard on it!
reader@ubuntu:~/scripts/chapter_09$ bash yes-no-optimized.sh 
Do you like this question? no
You did not? But I worked so hard on it!

我们现在不再对每个答案进行五次检查,而是使用两次检查:一次检查完整的单词(是/否),一次检查简短的单字母答案(y/n)。但是,当我们只指定了时,答案是如何工作的?这个问题的解决方案在于,和^^,我们已经将它们包含在变量中。所以,我们用${reply_variable,,}和${reply_variable^^}.代替了${reply_variable}在、、的情况下,变量首先被解析为其值,然后被转换为所有小写字母。正因为如此,这三个答案–是,是,是–都可以与相提并论,因为这就是 Bash 将如何扩展它们。你可能会猜测^^做了什么:它将字符串的内容转换成大写,这就是为什么我们可以将它与 NO 进行比较,即使我们给出的答案是 no。

Always try to place yourself in the users' shoes. They are dealing with many different tools and commands. In most of these cases, logic as given for dealing with different ways of writing yes/no has been integrated. This can make even the most friendly system administrator a bit lazy and train them to go for the one-letter answer. But you wouldn't want to punish the sysadmin that actually listens to you, either! So, make a point of dealing with the most reasonable answers in a friendly manner.

摘要

在本章中,我们讨论了 Bash 脚本中错误的许多方面。首先,描述了错误检查。首先,我们解释了退出状态是命令传达其执行是成功还是失败的一种方式。介绍了test命令及其简写[[...]]符号。这个命令允许我们在脚本中执行功能检查。例如,比较字符串和整数,检查文件或目录是否已创建且可访问/可写。我们快速复习了一下变量,然后简单介绍了如何使用设置的调试标志-x运行脚本。

本章第二部分涉及错误处理。我们描述了(非官方的)if-then-exit构造,我们用它来检查命令执行,如果失败就退出。在接下来的例子中,我们看到,当我们想要检查变量时,我们并不总是需要编写返回代码;我们可以用$?直接在测试用例中。接着,我们预览了如何使用if-then-else逻辑以更好的方式处理错误。我们在本章的第二部分结束时介绍了错误处理的速记语法,我们将在本书的其余部分继续使用它。

在本章的第三部分也是最后一部分,我们解释了错误预防。我们学习了如何检查参数是否正确,以及如何在调用脚本时避免绝对路径和相对路径的问题。在本章的最后一部分,我们回答了开头提出的问题:如何最好地处理用户的是/否输入?通过使用一些简单的 Bash 参数扩展(这将在本书的最后一章中进一步解释),我们能够简单地为我们的脚本用户提供多种应答风格。

本章介绍了以下命令:mktemptruefalse

问题

  1. 为什么我们需要退出状态?
  2. 退出状态、退出代码和返回代码有什么区别?
  3. 我们在测试中使用哪个标志来测试以下内容?
    • 现有目录
    • 可写文件
    • 现有的符号链接
  4. test -d /tmp/的首选速记语法是什么?
  5. 我们如何在 Bash 会话中打印调试信息?
  6. 如何才能检查一个变量是否有内容?
  7. 获取返回代码的 Bash 格式是什么?
  8. ||&&中,哪个是逻辑“与”,哪个是“或”?
  9. 获取参数数量的 Bash 格式是什么?
  10. 我们如何确保用户从哪个工作目录调用脚本并不重要?
  11. 在处理用户输入时,Bash 参数扩展如何帮助我们?

进一步阅读

如果您想深入了解本章的主题,以下资源可能会很有意思: