Shell 到底是什么:操作系统留给你的那扇门
Shell 是操作系统留给用户的那道门:你敲下的命令、写出的脚本,都要先经过它解释,再交给内核执行。这篇 Shell 脚本从入门到精通的学习笔记,沿着“能用、写对、扛住”三个阶段把学习路径理一遍,同时看看 2025 年 7 月发布的 Bash 5.3 给日常脚本带来了哪些新东西。
从 sh 到 bash 的谱系
很多人第一次被 Shell 坑,是因为不知道 sh 和 bash 并不是一回事。
| Shell | 常见路径 | 特点 |
|---|---|---|
| sh | /bin/sh | 最早的 Unix 命令解释器之一,语法奠定今天的基础 |
| bash | /bin/bash | GNU 重写并增强的版本,多数 Linux 发行版的默认交互 Shell |
| dash | /usr/bin/dash | 体积小、启动快,Ubuntu 上 sh 实际指向它 |
| zsh | /usr/bin/zsh | 功能极其丰富,macOS 从 Catalina 起默认使用 |
| fish | /usr/bin/fish | 对用户友好,但语法与 bash 不兼容 |
| ksh | /bin/ksh | Korn Shell,商业 Unix 上的老朋友 |
这里最需要记住的一点:写脚本时如果 shebang 写的是 /bin/sh,那么在 Debian 和 Ubuntu 上真正执行它的很可能是 dash,bash 的扩展语法统统不能用。
为什么现在还要学 Shell
- 几乎所有 Linux 服务器都自带,不需要额外安装任何运行时。
- 部署、备份、日志清理、CI/CD 流水线这些活儿,往往一行命令抵一段程序代码。
- 它是理解 Linux 的一把钥匙:管道、重定向、进程、权限,全都在这条命令线上体现。
不会 Shell 也能用 Linux,但会了 Shell,你会开始想把它用在每一件重复劳动上。
Bash 5.3:2025 年 7 月的这次更新
先看一张现状表,把版本坐标定下来。
| 项目 | 现状 |
|---|---|
| GNU Bash | 5.3,2025 年 7 月 5 日发布 |
| 上一稳定版 | 5.2,2022 年 9 月 |
| 配套命令行库 | Readline 8.3 |
| 静态检查工具 | ShellCheck 0.11.0,2025 年 8 月 |
| 格式化工具 | shfmt,属于 mvdan/sh 项目 |
| 测试框架 | bats-core 1.12.0,2025 年 5 月 |
| 相关标准 | POSIX.1-2024,即 IEEE 1003.1-2024 |
从 2022 年 9 月的 5.2 到 2025 年 7 月的 5.3,中间隔了将近三年。官方公告里写着这一版包含“重要的新功能”,而它给出的东西确实没有让人失望。
不 fork 的命令替换
传统写法 $(命令) 会启动一个子 shell 来执行命令,再把它捕获回来。5.3 增加了两种新形式,直接在当前 shell 的执行上下文里跑,省掉一层 fork。
# 传统写法:启动一个子 shell,输出被捕获
result=$(date +%F)
echo "$result"
# 5.3 新增:在当前 shell 上下文执行并捕获输出,不 fork
result=${ date +%F; }
echo "$result"
# 5.3 新增:结果不返回,而是留在 REPLY 变量里
${| echo "hello"; }
echo "$REPLY"对那种在一个循环里反复做命令替换的脚本,这两行省下的是实打实的进程开销。
补全与交互增强
- 新增
GLOBSORT变量,可以指定路径名补全结果按名称、大小、块数、修改时间、访问时间、状态变更时间或数值排序,还支持升序与降序。 compgen内建命令新增选项,能把生成的补全候选写进指定变量,而不是直接打到屏幕上。read内建命令新增-E选项,读取输入时启用 Readline 的补全,连 Tab 补全都能用上,写交互式工具很方便。source内建命令新增-p PATH,指定去哪个路径找要加载的文件,不再只认 PATH。trap新增-P选项,打印每个信号当前绑定的动作,调试信号处理时省事不少。
一批实用的零碎改进
- 新增两个动态变量:
BASH_MONOSECONDS提供单调时钟秒数,BASH_TRAPSIG保存当前 trap 对应的信号编号。 - 命令计时的精度从 3 位小数提高到最多 6 位。
- 多协程选项
MULTIPLE_COPROCS现在默认开启。 - 新增可加载内建命令
kv和strptime。 - 新增
bash_source_fullpath选项,让 Bash 把完整路径写进BASH_SOURCE。 umask内建命令补全了 POSIX 一致性所需的特性。- 当
[[ ]]里用的正则表达式编译失败时,现在会打印错误信息,而不是默默判假。
升级前要留意的地方
- posix 模式下,
test的<和>现在按字符串比较,不再有别的含义。 - 源码标准升级到 C23,K&R 风格的编译器已经编译不了 Bash。
- 检查脚本文件是否为二进制时,如果首行以
#!开头,会先看前两行再判断。
怎么升级
Bash 5.3 不会立刻进入各发行版仓库,想尝鲜得从源码编译。
# 下载源码包
wget http://ftp.gnu.org/gnu/bash/bash-5.3.tar.gz
tar -xzf bash-5.3.tar.gz
cd bash-5.3
# 配置并编译,需要 gcc、make 等构建工具
./configure
make
# 安装到系统目录,生产机请先在测试环境验证
sudo make install
# 确认版本
bash --version建议不要直接替换线上机器的默认 Shell。新版本先进测试机,跑一遍现有脚本,确认无误再考虑铺开。
十分钟写下第一个脚本
先确认环境
# 看看当前登录用的是哪个 Shell
echo "$SHELL"
# 看 Bash 的版本号
bash --version
# 系统里装着哪些 Shell
cat /etc/shells第一个脚本
#!/usr/bin/env bash
# 上面这行叫 shebang,告诉系统用哪个解释器来执行这个文件
echo "你好,Shell"
echo "今天是 $(date +%F)"给它加上执行权限再运行:
chmod +x hello.sh
./hello.sh一条可靠的脚本骨架
脚本能不能长期维护,往往在开头几行就决定了。下面这份骨架可以直接拿去改成自己的脚本。
#!/usr/bin/env bash
#
# 脚本名:backup.sh
# 功能:把指定目录打包备份到目标目录
# 用法:./backup.sh 源目录 目标目录
set -euo pipefail
# -e 命令返回非零就立即退出,不再往下跑
# -u 使用未定义变量时报错,顺手抓出拼写错误
# -o pipefail 管道中任一命令失败,整条管道都算失败
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log() {
# 日志统一写到标准错误,避免污染正常输出
echo "[$(date +'%F %T')] $*" >&2
}
if [[ $# -ne 2 ]]; then
log "用法:$0 源目录 目标目录"
exit 64
fi
src="$1"
dst="$2"
if [[ ! -d "$src" ]]; then
log "源目录不存在:$src"
exit 66
fi
mkdir -p "$dst"
tar -czf "$dst/backup-$(date +%Y%m%d-%H%M%S).tar.gz" -C "$(dirname "$src")" "$(basename "$src")"
log "备份完成,输出到 $dst"出错时记得收尾
脚本中途退出,临时文件不该留在磁盘上。trap 就是干这个的。
#!/usr/bin/env bash
set -euo pipefail
# 临时文件用完就删,脚本中途退出也能清理
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
# 收到中断信号时给出提示,并用 130 退出
trap 'echo "已取消" >&2; exit 130' INT
echo "临时文件是:$tmp"核心语法速通
变量与引用
name="世界"
greeting="你好,${name}"
# 双引号里的变量会被展开,单引号里原样保留
echo "$greeting"
echo '${name} 在单引号里不会被展开'引用是 Shell 里最容易出错的地方。变量不加引号会被拆词、被通配符展开,路径里一旦有空格就崩。养成习惯:只要有变量,就加双引号。例外是 [[ ]] 内部不需要,以及 $* 和 $@ 这类本身有特殊含义的写法。
特殊变量速查
echo "脚本名:$0"
echo "参数个数:$#"
echo "第一个参数:$1"
echo "上一条命令的退出码:$?"
# "$*" 把所有参数拼成一个字符串,"$@" 保持每个参数独立完整
for arg in "$@"; do
echo "参数:$arg"
done退出码是 Shell 的命脉:0 表示成功,非 0 表示失败,if、&&、|| 全都靠它判断。命令替换用 $(),把一条命令的输出塞进变量:
today=$(date +%Y-%m-%d)
filecount=$(ls -1 | wc -l)
echo "今天是 $today,目录里有 $filecount 个条目"参数展开:内置的字符串工具箱
Shell 自带一套完整的字符串操作语法,不用调用任何外部命令:
| 写法 | 作用 |
|---|---|
| ${var:-默认值} | var 为空时使用默认值 |
| ${var:?提示} | var 为空时报错并退出 |
| ${var#前缀} | 从头删除最短匹配 |
| ${var##前缀} | 从头删除最长匹配 |
| ${var%后缀} | 从尾删除最短匹配 |
| ${var%%后缀} | 从尾删除最长匹配 |
| ${var/旧/新} | 替换第一个匹配 |
| ${var//旧/新} | 替换全部匹配 |
| ${#var} | 取字符串长度 |
实战演示,处理文件名最常用:
filename="report-2026.tar.gz"
echo "${filename#*-}" # 2026.tar.gz,删掉最短的 xxx- 前缀
echo "${filename%.tar.gz}" # report-2026,删掉 .tar.gz 后缀
echo "${filename%.*}" # report-2026.tar,只删最后一个扩展名
echo "${filename//./_}" # report-2026_tar_gz,点全部换成下划线
echo "${#filename}" # 18,字符串长度算术运算用双圆括号,注意和命令替换 $() 是两回事:
count=10
count=$((count + 1)) # 11
echo $(((3 + 4) * 2)) # 14条件判断
bash 的条件判断首选双方括号 [[ ]]:不做单词拆分、支持模式匹配,文件名带空格也不会出事:
if [[ -f "$file" ]]; then
echo "是个普通文件"
elif [[ -d "$file" ]]; then
echo "是个目录"
else
echo "不存在"
fi常用测试运算符记几个就够:-f 存在且是文件、-d 存在且是目录、-z 字符串为空、-n 字符串非空、-e 存在。短路与 &&、或 || 是单行版的 if:
[[ -d "$dir" ]] || mkdir -p -- "$dir" # 目录不存在就创建
cd "$dir" && ls -la判断条件有三套写法,各有归属。
| 写法 | 归属 | 特点 |
|---|---|---|
| test | POSIX | 最原始,结果通过退出码返回 |
| [ ] | POSIX | test 的别名,字符串相等用单个等号 |
| [[ ]] | bash 扩展 | 支持正则与通配符匹配,不对变量拆词 |
写 bash 脚本时优先用 [[ ]]:它更安全,也不容易因为变量为空而报语法错。
循环
数字循环、逐行读文件、多分支匹配,三种套路覆盖九成场景:
# 数字序列循环
for i in {1..5}; do
echo "第 $i 轮"
done
# 用通配符展开遍历文件
for f in /var/log/*.log; do
echo "日志文件:$f"
done
# 逐行读文件:IFS= 保住行首空格,-r 防反斜杠转义,缺一不可
while IFS= read -r line; do
echo "行内容:$line"
done < /etc/hostscase 天生适合写命令子命令:
case "$1" in
start) echo "启动服务" ;;
stop) echo "停止服务" ;;
restart) echo "重启服务" ;;
*) echo "用法:$0 start 或 stop 或 restart"; exit 1 ;;
esac函数与参数
# 函数定义
deploy() {
local service="$1" # local 限定作用域,避免污染全局
local version="${2:-latest}" # 第二个参数没传时默认取 latest
echo "准备部署 $service,版本 $version"
return 0
}
deploy nginx 1.27
deploy redisShell 变量默认全局,函数里忘了 local 就会污染外面的世界:
backup_one() {
local src=$1
local dest=$2
cp -- "$src" "$dest" || return 1
echo "已备份:$src"
}
backup_one "a.txt" "backup/a.txt" || echo "备份失败" >&2把前面的知识拼起来,就是一个可以直接用的批量备份脚本:
#!/usr/bin/env bash
# 把源目录下的 txt 文件备份到目标目录,报告成功个数
set -euo pipefail
src=${1:?用法:$0 源目录 目标目录}
dest=${2:?缺少目标目录}
mkdir -p -- "$dest"
count=0
for f in "$src"/*.txt; do
[[ -e "$f" ]] || continue # 一个 txt 都没有时跳过字面量
cp -- "$f" "$dest/"
count=$((count + 1))
done
echo "共备份 $count 个文件"数组
servers=("web01" "web02" "web03")
echo "${servers[0]}" # 第一个元素
echo "${#servers[@]}" # 元素个数
echo "${servers[@]}" # 全部元素
# 关联数组,键可以是字符串
declare -A ports=([http]=80 [https]=443)
echo "${ports[https]}"管道、重定向与健壮性开关
三条数据通道
每个命令生来有三个通道:标准输入(0)、标准输出(1)、标准错误(2)。管道把前一个命令的输出接到后一个命令的输入,重定向把它们接到文件:
./job.sh > run.log 2>&1 # 输出和报错都写进 run.log,顺序不能颠倒
./job.sh 2> errors.log # 只收集报错
./job.sh > /dev/null 2>&1 # 全部丢弃
./job.sh | tee run.log # 屏幕和文件同时写2>&1 必须写在 > run.log 之后——它的含义是“把通道 2 指向通道 1 现在指向的地方”,顺序反了报错就漏出去了。
set -euo pipefail 与 trap
开头那行健壮性开关拆开看:
set -euo pipefail
# -e:任何命令失败立即退出,不再硬着头皮往下跑
# -u:引用未定义的变量立即报错,抓住拼写错误
# -o pipefail:管道里任何一环失败,整条管道就算失败默认情况下管道只看最后一环——grep foo big.log | head 里 grep 崩了,只要 head 正常,退出码照样是 0,事故就这么被吞掉。pipefail 专治这个。
临时文件用 trap 收尾,无论脚本怎么死都会执行清理:
tmpdir=$(mktemp -d)
cleanup() {
rm -rf -- "$tmpdir"
}
trap cleanup EXIT # 正常结束、报错退出、Ctrl+C,退出前都会触发
data="$tmpdir/data.csv"
echo "临时文件写到 $data"这两招加上引号纪律,就是 Shell 脚本从“能跑”到“可靠”的全部秘密。
POSIX.1-2024 改了 sh 的哪部分
2024 年 6 月发布的 POSIX.1-2024(第 8 版)是 POSIX 标准四年一次的大更新,Shell 语言部分有三处值得注意:
$'...'美元单引号写法正式标准化(bash 与 zsh 早就支持,现在是标准正文),单引号里的转义序列如换行符有了可移植写法。timeout、realpath、readlink、gettext系列进入必配工具清单——从这一版起,在跨平台脚本里放心用timeout 300 ./job.sh防挂死是符合标准的。- 记号识别规则收紧,
[[与]]被列入保留字但行为“结果未规定”——翻译成人话:跨平台 sh 脚本里依然别用双方括号,它不是标准语法。
写可移植脚本的姿势没变:目标环境按最老的算,bash 专属语法要么别用,要么老老实实声明 #!/usr/bin/env bash。
从入门到精通的学习路线
“精通”这个词听着唬人,拆开看其实是一层一层垒起来的台阶。
| 阶段 | 目标 | 建议投入 |
|---|---|---|
| 入门 | 变量、引号、流程控制,能写几十行的日常小脚本 | 一到两周 |
| 进阶 | 吃透参数展开、管道重定向、set -euo pipefail 与 trap | 三到四周 |
| 工程化 | ShellCheck 全绿、shfmt 统一格式、bats 写测试、公共函数抽库 | 一到两个月 |
| 精通 | 精读 man bash 的展开章节,掌握进程替换与协程,徒手写可靠安装脚本 | 长期 |
第一阶段:把命令用顺
目标是看懂别人的脚本,自己也能写出能跑的脚本。重点是这几块:
- 引号:什么时候必须加双引号,什么时候用单引号。
- 重定向:
>、>>、2>、&>、<,还有 here document。 - 管道:
|与|&,以及用PIPESTATUS检查管道中每一段的退出码。 - 命令替换、变量展开、通配符展开的时机与顺序。
- 常用工具:grep、sed、awk、find、xargs、cut、sort、uniq。
第二阶段:写得对
能跑只是及格线,接下来要让它可靠、可读、可维护。
- 严格模式:
set -euo pipefail。 - 用 shellcheck 做静态检查,把常见写法错误消灭在上线前。
- 用 shfmt 统一缩进与换行,团队协作时省掉大量无意义的格式争论。
- 参考 Google 的风格指南,把命名、注释、错误输出的习惯固定下来。
第三阶段:扛得住
这一步是从“会写脚本”走向“能维护系统”的分界线。
- 用 bats-core 给关键脚本写测试,尤其是涉及删除、部署、权限修改的脚本。
- 在 CI 里串起 shellcheck、shfmt、bats 三道检查。
- 用
trap处理清理和信号,保证脚本被打断时状态可控。 - 设计好退出码和日志格式,让调用方和值班的人都能看懂。
第四阶段:读懂别人的脚本
GitHub 上有大量高质量的 Shell 脚本可供参考,比如 awesome-shell 和 awesome-bash 这两个精选列表整理了框架、工具、教程和实战项目。读别人的脚本时,重点看两件事:他们怎么组织代码结构,以及他们怎么处理错误和边界情况。
用 ShellCheck 和 shfmt 守住代码质量
ShellCheck 静态检查
# 安装,不同平台命令不同
sudo apt install shellcheck # Debian 或 Ubuntu
brew install shellcheck # macOS
# 直接检查一个脚本
shellcheck backup.sh
# 脚本以 sh 开头时,指定方言
shellcheck -s sh legacy.sh
# 只报告严重级别较高的告警
shellcheck -S warning backup.sh
# 允许跟随 source 进来的文件一起分析
shellcheck -x lib/common.shShellCheck 0.11.0 于 2025 年 8 月发布,新增了 SC2327 到 SC2332 一批规则。比如 SC2327 会警告 var=$(cmd > file) 这种“捕获了重定向输出、结果永远是空”的写法,SC2329 会提示从未被调用的函数。这一版还跟进了 Bash 5.3,认得新的命令替换形式和 source -p。另外,被吐槽多年的 SC2002(无用 cat)从这一版起默认不再提示,需要 --enable=useless-use-of-cat 才能打开。
遇到确实需要保留的写法,可以在脚本里定向关闭某条规则。
# 在单独一行关掉某条规则,作用于下一行
# shellcheck disable=SC2086
echo $unquoted_var
# 也可以在文件开头声明,整份文件生效
# shellcheck disable=SC1091shfmt 统一格式
# 用 Go 安装最新版本
go install mvdan.cc/sh/v3/cmd/shfmt@latest
# 格式化并直接写回文件
shfmt -w backup.sh
# 用两空格缩进
shfmt -i 2 -w backup.sh
# 只列出格式不合规的文件,适合放进流水线做检查
shfmt -l .shfmt 是 mvdan/sh 项目的一部分,能按统一风格重排缩进、空格和换行,还支持 --simplify 简化冗余写法、--minify 压缩体积。它和主流编辑器集成之后,可以做到保存即格式化。
编辑器与流水线
VS Code 里装上 shell-format 和 shellcheck 插件,保存时自动格式化加检查,问题基本当场就能看见。CI 里则通常是这样一条检查链。
# 静态检查,-x 允许跟随 source 的文件
shellcheck -x scripts/*.sh
# 只报告格式差异,不修改文件
shfmt -d -l scripts/
# 跑测试用例
bats tests/*.bats给脚本写测试:bats-core
bats-core 是 Bash Automated Testing System,用 Bash 就能写测试,输出兼容 TAP,可以直接接进各类 CI 平台。2025 年 5 月发布的 v1.12.0 增加了 bats::on_failure 钩子,测试或 setup 失败时可以统一收集现场信息,排查跨环境问题比以前方便。
#!/usr/bin/env bats
# 文件名:backup.bats
setup() {
# 每个用例执行前都会运行,这里准备一个临时目录
TEST_DIR="$(mktemp -d)"
}
teardown() {
# 用例结束后清理现场
rm -rf "$TEST_DIR"
}
@test "缺少参数时返回用法错误" {
run ./backup.sh
[ "$status" -eq 64 ]
[[ "$output" == *"用法"* ]]
}
@test "源目录不存在时给出提示" {
run ./backup.sh /no/such/dir "$TEST_DIR"
[ "$status" -eq 66 ]
}测试里用 run 执行被测命令,结果会分别落在 $status 和 $output 里,判断起来和写普通 Shell 判断没什么区别。对一段会删除文件的清理脚本来说,这点测试投入非常划算。
新手最容易踩的坑
- 变量不加引号。
for f in $(ls)遇到带空格的文件名直接乱套,rm $file更可能删错东西。遍历文件用 glob:for f in ./*,引用一律"$f"。 - 把 bash 语法写进 #!/bin/sh 脚本。
[[ ]]、==、source、数组、{1..5}、set -o pipefail全是 bash 专属。dash 一接手,报错报得莫名其妙。跨平台脚本按 POSIX 最小集写。 - read 忘了 -r 和 IFS=。 少了它们,行首空格丢失、反斜杠变成转义符。逐行读文件的固定咒语是
while IFS= read -r line。 - set -e 不是万能保险。
if条件里的命令、&&与||链中的命令失败不会触发退出,管道默认只看最后一环。关键步骤自己检查退出码。 - rm -rf "$dir"/* 的空变量灾难。 dir 为空时这条命令变成删除根目录下所有内容。防御写法:
rm -rf -- "${dir:?dir 未设置}"/*,变量为空当场报错。 - Windows 编辑器带来的 CRLF。 在 Windows 记事本里写的脚本,每行行尾带
\r,Linux 上报command not found且找不到原因。编辑器切 LF 行尾,或sed -i 's/\r$//' script.sh一键修复。 - cd 不检查返回值。 目录不存在时 cd 失败,脚本继续在原目录里执行后续命令,文件写错地方。固定写法
cd "$dir" || exit 1。 - 函数里忘了 local。 Shell 变量默认全局,函数里的临时变量会把外面的同名变量悄悄改掉。
- 长任务不挂 timeout。 网络请求、子进程可能永远不返回。POSIX.1-2024 起 timeout 已是标配工具:
timeout 300 ./job.sh。 - 在脚本里依赖交互式配置。 alias 与部分环境变量只在交互 Shell 生效,cron 与 CI 环境里一个都不存在。脚本开头显式设置所有依赖的变量。
工具链与学习资料
官方文档与手册
- Bash 参考手册,语法细节的最终依据:Bash Reference Manual
- GNU Bash 项目主页,下载源码和最新补丁:GNU Bash
- Bash 5.3 发布公告,原文见 GNU 邮件列表:bug-bash 邮件列表归档
- POSIX.1-2024 的 Shell 与实用程序卷,判断某个写法是否可移植的标准答案:POSIX.1-2024
风格指南与最佳实践
- Google 发布的 Shell 风格指南,团队协作时很值得参考:Google Shell Style Guide
- 系统讲解 Bash 概念与陷阱的社区指南:BashGuide
- 常见问题合集,几乎每个坑都能在这里找到答案:BashFAQ
- 用纯 Bash 内建实现常见任务的技巧集:pure-bash-bible
常用工具
- 静态检查:ShellCheck 仓库 与 ShellCheck 官网
- 格式化:mvdan/sh
- 测试框架:bats-core
- 把一条命令逐段拆解讲解的在线工具:explainshell
书与长文
- 《Linux 命令行大全》适合零基础起步,把命令行的用法讲得很系统。
- 《bash Cookbook》偏向实战菜谱,遇到具体问题可以直接查。
- 免费的英文电子书 The Linux Command Line,从命令行讲到脚本,是零基础最顺的路径:linuxcommand.org
- 老牌的 Advanced Bash-Scripting Guide 内容仍然完整,但更新已经停滞多年,里面的例子要注意对照新版手册。
一个实用的建议:别把资料收藏当学习。挑一个自己每天都在重复的操作,用脚本把它自动化掉,遇到卡点再去查手册,这样学得最快。
和 Python 比,什么时候该用 Shell
这两个常被放在一起比较,答案其实取决于任务性质。
| 任务类型 | 更合适的选择 |
|---|---|
| 调用现成命令做流程编排 | Shell |
| 部署、备份、日志清理 | Shell |
| 管道式文本过滤与统计 | Shell |
| 复杂数据结构与算法 | Python |
| 需要单元测试与类型检查 | Python |
| 网络请求与 API 集成 | Python |
| 超过几百行且逻辑复杂 | Python |
Google 的风格指南里有一条很直白的建议:脚本一旦超过一百行,或者用上了不直观的控制流,就该考虑换成更结构化的语言。Shell 的强项是调用命令,弱项是数据结构和错误处理,认清这条边界,选择就不难了。
Shell 是胶水,不是砖头。用它把现成的工具粘起来,而不是拿它盖楼。
写在最后
Shell 是一门“看起来简单、写对很难”的语言。它的坑大多不在语法本身,而在引号、退出码、子进程这些看不见的细节里——一段二十行的清理脚本,破坏范围可能远大于几百行的报表脚本。
Shell 脚本的“精通”不是背下多少语法,而是三件事:引号纪律刻进肌肉记忆、退出码链条想得比命令本身多一层、每个脚本默认带上 set -euo pipefail 与 trap。2025 年的 Bash 5.3 也没有加什么花哨概念,反而在命令替换去 fork、补全排序、计时精度这些地方做了改进。对一门诞生快五十年的语言来说,这种克制的更新节奏挺难得。
如果你刚开始学,建议先把两件事变成肌肉记忆:脚本开头写 set -euo pipefail,写完立刻过一遍 shellcheck。至于“精通”,那通常是从某件非做不可的重复劳动开始的。