原文: https://pythonspeed.com/articles/shell-scripts/
作者:Itamar Turner-Trauring最后更新于 2022 年 3 月 24 日,最初创建于 2022 年 3 月 22 日
当您自动化某些任务时,例如为 Docker 打包您的应用程序时,您经常会发现自己正在编写 shell 脚本。您可能有一个bash脚本来驱动打包过程,另一个脚本作为容器的入口点。随着您的包装变得越来越复杂,您的 shell 脚本也越来越复杂。
一切正常。
然后,有一天,你的 shell 脚本做了一些完全错误的事情。
那是你意识到你的错误的时候:bash和一般的 shell 脚本语言,在默认情况下大多是被破坏的。除非您从第一天开始就非常小心,否则几乎可以保证任何超过一定复杂度级别的 shell 脚本都是错误的……并且改进正确性功能非常困难。
bash作为一个具体的例子,我们重点来看看。
考虑以下 shell 脚本:
#!/bin/bash
touch newfile
cp newfil newfile2 # Deliberate typo
echo "Success"当我们运行它时,你认为会发生什么?
$ bash bad1.sh
cp: cannot stat 'newfil': No such file or directory
Success即使命令失败,脚本也会继续运行!将此与 Python 进行比较,其中异常会阻止以后的代码运行。
您可以通过添加set -e到 shell 脚本的顶部来解决此问题:
#!/bin/bash
set -e
touch newfile
cp newfil newfile2 # Deliberate typo, don't omit!
echo "Success"现在:
$ bash bad1.sh
cp: cannot stat 'newfil': No such file or directory接下来让我们考虑以下脚本,它尝试将目录添加到PATH环境变量中。 PATH是如何找到可执行文件的位置。
#!/bin/bash
set -e
export PATH="venv/bin:$PTH" # Typo is deliberate
ls当我们运行它时:
$ bash bad2.sh
bad2.sh: line 4: ls: command not found它找不到ls,因为我们有一个错字,写PTH而不是PATH——并且bash没有抱怨未知的环境变量。在 Python 中你会得到一个NameError例外;在编译语言中,代码甚至无法编译。在bash脚本中只是继续运行;会出什么问题?解决方案是set -u:
#!/bin/bash
set -eu
export PATH="venv/bin:$PTH" # Typo is deliberate
ls现在 bash 发现了错字:
$ bash bad2.sh
bad2.sh: line 3: PTH: unbound variable我们认为我们用 解决了失败的命令问题set -e,但我们并没有解决所有情况:
#!/bin/bash
set -eu
nonexistentprogram | echo
echo "Success!"当我们运行它时:
$ bash bad3.sh
bad3.sh: line 3: nonexistentprogram: command not found
Success! 解决方案是set -o pipefail:
#!/bin/bash
set -euo pipefail
nonexistentprogram | echo
echo "Success!"现在:
$ bash bad3.sh
bad3.sh: line 3: nonexistentprogram: command not found至此,我们已经实现了(大部分)非官方的bash严格模式。但这还不够。
注意:本文的早期版本包含有关子shell 的错误信息。感谢 Loris Lucido 指出我的错误。
使用该$()语法,您可以启动一个子shell:
#!/bin/bash
set -euo pipefail
export VAR=$(echo hello | nonexistentprogram)
echo "Success!"当我们运行它时:
$ bash bad4.sh
bad4.sh: line 3: nonexistentprogram: command not found
Success!这是怎么回事?如果子shell 中的错误是命令参数的一部分,则它们不会被视为错误。这意味着 subshell 的错误会被丢弃。
一个例外是直接设置变量,所以我们需要这样编写代码:
#!/bin/bash
set -euo pipefail
VAR=$(echo hello | nonexistentprogram)
export VAR
echo "Success!"现在我们的程序运行正常:
$ bash good4.sh
good4.sh: line 3: nonexistentprogram: command not found这可能是对bash的不良行为的充分证明,但肯定不是完整的证明。
无论如何,您可能想要使用 shell 脚本的一些原因是什么?
几乎每个 Unix-y 计算环境都会有一个基本的 shell。因此,如果您正在编写一些打包或启动脚本,那么很容易使用您知道会出现的工具。
问题是,如果你正在打包一个 Python 应用程序,你几乎可以保证开发环境、CI 和运行时环境都安装了 Python。那么为什么不使用默认情况下实际处理错误的编程语言呢?
更广泛地说,几乎每一种具有相当规模用户群的编程语言都会有某种面向脚本的库或习语。例如,Rust 也有xshell, 和其他库。因此,在大多数情况下,您可以使用您选择的编程语言而不是 shell 脚本。
理论上,如果您知道自己在做什么,并且保持专注并且不会忘记任何样板文件,那么您可以编写正确的 shell 脚本,甚至是非常复杂的脚本。你甚至可以编写单元测试。
在实践中:
set -euo pipefail
如果你正在编写 shell 程序,shellcheck这是一个非常有用的捕捉 bug 的方法。不幸的是,仅靠它是不够的。
考虑以下程序:
#!/bin/bash
echo "$(nonexistentprogram | grep foo)"
export VAR="$(nonexistentprogram | grep bar)"
cp x /nosuchdirectory/
echo "$VAR $UNKNOWN_VAR"
echo "success!"如果我们运行这个程序,它会打印“成功!”,即使它有 4 个单独的问题(至少):
$ bash bad6.sh
bad6.sh: line 2: nonexistentprogram: command not found
bad6.sh: line 3: nonexistentprogram: command not found
cp: cannot stat 'x': No such file or directory
success!怎么shellcheck做?它会解决一些问题……但不是全部:
shellcheck,它会指出问题所在exportshellcheck -o all它会运行所有检查,它也会指出echo "$(nonexistentprogram ...)" 也就是说,假设您使用的是 2021 年 11 月发布的 v0.8。旧版本没有此检查,因此任何早于的 Linux 发行版都会为您提供shellcheck不会遇到该问题的版本。set -euo pipefail如果您依赖shellcheck我强烈建议升级并确保您使用-o all.
Shell 脚本在某些情况下很好:
set -euo pipefail 就足够了(并确保使用shellcheck -o all)。一旦你发现自己做了除此之外的任何事情,你最好使用一种不易出错的编程语言。鉴于大多数软件往往会随着时间的推移而增长,你最好的选择是从不那么坏的东西开始。