关于 bash-shortening
用于缩短 Bash 脚本并保持可读性的 Skill,结合分类参考、只读分析和 Python 改写器处理部分模式,适合审查或简化现有 Bash 代码。
主要功能
- 01按九个类别分析脚本
- 02用 Python 脚本改写指定模式
- 03以统一差异预览更改
- 04使用 bash -n 和 shellcheck 验证修改
适用场景
- 01检查 Bash 脚本的精简机会
- 02对脚本应用自动改写
- 03识别精简后会降低可读性的代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ paulnsorensen/ skillz-that-grillz --skill bash-shortening安装后在 Claude Code 中输入/bash-shortening确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdevals/evals.jsonreferences/advanced.mdanti-patterns.mdarithmetic.mdbrace-expansion.mdcommand-substitution.mdfunctions.mdparameter-expansion.mdprocess-substitution.mdreal-world.mdscripts/bash-shorten.pyengine.pyrules.pysgconfig.ymlscripts/sg-rules/backticks.ymltest-numeric-eq.ymltest-numeric-ge.ymltest-numeric-gt.ymltest-numeric-le.ymltest-numeric-lt.ymltest-numeric-ne.yml详细介绍
bash-shortening
缩短 Bash 脚本,同时不降低可读性。来源:Karandeep Singh 的《Bash Code Shortening》文章中的 51 种技巧,在这里整理为一份速查表和分类参考资料。
理念
缩短代码的目的是表达意图,而不是省敲几下键盘。好处在于减少变动因素(子进程、临时文件、中间变量),而不是让每行代码更密集。创建并删除临时文件的 7 行脚本,不如 1 行管道;晦涩的 ${1:-${X:-${Y/-,/ }}} 链,也不如清晰的 3 行代码。
每次判断都依据两条经验法则:
- 缩短代码能否消除某一类错误?(例如忘记对临时文件执行
rm、嵌套if中遗漏分支、在mkdir -p前检查目录时发生竞争条件。)如果能,就缩短。 - **缩短代码是否会让下一位读者难以理解其意图?**如果会,就展开。
两条法则可能同时适用——发生冲突时,优先保证清晰。长篇说明见 references/anti-patterns.md。
如何使用此技能
**架构:工作者负责收集,主线程负责编写。**只读分析工作者并行运行,收集违规项。随后主线程作为唯一写入者,运行脚本、验证结果,并手动编辑其余内容。这样可以避免并行写入同一文件(用户担心的丢失更新竞争条件)。
- **确定目标。**文件路径、粘贴的代码片段,或用
bash围栏标记的代码块。确认目标确实是 Bash(参见“何时不应使用此技能”)。 - **启动分析扫描。**在一次批量调用中,为每个参考类别启动一个只读分析工作者,让它们并发运行。分析器只报告违规项——不写入、不运行脚本,也不触碰目标。见下文“并行扫描——工作者约定”。
- **合并分析报告。**收集每个分析器报告的
路径:行号 — 冗长写法 → 惯用写法命中项。去除重复项(同一行可能被两个类别标记——只保留一条,并注明两种归类)。 - **在主线程中运行脚本。**运行
python3 scripts/bash-shorten.py --apply <target>(每次调用处理一个文件——有多个文件时逐个循环处理)。由唯一写入者一次性应用高置信度的core规则。绝不要将--apply委派给工作者。 - **验证脚本的输出。**运行
bash -n <target>检查语法;如果可用,运行shellcheck <target>,并查看差异。如果某条规则产生了意外结果,先还原,再通过--rules或--skip缩小范围后重试。 - **列出剩余的待办项。**展示脚本未覆盖的分析器命中项,让用户选择要应用的类别,尤其是在反模式命中项与缩短代码的命中项冲突时。
- **手动编辑其余内容。**每次只应用一种技巧,并说明理由。使用对应参考文件中的
before / after示例。保留引号("$var")、set -euo pipefail和任何错误处理。需要添加注释来解释代码作用的重写,到此为止,不要继续。
不要悄无声息地批量重写。工作者负责收集,主线程负责编写;每项更改都应对应一种技巧,让用户能够从中学习。
自动重写
此技能附带 scripts/bash-shorten.py——一个用于高置信度模式的 Python 重写器。它没有第三方 Python 依赖,但要求 PATH 中有 ast-grep(sg);参见下文的“需要 ast-grep”部分。默认以试运行模式运行(向 stdout 打印统一 diff,并向 stderr 输出各规则的计数),只有传入 --apply 才会写入文件。之后务必对输出运行 shellcheck。
# preview rewrites
python3 scripts/bash-shorten.py path/to/script.sh
# apply (atomic write, preserves the file in place)
python3 scripts/bash-shorten.py --apply path/to/script.sh
# only run a subset of rules
python3 scripts/bash-shorten.py --rules backticks,test-numeric script.sh
# disable a rule
python3 scripts/bash-shorten.py --skip find-exec-rm-delete script.sh
# enable the modernize rule group (off by default — see "Rule groups")
python3 scripts/bash-shorten.py --include modernize --apply script.sh
# explore the ruleset
python3 scripts/bash-shorten.py --list
python3 scripts/bash-shorten.py --explain test-numeric
# verify the rules still work after editing
python3 scripts/bash-shorten.py --self-test
两组规则(core、modernize),每条规则都内置了正向和反向测试样例(运行 --list 查看当前计数)。core 组默认启用;modernize 组需显式启用。大多数 core 规则对应源文章中的示例(sed-replace-first/all、echo-wc-c、cut-c-substring、expr-arith-vars/literal/increment、combined-tests、test-numeric、empty-default、param-default、mkdir-guard、for-range-expansion);其余是文章未提及但效果显而易见的额外核心模式(backticks、legacy-null-check、empty-string-eq、find-exec-rm-delete、cat-file-pipe-grep——其中三种会被 shellcheck 标记,但不会自动修复)。
重写器有意不处理的内容:任何需要数据流分析的情况(内联仅使用一次的变量)、多语句检测(临时文件 → 管道)、提取函数、并行化,或行为判断(“这个 &&/|| 链是否安全?”)。这些都需要参照资料进行手动编辑。
当用户询问“能不能缩短整个脚本?”时,主线程会在只读分析器扫描结束后运行此脚本(见下一节)。脚本负责处理枯燥的 60% 重写;分析器负责发现正则表达式看不到的问题;只有主线程可以写入文件。
并行扫描——工作者约定
单个上下文内的审查者很容易只盯着最先检查的一两个类别,从而漏掉一整类问题(命令替换、参数展开、算术、花括号展开、进程替换、函数、高级模式、真实案例)。解决方法是并行启动只读分析器——每个参考类别一个。它们负责收集违规项,不负责写入。主线程之后运行脚本并进行手动编辑。
工作者严格只读
每个并行工作者都遵循相同约定:读取目标文件、报告违规项,然后返回。 工作者不得写入磁盘、应用重写或运行 scripts/bash-shorten.py --apply。如果任务涉及写入,它就不是工作者的任务,而是主线程的任务。
原因:并行写入同一文件会产生竞态。即使将每个工作者限定在各自的类别内,它们也会针对共享行生成重叠的 diff,主线程仍需手动合并。先收集,再一次性写入。
工作者类型
| 工作者 | 数量 | 职责 | 输出 |
|---|---|---|---|
| 类别分析器 | 每个参考类别一个(默认 9 个) | 阅读 references/<category>.md 并扫描目标文件,查找其覆盖的模式 | Markdown 列表,格式为 path:line — 冗长形式 → 惯用形式,不进行重写 |
给每个类别分析器的简要说明(将 <category> 和 <target> 替换为实际值——除非另有说明,此技能中的路径均相对于技能目录;将待缩短脚本相对于仓库的路径作为 <target> 传入):
阅读
references/<category>.md(相对于 bash-shortening 技能目录)。扫描<target>,找出其中所有符合该文件所涵盖模式的实例。返回 Markdown 列表,格式为:path:line — 冗长形式 → 惯用形式。不要重写文件,不要运行scripts/bash-shorten.py,不要写入任何内容。如果发现的问题与反模式参考资料冲突,请标记出来,但仍要将其列出。
派发规则: 在一次批量调用中启动所有分析器,使其并发运行。顺序派发会失去并行处理带来的延迟优势,而这正是采用扇出方式的全部原因。
不依赖特定 Harness——选择适合的原语
约定是“并行执行 N 个只读任务并收集结果”。 使用 Harness 提供的任意原语——此技能不依赖其中任何一种:
- 并行子 Agent/子任务调用(通过一条批量消息同时启动所有工作进程)。
- 在同一轮助手回复中并行调用多个工具。
- 在 Agent 图框架中使用任务/节点扇出。
- 使用普通 Shell:对工作任务说明使用
xargs -P 9或parallel,并将其传入 Agent CLI;将每个工作进程的标准输出分别捕获到单独文件中。 - 没有可用的并行原语: 一次遍历类别表,按顺序逐项检查。明确勾选每一项——只盯着最先找到的几处而忽略其余内容,正是本节要避免的失败模式。
类别
以下是完整的检查清单——不要在两三个类别中找到匹配项后就停下。只盯着 basename/dirname/for-range,却跳过命令替换、参数展开、算术运算、花括号展开、进程替换、函数、高级模式和真实案例,正是本节要避免的失败模式。
| 类别 | 参考资料 | 文章编号 | 要查找的模式 |
|---|---|---|---|
| 命令替换排查 | references/command-substitution.md | 3-7 | 使用 $(cmd) 消除临时文件、管道与复用输出的对比、xargs 模式、find … | xargs 与进程替换的对比 |
| 参数展开排查 | references/parameter-expansion.md | 8-15 | if [ -z "$X" ]; then X=… 默认值、cut -c/echo | sed 替换、${#S} 长度、basename/dirname Shell 写法 |
| 函数模式排查 | references/functions.md | 16-20 | 重复的日志代码块、默认参数样板代码、用 echo 返回值、命名参数模式 |
| 花括号展开排查 | references/brace-expansion.md | 21-26 | 按顺序执行 mkdir/touch a b c、for i in 1 2 3 4 5 范围、零填充序列 |
| 进程替换排查 | references/process-substitution.md | 27-31 | 将内容传入 diff/循环的临时文件、echo "x" | cmd → <<< 此处字符串 |
| 算术运算排查 | references/arithmetic.md | 32-38 | expr、$(…)+1 递增、[ -gt/-lt/-eq ] 数值测试、[ "$A" ] && [ "$B" ] |
| 真实案例排查 | references/real-world.md | 39-45 | 配置解析、日志分析、运行状况检查、批处理、备份、API + jq |
| 高级模式排查 | references/advanced.md | 48-51 | 重复的 echo 行(→ heredoc)、有 3 个以上分支的 if/elif(→ 关联数组或 case)、按顺序执行的独立命令(→ & … & wait)、循环中的 cut -d,(→ 自定义 IFS) |
| 反模式排查 | references/anti-patterns.md | 1-2, 46-47 | 嵌套展开、晦涩的单行代码、缩短后反而会有害的地方——标记出来供用户处理,不要自动重写 |
合并、应用、验证——单一写入者
所有分析器返回后,主线程负责所有写入操作:
- 收集并去重各类别的命中列表。同一行可能被两个分析器标记——只保留一条记录,并注明两者的归因。
- 从主线程运行重写脚本。对每个文件调用一次
python3 scripts/bash-shorten.py --apply <target>(如果有多个文件则循环调用——脚本只接受一个位置参数)。默认使用core;添加--include modernize可选择启用 sd/rg/fd 重写。这是唯一运行--apply的地方。 - 验证脚本的输出。使用
bash -n <target>检查语法;如果有shellcheck <target>,也运行它;并手动查看差异。如果某条规则产生了意外结果,请先还原,再通过--rules或--skip缩小范围后重试。 - 列出分析器剩余的待处理项——即脚本未覆盖的命中项。让用户选择要应用的类别,尤其是在反模式命中与缩短命中冲突时。
- 应用手动编辑,每次更改只采用一种技巧,并说明理由(遵循上述“如何使用此技能”工作流)。每次编辑都由主线程单独写入;不进行扇出,也不并行执行。
**为什么只设一个写入者:**分析器在检查内容上彼此独立,但它们建议的重写可能会重叠在同一行(例如,for i in 1 2 3 4 5 的同一个命中会同时出现在花括号展开、算术表达式,甚至真实案例类别中)。如果工作线程并行写入,最后写入者会悄无声息地覆盖其他修改。先收集所有违规项,再从一个地方统一应用,可以让差异可审计,并防止丢失更新的错误。
**为什么分析时要扇出:**各类别彼此独立,在工作记忆中需要不同的参考文件;并行工作线程能确保覆盖范围,而单次检查多个文件时很容易漏掉内容。只读任务不会相互竞争,因此这里并行处理没有成本——这正是单一写入者纪律不适用的地方。
规则组
| 组 | 默认状态 | 内容 |
|---|---|---|
core | 开启 | 18 条不改变工具的惯用 Bash 重写规则 |
modernize | 关闭 | sed-replace-to-sd、grep-fixed-to-rg、find-name-to-fd——重写为非 coreutils 二进制工具(sd、rg、fd),用户必须先安装这些工具 |
modernize 规则有意采取保守策略:只有字面量 sed 模式才会映射到 sd;只有 grep -F(固定字符串)才会映射到 rg -F(普通的 grep PAT 使用 BRE,与 rg 的正则语法差异足以导致行为悄然改变);只有不带其他标志的 find . -type f -name "GLOB" 才会映射到 fd(即便如此,find → fd 重写也会改变行为:fd 默认遵循 .gitignore)。
通过 --include modernize 选择启用。安装程序(scripts/install.sh)会安装 sd、ripgrep、fd 和 ast-grep,这样重写后的代码就能运行,且重写器也能获得所需的依赖项。
必须使用 ast-grep
bash-shorten.py 需要将 ast-grep(sg)
添加到 PATH 中。结构化模式(backticks、test-numeric)会先通过 scripts/sg-rules/ 中的规则包使用 ast-grep 处理,
然后再对输出运行其余的正则规则。Tree-sitter 只解析一次 bash,因此上下文相关规则(跳过 # 注释、跳过 here-doc 主体)
无需在正则层中使用临时的后行断言,也能正常工作。
如果找不到 sg,脚本会退出并显示友好的诊断信息。有两种处理方式:
brew install ast-grep # macOS / Linuxbrew
cargo install ast-grep --bin sg
或者完全跳过脚本,只运行并行扫描中的类别分析器——对于没有 ast-grep 的环境,本文件中基于参考的处理方法可作为备用方案。
为什么有些规则仍然只使用正则表达式。
mkdir-guard、empty-default、param-default、expr-increment需要跨元变量相等(两个位置中的名称相同)。YAML ast-grep 规则无法简洁地表达这一约束;Python 正则会改用反向引用。combined-tests——[ ... ]会展平为单词令牌列表,因此对运算符一侧进行结构化匹配很不可靠。for-range-expansion——需要运行时算术来验证捕获的整数是否构成步长为 1 的升序序列;纯 YAML 规则 无法执行此计算。sed-replace-*——字面模式保护需要在匹配器中限制字符类。
test-numeric 之前也在此列表中,但后来作为六条按运算符划分的规则移到了 sg-rules/ 中——tree-sitter-bash 会区分
test_command([ ])和 conditional_expression([[ ]]),因此 sg 形式能正确跳过 [[ ]],而正则会误匹配其中内容
(问题 #18)。
这些仅使用正则表达式的规则仍会在 ast-grep 处理后运行。编写新规则的指南请参阅
AGENTS.md——默认使用 ast-grep,只有在上述限制之一造成阻碍时才退回到正则表达式。
快速优化
最常见的冗余模式,以及完整说明的阅读位置。 括号中的数字是源文章中的示例编号。
| 冗长写法 | 惯用写法 | 参考 |
|---|---|---|
if [ -z "$X" ]; then X=default; fi | X=${X:-default} | parameter-expansion(8) |
$(echo "$S" | cut -c1-5) | ${S:0:5} | parameter-expansion(10) |
$(basename "$P") / $(dirname "$P") | ${P##*/} / ${P%/*} (仅手动修改——遇到末尾带斜杠的路径时会悄悄出错;当路径不含 / 时,dirname 的行为也与 ${P%/*} 不同(真正的 dirname 会返回 .,而参数展开会返回原字符串);重写器不会自动应用此改写) | parameter-expansion(11-12) |
$(echo "$S" | sed 's/a/b/g') | ${S//a/b} | parameter-expansion(13-14) |
$(echo -n "$S" | wc -c) | ${#S} | parameter-expansion(15) |
$(expr $A + $B) / C=$(expr $C + 1) | $((A + B)) / ((C+=1))(或 C=$((C + 1))) | arithmetic(32-34) |
[ $X -gt 100 ] / [ $A ] && [ $B ] | ((X > 100)) / [[ $A && $B ]] | arithmetic(35,37) |
mkdir a; mkdir b; mkdir c | mkdir -p {a,b,c} | brace-expansion(21-22) |
for i in 1 2 3 4 5 | for i in {1..5}(或 {01..10}、{2..10..2}) | brace-expansion(23-26) |
cmd > /tmp/x; cmd2 < /tmp/x; rm /tmp/x | cmd | cmd2 或 cmd2 < <(cmd) | command-substitution(5),process-substitution(29) |
sort a > /tmp/a; sort b > /tmp/b; diff ... | diff <(sort a) <(sort b) | process-substitution(27) |
if [ "$E" = dev ]; elif ... ; fi(3 个或更多分支) | case 或 关联数组 ${URLS[$E]:-default} | functions(简介),advanced(49) |
重复使用 echo "[$(date)] [LEVEL] msg" | 使用带有 ${1^^} 的 log() 函数 | functions(16) |
find ... > /tmp/x; while read; ...; done < /tmp/x; rm | find ... | xargs cmd 或 done < <(find ...) | command-substitution(6),process-substitution(29) |
多行 echo "...",重复 N 次 | cat <<EOF ... EOF here-doc | advanced(48) |
cmd1; cmd2; cmd3(顺序执行、彼此独立) | cmd1 & cmd2 & cmd3 & wait | advanced(50) |
循环内使用 cut -d, -f1,2,3 | while IFS=, read -r a b c | advanced(51) |
如果这里没有列出用户的模式,请搜索下方的参考索引—— 文章中的每个示例都已保留。
参考索引
阅读与相应技巧类别匹配的文件。每个文件都包含源文章中的完整 改写前后示例,以及值得了解的注意事项。
| 文件 | 涵盖内容 | 示例 |
|---|---|---|
references/command-substitution.md | 管道、xargs、移除临时文件、命令输出的一次性使用与重复使用 | 3-7 |
references/parameter-expansion.md | 默认值、替代值、子字符串、路径提取、替换、长度 | 8-15 |
references/functions.md | 日志记录、默认参数、内联条件语句、通过 echo 返回值、命名参数 | 16-20 |
references/brace-expansion.md | 目录/文件展开、数字和字符序列、步进、零填充 | 21-26 |
references/process-substitution.md | <(cmd)、>(cmd)、here-string(<<<)、将命令输出传入循环 | 27-31 |
references/arithmetic.md | $(( ))、(( ))、[[ ]]、比较运算符、三元运算符注意事项 | 32-38 |
references/real-world.md | 配置解析、日志分析、健康检查、批处理、备份、用户管理、API + jq | 39-45 |
references/anti-patterns.md | 何时不应缩短——难以理解的单行命令、嵌套展开,以及相关理念 | 1-2,46-47 |
references/advanced.md | Here-doc、关联数组、并行执行 + wait、用于 CSV 的自定义 IFS | 48-51 |
不适合使用此技能的情况
- 非 bash shell。 大多数参数扩展(
${var//x/y}、${var:offset:length})、[[ ]]、数组和进程替换都是 bash 特有语法。如果脚本的 shebang 是#!/bin/sh、#!/usr/bin/env dash, 或目标 shell 是posh/Alpineash/busybox,请切换注册项或拒绝处理并说明原因。fish和zsh有各自的语法——这里的内容都不适用。 - 单行代码极限压缩。 如果用户明确要求为代码高尔夫挑战写出尽可能短的代码行,那么牺牲可读性来缩短代码是目标,而不是缺陷——但要指出可读性方面的代价,让用户自行决定。
- 关键基础设施脚本。 启动脚本、初始化脚本,以及在日志配置完成前运行的脚本,越平淡可靠越好。不要为了优雅而牺牲在凌晨 3 点故障期间运行的代码的清晰度。
- 要求兼容 POSIX。 如果脚本以
#!/bin/sh形式发布,用于跨发行版安装脚本,请遵循 POSIX。反模式参考中有一份可移植性检查清单。
不要做的事
- 不要一次性重写整个文件——每次只改进一种技巧,并清楚说明理由。
- 不要仅仅为了缩短代码而引入新依赖(
yq、jq、parallel)。可以提出建议,但只有用户同意后才能采用。 - 缩短代码时,不要删除注释或
set -euo pipefail——它们至关重要。 - 没有测量就不要声称重写后速度更快。消除子进程通常能提升速度,但应说“应该会更快”,不要说“快了 5 倍”,除非你对两者都运行过
time。
审查时要留意的常见错误
当 LLM(或赶时间的人)试图缩短 bash 代码时,常会出现以下问题:
- 重写代码中未加引号的
$var。 缩短代码绝不能省略引号;分词错误比代码冗长更糟。 - 算术三元表达式返回字符串。
$((C > 10 ? "high" : "low"))不可行——bash 算术表达式只能处理整数。应使用[[ $C -gt 10 ]] && S=high || S=low或case。(来源文章的示例 36 有此错误;算术参考也标出了这一点。) - 把
&& ... || ...当作 if-then-else 使用。 只有在第一个分支不可能失败时才安全。如果第一个命令在成功时也有可能返回非零值,||分支仍会执行。对于非简单分支,请使用if/else。 mkdir未使用-p。 缩短代码时会移除存在性检查,因此-p标志是确保重写安全的关键。不要两者都删掉。xargs未使用-r或-0。 输入为空或文件名中含空格都会导致xargs出错。请使用-r(输入为空时不运行),并搭配find -print0使用-0,以确保路径安全。文章没有提到这一点;审查时请指出。
来源
51 种技巧,来源: https://karandeepsingh.ca/posts/bash-code-shortening-techniques/,作者为 Karandeep Singh(2023 年)。来源文章中的每个编号示例都保留在参考资料中——数量和编号与原文一致。