Shell 脚本很重要

2025-06-07

Shell 脚本很重要

Bash 徽标

Shell 真是个怪兽。尽管它与软件工程的所有当前趋势(强类型、编译检查优先于运行时检查等等)相悖,但 Shell 脚本依然存在,并且仍然是每个开发人员生活中的重要组成部分。

关于 shell 脚本的奇怪之处在于,即使是良好实践的强烈倡导者,在谈到 shell 脚本时也会忘记他们所知道的一切。

版本控制?何必呢,代码都是一次性的。

代码质量?这只是个 shell 脚本,反正就是垃圾。

测试?不行。目前没有合适的工具。

错,错,错。Shell 脚本有其价值。所有为真实代码所做的工作都应该在非琐碎的 Shell 脚本中完成,即使是一次性脚本。这包括版本控制、代码审查、持续集成、静态代码分析和测试。

这里总结了编写 shell 脚本时可以做和应该做的所有事情。

注意:本文将使用 Bash 作为参考 shell。大部分内容可以转换到其他符合 POSIX 标准的 shell。

保持脚本的版本控制

将 Shell 脚本置于版本控制之下有多个优点:

  • 它构成了一个库。Shell 脚本可能很难编写。如果你能找到某个地方某个难题的参考资料,你的同事在需要的时候会感谢你的。你应该尽快在某个地方建立一个“shell-scripts”仓库。
  • 它们可以得到适当的审查。Shell 脚本很容易出错,而且可能造成很大的损失。Shell脚本的代码审查应该是强制性的,就像任何其他代码一样。
  • 它们可以改进。我不会向你解释什么是版本控制。但是有了版本控制的 Shell 脚本,定期改进就变得很容易了。

从现在开始,请在运行所有 Shell 脚本之前对其进行版本控制。在生产环境中执行脚本之前,请优先安排人员审核。这不仅浪费同事的时间,还能为团队节省时间。

使用 ShellCheck 提高脚本质量

虽然您可以使用命令检查脚本的语法有效性bash -n,但还有更多强大的工具。

ShellCheck是一款用于 Shell 脚本的静态代码分析工具。它真的是一款非常棒的工具,能够帮助你在使用过程中提升技能。所以,一定要使用它。你可以在你的机器上全局安装它,在持续集成中使用它,它甚至可以与大多数主流编辑器完美集成。使用 ShellCheck 真的没有任何缺点,它还能帮你避免自己犯错。

如果 Steam 在 2015 年就使用了 ShellCheck,那么这句话就永远不会出现在生产环境中

rm -rf "$STEAMROOT/"*
Enter fullscreen mode Exit fullscreen mode

此代码违反了ShellCheck 的SC2115 规则。

使用 Bash 非官方严格模式

非官方的严格模式源自 Aaron Maxwell 的文章《使用非官方的 Bash 严格模式(除非你喜欢调试)》。他建议在每个 Bash 脚本的开头添加以下几行:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
Enter fullscreen mode Exit fullscreen mode
  • set -e如果任何命令返回非零状态码,则将退出脚本。为了防止该选项在命令返回非零状态码时触发(即使没有发生错误),有两种解决方案:

  • 使用|| true模式:

command_returning_non_zero || true
Enter fullscreen mode Exit fullscreen mode
  • 暂时禁用该选项:
set +e
command_returning_non_zero
set -e
Enter fullscreen mode Exit fullscreen mode
  • set -u将防止使用未定义的变量。对于未定义的位置参数($1$2、...),可以使用参数扩展结构为其赋予默认值:
my_arg=${1:-"default"}
Enter fullscreen mode Exit fullscreen mode
  • set -o pipefail将强制管道在第一个非零状态代码时失败。

  • IFS=$'\n\t'使迭代和拆分不那么令人意外,尤其是在循环的情况下。此变量的默认值通常是,IFS=$' \n\t'但空格作为分隔符往往会产生令人困惑的结果。

阅读原文,了解更多详细信息以及使用严格模式时常见挑战的解决方案

非官方的严格模式比我们之前见过的模式更具侵入性,可能难以应对,但从长远来看,这是值得的。花点时间尝试一下。

做一些清理工作!

当脚本被中断时,无论是由于用户操作还是发生了其他意外情况,大多数 Shell 脚本都不会清理它们造成的混乱。最糟糕的情况是,它们甚至可能无法重启那些不得不暂时禁用的服务。考虑到用trap命令执行清理和错误捕获是多么容易,这实在令人遗憾。

再次,在“ “退出陷阱”如何让你的 Bash 脚本更加健壮和可靠”中,Aaron Maxwell 给出了一些很好的建议。

始终在 shell 脚本中添加以下内容:

cleanup() {
    # ...
}
trap cleanup EXIT
Enter fullscreen mode Exit fullscreen mode

该命令将在脚本退出后立即trap执行函数。在此函数中,您可以删除临时文件、重启服务或执行任何与脚本相关的操作。cleanup

使用 shUnit2 测试你的脚本

shUnit2是一个用于 Shell 脚本的单元测试框架。它受 JUnit 启发。它包含在标准仓库中,因此您可以apt-get install shunit2在基于 Ubuntu 的发行版上安装它。

shUnit2 包含一个可以source在测试文件中运行的 Shell 脚本。使用它的方法有很多种。为了避免主脚本过于杂乱,我更喜欢将测试写在一个单独的文件中。这意味着我将拥有一个script.sh文件和一个test_script.sh文件。

下面是一个提供两个数字相加功能的脚本示例。

add.sh必须具有以下结构:

add() {
    local a=$1
    local b=$2
    echo $(( a + b ))
}

if [[ "${BASH_SOURCE[0]}" = "$0" ]]; then
    # Main code of the script
    add $1 $2
fi
Enter fullscreen mode Exit fullscreen mode

测试[[ "${BASH_SOURCE[0]}" = "$0" ]]只在直接执行脚本时才执行主代码,而不是sourced。

test_add.sh将会像这样:

. ./add.sh

test_add() {
    actual=$(add 5 8)
    expected=13
    assertEquals "$expected" "$actual"
}

. shunit2
Enter fullscreen mode Exit fullscreen mode

首先,测试文件source是主文件add.sh(在 Bash 中,.是 的别名source)。它声明的函数随后可在测试脚本中使用。

实际的测试是一些以 开头的简单函数test。最后,全局安装的dshunit2函数source发挥了它的魔力

然后可以执行测试文件:

$ bash test_add.sh
test_add

Ran 1 test.

OK
Enter fullscreen mode Exit fullscreen mode

shUnit2 的功能细节在其文档中有说明。

shUnit2 有一些替代方案,例如BatsRoundup,但我还没有机会使用它们。不过它们的用法应该比较类似。本节的重点是,无论你最终选择哪种解决方案,测试 Shell 脚本都是可行的,而且应该这样做。

记录脚本正在执行的操作

过去,我犯了一个错误,就是不记录任何内容。我喜欢运行脚本,然后看着它神奇地运行,控制台里却没有任何难看的东西。但我错了,因为当某些东西没有按预期工作时,就不可能知道发生了什么。运行脚本不应该感觉像魔术一样,它应该有点冗长且易于理解。为此,请尽可能在脚本中记录日志

为此,我通常在脚本中添加以下几行:

readonly LOG_FILE="/tmp/$(basename "$0").log"
info()    { echo "[INFO]    $*" | tee -a "$LOG_FILE" >&2 ; }
warning() { echo "[WARNING] $*" | tee -a "$LOG_FILE" >&2 ; }
error()   { echo "[ERROR]   $*" | tee -a "$LOG_FILE" >&2 ; }
fatal()   { echo "[FATAL]   $*" | tee -a "$LOG_FILE" >&2 ; exit 1 ; }
Enter fullscreen mode Exit fullscreen mode

这个小型日志框架可以轻松追踪脚本执行过程中发生的一切。记录日志变得非常简单,只需输入 即可info "Executing this and that..."。然后,您就可以轻松地grep在日志文件中查找特定内容。您可以根据需要随意改进这些功能,例如添加日期、调用函数名称(使用$FUNCNAME)等。

我不使用内置函数,logger因为它需要特殊权限才能写入,/var/log而且我不喜欢它的用法。写入日志文件/tmp通常就足够了。cron不过对于脚本来说,你可能需要研究一下logger

根据需要使用-v--verbose调用的命令来提高日志记录的质量。

学习调试脚本

除了记录日志之外,调试 Shell 脚本最简单的方法是使用 运行它bash -x。另一种方法是set -x在脚本内部使用。此选项将使 Bash 在执行每个命令之前打印它们,并将变量替换为其实际值。与非官方的严格模式一起使用,此方法有助于查看脚本中发生的情况,同时降低破坏环境的风险。

值得一提的是,Bash 也有一些调试器,例如bashdb 。bashdb 的工作方式与 gdb 相同,可以用来添加断点、切换到分步执行、显示变量的值等。您可以观看视频“使用 BashDB 调试 Shell 脚本”来学习如何使用 bashdb

记录你的脚本

任何 Shell 脚本都应该有一个--help选项。这看起来不容易?其实很简单,这要归功于以下技巧:

#/ Usage: add <first number> <second number>
#/ Compute the sum of two numbers
usage() {
    grep '^#/' "$0" | cut -c4-
    exit 0
}
expr "$*" : ".*--help" > /dev/null && usage
Enter fullscreen mode Exit fullscreen mode

usage函数将打印以注释开头的每一行#/,不带此前缀。

expr命令将检查所有参数连接后的字符串是否包含--help。如果包含,则调用usage

这绝对不是解析参数最干净的方法,但这种快速方法可确保您添加一个--help标志。

为了良好的实践,这个 StackOverflow 帖子while解释了如何使用//case结构正确解析脚本的参数shift

要从您的评论生成 HTML 文档,您还可以检查shocco.sh,它启发了上述技巧

随机建议

以下是我历经艰辛才总结出来的一些好习惯。我会在讲解每条建议时解释其背后的原理。

使用 Bash 编写脚本

默认使用 Bash,必要时可以使用 Sh。除非有充分的理由,否则尽量不要使用 Ksh、Zsh 或 Fish。这种选择不仅可以确保你的脚本几乎可以在任何地方运行,还能促进整个团队对脚本的理解。你不是为自己编写生产脚本。

使用 Bashism

如果你使用 Bash,不要只用一半。使用参数扩展。使用局部变量只读变量。使用改进的条件表达式

引用你的变量

即使你知道不需要加引号,也应该始终给变量加引号。唯一的例外是你明确希望进行扩展。更多关于分词的信息

命名参数

毋庸置疑,明确命名参数($1$2、...)可以使代码具有自文档性,并有助于提高可读性。Bash 的参数扩展非常适合为位置参数命名和分配默认值:

my_arg=${1:-default}
Enter fullscreen mode Exit fullscreen mode

使用子 shell 来控制全局范围内的内容

一个例子胜过千言万语:

var=1
echo $var
(
    echo $var
    var=5
    echo $var
)
echo $var
Enter fullscreen mode Exit fullscreen mode

将打印:

1
1
5
1
Enter fullscreen mode Exit fullscreen mode

我主要用在需要临时修改$IFS(例如,遍历简单的 CSV 类文件)然后将其重置为原始值时。

使用模板

这个脚本模板总结了本文分享的所有代码片段。我相信它可以为任何类型的脚本提供良好的基础。

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

#/ Usage:
#/ Description:
#/ Examples:
#/ Options:
#/   --help: Display this help message
usage() { grep '^#/' "$0" | cut -c4- ; exit 0 ; }
expr "$*" : ".*--help" > /dev/null && usage

readonly LOG_FILE="/tmp/$(basename "$0").log"
info()    { echo "[INFO]    $*" | tee -a "$LOG_FILE" >&2 ; }
warning() { echo "[WARNING] $*" | tee -a "$LOG_FILE" >&2 ; }
error()   { echo "[ERROR]   $*" | tee -a "$LOG_FILE" >&2 ; }
fatal()   { echo "[FATAL]   $*" | tee -a "$LOG_FILE" >&2 ; exit 1 ; }

cleanup() {
    # Remove temporary files
    # Restart services
    # ...
}

if [[ "${BASH_SOURCE[0]}" = "$0" ]]; then
    trap cleanup EXIT
    # Script goes here
    # ...
fi
Enter fullscreen mode Exit fullscreen mode

保持知情

Shell 脚本最近没什么动静。所以,读一些相关的资料来了解一下吧,这样更容易跟上潮流!以下是一些有趣的资源:


我希望这篇文章能让您了解 Shell 脚本的潜力。工具已经准备好了,实践也已经掌握。现在,就看您自己如何让脚本成为您和您的团队工作中的乐趣了!

文章来源:https://dev.to/thiht/shell-scripts-matter
PREV
使用 React App 文件夹结构构建随机报价机
NEXT
让我们讨论一下 REGEX !!