Bash脚本精通

skillgohub.com 中文指南 | 中文版

Bash脚本精通

很多人写脚本只求"能出结果",真正遇到带空格的文件名、空目录的 glob、或某个命令悄悄失败却继续往下跑,才发现脚本"几乎能用"是最贵的失败模式。Bash 语法宽容,恰恰因此容易写出貌似正确实则脆弱的代码。这篇指南不讲花哨技巧,专注让你从复制粘贴,走向能应对真实输入的可靠脚本——无论是公司服务器上的定时任务,还是个人电脑上一键部署。

一句话让脚本悄悄损坏数据的典型场景

几乎所有踩过坑的人都有一段"社死"经历:一个 for 循环把含空格的文件名拆成了好几截,一条 grep 匹配错了文件,或者一个备份脚本把要保留的目录给删了。问题不在你不会写,而在 Bash 的默认行为太"宽容"。本文的目标,就是把这些隐藏陷阱一个一个讲透,让你写的每个脚本在真实输入下都站得住脚。

Bash Scripting Mastery - featured image

严格模式不是可选:每个真实脚本都该开启

性价比最高的一步,是让每个脚本都从严格模式起头,并认真对待它。也就是 set -euo pipefail:遇到第一个错误就退出、把未定义变量当错误、管道中任何一环失败都算失败。set -e 会让你那些"静默失败"大幅减少,因为大部分此类故障都来自命令返回非零而脚本却继续跑。

Bash Scripting Mastery comparison and review

严格模式有学习曲线,因为它会让原本"合法"的写法响亮地报错。一个有意容忍失败的循环需要显式加 | trueif 守卫。这股"噪音"恰恰是重点:你应该在出错的确切位置写下为何可接受,而不是让整段脚本掠过真正的错误。

引号、引号、还是引号:一半 Bug 的根源

未加引号的变量是 Bash 最隐蔽的 Bug 来源,尤其集中在文件名和用户输入上。一个叫 报告 最终 v2.txt 的文件,会被未加引号的 $file 拆成多个参数,rm $file 就变成了删除三个不存在的路径,反而把你想删的文件留了下来。修复靠的是肌肉记忆:所有变量展开都用双引号包起来,参数列表用 "$@" 而不是 $*

Bash Scripting Mastery step by step guide

除了引号,列表类数据优先用数组而非空格分隔的字符串。for item in "${arr[@]}" 能正确处理含任意空白的元素,for item in $string 则不能。需要拆分字符串时,用 mapfile 或带受控分隔符的 read -a,而不是依赖空格分词。

不靠猜测地调试 Shell 脚本

脚本行为不对时,忍住乱插 echo 的冲动。用 bash -x 逐条追踪命令及其展开结果,或给可疑区域局部加 set -xbash -n 在不执行的情况下抓语法错误,而 shellcheck 几乎是必须的:它能查出未加引号变量、未使用变量、与 set -e 的交互问题,以及大量经验老手都会漏掉的可移植性陷阱。

Bash Scripting Mastery cost and pricing analysis

对一眼看不出逻辑的故障,加一个 trap 'echo "ERROR at $LINENO" >&2' ERR 调试钩子,出错时打印行号和最近命令。配合 set -u 抓变量名拼写错误。真正的专业,是把"黑盒失败"变成"有根据的猜测"。

处理文件、目录,以及空 glob 的恐怖

Shell 的通配符在匹配不到时会把字面量原样传下去,所以 rm /tmp/output/* 在无文件时变成删除"星号",什么都没删——更糟的是搭配 rm -f 会静默成功。用 nullglob 选项或显式存在检查来守卫。遍历删除文件前,先测试路径确实是文件(-f)再删。

Bash Scripting Mastery tools and features overview

清理逻辑用 trap 'cleanup' EXIT,无论正常退出还是报错都能清理临时文件、恢复工作目录。临时文件应放在 mktemp -d 创建的专属目录里,并由 trap 负责删除。这是"可放心交给 cron 的脚本"和"三个月爬满磁盘的脚本"之间的分水岭。

严肃写脚本用哪种 Shell 和工具:实用对比

Shell / 工具核心特性定价
Bash(GNU)数组、set -euo pipefail、进程替换、随处可用免费(GPL),几乎所有 Linux/macOS 自带
Zsh(搭配 Oh My Zsh)最丰富的交互特性、共享数组语法、插件生态免费(MIT 系许可),macOS 默认
Fish友好的自动补全、更安全的默认值、一等函数免费(GPL),POSIX 兼容性较弱
Shellcheck静态分析,抓引号/逻辑/可移植性 Bug免费(GPLv3),各包管理器都能装
Python / Ruby处理复杂逻辑、JSON、HTTP、结构化数据时替代 Bash免费(开源),常与 Bash 配合调度

成熟团队的普遍趋势是"Bash 负责调度,真正的语言负责逻辑"。Bash 的强项是把工具粘起来、管理进程生命周期;弱项是字符串处理和结构化数据。知道何时改用 Python 而不是硬把 awk 逼成 JSON 解析器,是判断力的体现;遇到金融数据这类重结构性活计,直接交给Python 金融数据处理这类专门语言更省心。想系统打牢脚本思路,可以看看Python 入门指南Python 自动化脚本,为复杂任务找到更顺手的语言。

真正会"说话"的错误处理与返回码

返回码是 Bash 唯一的错误通道,可大多数脚本都浪费了它:成功退 0,失败退非零(理想情况有文档化的区间)。别在函数里吞掉子命令的退出码然后习惯性 return 0。捕获输出时明确是否检查了退出状态:output=$(cmd) | { echo "cmd 失败"; exit 1; },让意图在裸命令替换会隐藏它的地方显现出来。可靠的退出码纪律,也是让脚本能安心作为更大办公自动化流程一环的关键。

函数、作用域与全局变量的陷阱

Bash 里的函数默认共享脚本全局作用域,超过五十行就是雷区。在函数内声明变量用 local,最外层只放配置。用动词命名函数(ensure_dirfetch_remote),每个函数控制在一屏内。把风险最高的命令(写生产、碰远程、改数据)封装成函数,让危险区有单一可审计的入口。

别重复造轮子:自动化重复工作流

写脚本前先确认是否已有成熟工具。解析 CSV、调 API、处理 JSON,用 jqawk、或一门真正语言的脚本,比手搓 Bash 解析器更可靠。目标是可靠地消除重复劳动,而不是写更多 Bash。让脚本具备幂等性(跑两次结果等于跑一次),破坏性步骤放在显式 --force 或确认之后,默认选项要安全。

性能、可移植性,以及何时不要用 Bash

Bash 不擅长大数据循环——命令替换和管道 fork 都有开销。遍历十万行时,用 awksedsortuniq 的管道一次搞定,或整个搬进 Python/编译型工具。可移植性是另一条轴:Bash 专有特性([[ ]]、数组、readarray)在 dash 当 /bin/sh 的系统上跑不了。现代基础设施更务实——shebang 显式写 bash 并记录说明。

从"能用"到可复用的脚本实践

今天写的脚本就是明天信任的自动化,这种信任靠一致性赢得。统一模板:严格模式、帮助参数、清晰返回码、日志、清理 trap,让每个新脚本从"已知良好"的形状起步。把脚本放进仓库像普通代码一样评审,CI 里跑 shellcheck。如果这些脚本还要和表格、报表打交道,用Excel 办公自动化的思路把数据搬运环节一并接进去,整体效率会更高。

常见问题

为什么 set -e 没有在 if 条件里失败的命令上停下脚本?

因为 set -e 特意不会触发 ifwhileuntil&&| 条件里的命令——Bash 认为你在拿状态当测试。想让这种情况下的失败致命,就用 if ! cmd; then ... 显式捕获并处理。

有没有同时兼容 bash 和 sh 的字符串拆分方法?

没有干净的办法。Bash 的 read -aIFS=, read -ra 都是 bash 专属。POSIX sh 下只能用 while IFS=, read -r a rest; do ...; done 循环,比较笨拙。务实做法:真要 sh 兼容就尽量少解析;能要求 bash 就用数组和 mapfile

三行的微型脚本也值得上 shellcheck 吗?

值得,默认就上。它免费、秒级完成,能抓出最常见两种静默 Bug——未加引号变量和未使用变量,调试它们花费的时间远超 shellcheck 运行时间。小脚本同样会进 cron,一个引号 Bug 同样烧时间。

命令替换该用 $(cmd) 还是反引号?

$(cmd)。反引号是遗留写法,难嵌套、转义易混。$() 支持干净嵌套($(dirname "$(command -v foo)")),也更易读。现代 linter 都会标反引号,没有任何理由在目标为 bash 的脚本里用它。

📌 Pinterest 🐦 Twitter 📘 Facebook