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