mcpField
B
bash-shortening

bash-shortening

github.com/paulnsorensen开发156 次安装今天更新
代码仓库

关于 bash-shortening

用于缩短 Bash 脚本并保持可读性的 Skill,结合分类参考、只读分析和 Python 改写器处理部分模式,适合审查或简化现有 Bash 代码。

主要功能

  1. 01按九个类别分析脚本
  2. 02用 Python 脚本改写指定模式
  3. 03以统一差异预览更改
  4. 04使用 bash -n 和 shellcheck 验证修改

适用场景

  1. 01检查 Bash 脚本的精简机会
  2. 02对脚本应用自动改写
  3. 03识别精简后会降低可读性的代码

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/paulnsorensen/skillz-that-grillz --skill bash-shortening

安装后在 Claude Code 中输入/bash-shortening确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

AGENTS.md
evals/
evals.json
references/
advanced.md
anti-patterns.md
arithmetic.md
brace-expansion.md
command-substitution.md
functions.md
parameter-expansion.md
process-substitution.md
real-world.md
scripts/
bash-shorten.py
engine.py
rules.py
sgconfig.yml
scripts/sg-rules/
backticks.yml
test-numeric-eq.yml
test-numeric-ge.yml
test-numeric-gt.yml
test-numeric-le.yml
test-numeric-lt.yml
test-numeric-ne.yml

详细介绍

bash-shortening

缩短 Bash 脚本,同时不降低可读性。来源:Karandeep Singh 的《Bash Code Shortening》文章中的 51 种技巧,在这里整理为一份速查表和分类参考资料。

理念

缩短代码的目的是表达意图,而不是省敲几下键盘。好处在于减少变动因素(子进程、临时文件、中间变量),而不是让每行代码更密集。创建并删除临时文件的 7 行脚本,不如 1 行管道;晦涩的 ${1:-${X:-${Y/-,/ }}} 链,也不如清晰的 3 行代码。

每次判断都依据两条经验法则:

  1. 缩短代码能否消除某一类错误?(例如忘记对临时文件执行 rm、嵌套 if 中遗漏分支、在 mkdir -p 前检查目录时发生竞争条件。)如果能,就缩短。
  2. **缩短代码是否会让下一位读者难以理解其意图?**如果会,就展开。

两条法则可能同时适用——发生冲突时,优先保证清晰。长篇说明见 references/anti-patterns.md。

如何使用此技能

**架构:工作者负责收集,主线程负责编写。**只读分析工作者并行运行,收集违规项。随后主线程作为唯一写入者,运行脚本、验证结果,并手动编辑其余内容。这样可以避免并行写入同一文件(用户担心的丢失更新竞争条件)。

  1. **确定目标。**文件路径、粘贴的代码片段,或用 bash 围栏标记的代码块。确认目标确实是 Bash(参见“何时不应使用此技能”)。
  2. **启动分析扫描。**在一次批量调用中,为每个参考类别启动一个只读分析工作者,让它们并发运行。分析器只报告违规项——不写入、不运行脚本,也不触碰目标。见下文“并行扫描——工作者约定”。
  3. **合并分析报告。**收集每个分析器报告的 路径:行号 — 冗长写法 → 惯用写法 命中项。去除重复项(同一行可能被两个类别标记——只保留一条,并注明两种归类)。
  4. **在主线程中运行脚本。**运行 python3 scripts/bash-shorten.py --apply <target>(每次调用处理一个文件——有多个文件时逐个循环处理)。由唯一写入者一次性应用高置信度的 core 规则。绝不要将 --apply 委派给工作者。
  5. **验证脚本的输出。**运行 bash -n <target> 检查语法;如果可用,运行 shellcheck <target>,并查看差异。如果某条规则产生了意外结果,先还原,再通过 --rules 或 --skip 缩小范围后重试。
  6. **列出剩余的待办项。**展示脚本未覆盖的分析器命中项,让用户选择要应用的类别,尤其是在反模式命中项与缩短代码的命中项冲突时。
  7. **手动编辑其余内容。**每次只应用一种技巧,并说明理由。使用对应参考文件中的 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.md3-7使用 $(cmd) 消除临时文件、管道与复用输出的对比、xargs 模式、find … | xargs 与进程替换的对比
参数展开排查references/parameter-expansion.md8-15if [ -z "$X" ]; then X=… 默认值、cut -c/echo | sed 替换、${#S} 长度、basename/dirname Shell 写法
函数模式排查references/functions.md16-20重复的日志代码块、默认参数样板代码、用 echo 返回值、命名参数模式
花括号展开排查references/brace-expansion.md21-26按顺序执行 mkdir/touch a b c、for i in 1 2 3 4 5 范围、零填充序列
进程替换排查references/process-substitution.md27-31将内容传入 diff/循环的临时文件、echo "x" | cmd → <<< 此处字符串
算术运算排查references/arithmetic.md32-38expr、$(…)+1 递增、[ -gt/-lt/-eq ] 数值测试、[ "$A" ] && [ "$B" ]
真实案例排查references/real-world.md39-45配置解析、日志分析、运行状况检查、批处理、备份、API + jq
高级模式排查references/advanced.md48-51重复的 echo 行(→ heredoc)、有 3 个以上分支的 if/elif(→ 关联数组或 case)、按顺序执行的独立命令(→ & … & wait)、循环中的 cut -d,(→ 自定义 IFS)
反模式排查references/anti-patterns.md1-2, 46-47嵌套展开、晦涩的单行代码、缩短后反而会有害的地方——标记出来供用户处理,不要自动重写
合并、应用、验证——单一写入者

所有分析器返回后,主线程负责所有写入操作:

  1. 收集并去重各类别的命中列表。同一行可能被两个分析器标记——只保留一条记录,并注明两者的归因。
  2. 从主线程运行重写脚本。对每个文件调用一次 python3 scripts/bash-shorten.py --apply <target>(如果有多个文件则循环调用——脚本只接受一个位置参数)。默认使用 core;添加 --include modernize 可选择启用 sd/rg/fd 重写。这是唯一运行 --apply 的地方。
  3. 验证脚本的输出。使用 bash -n <target> 检查语法;如果有 shellcheck <target>,也运行它;并手动查看差异。如果某条规则产生了意外结果,请先还原,再通过 --rules 或 --skip 缩小范围后重试。
  4. 列出分析器剩余的待处理项——即脚本未覆盖的命中项。让用户选择要应用的类别,尤其是在反模式命中与缩短命中冲突时。
  5. 应用手动编辑,每次更改只采用一种技巧,并说明理由(遵循上述“如何使用此技能”工作流)。每次编辑都由主线程单独写入;不进行扇出,也不并行执行。

**为什么只设一个写入者:**分析器在检查内容上彼此独立,但它们建议的重写可能会重叠在同一行(例如,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; fiX=${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 cmkdir -p {a,b,c}brace-expansion(21-22)
for i in 1 2 3 4 5for i in {1..5}(或 {01..10}、{2..10..2})brace-expansion(23-26)
cmd > /tmp/x; cmd2 < /tmp/x; rm /tmp/xcmd | 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; rmfind ... | xargs cmd 或 done < <(find ...)command-substitution(6),process-substitution(29)
多行 echo "...",重复 N 次cat <<EOF ... EOF here-docadvanced(48)
cmd1; cmd2; cmd3(顺序执行、彼此独立)cmd1 & cmd2 & cmd3 & waitadvanced(50)
循环内使用 cut -d, -f1,2,3while IFS=, read -r a b cadvanced(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 + jq39-45
references/anti-patterns.md何时不应缩短——难以理解的单行命令、嵌套展开,以及相关理念1-2,46-47
references/advanced.mdHere-doc、关联数组、并行执行 + wait、用于 CSV 的自定义 IFS48-51

不适合使用此技能的情况

  • 非 bash shell。 大多数参数扩展(${var//x/y}、 ${var:offset:length})、[[ ]]、数组和进程替换都是 bash 特有语法。如果脚本的 shebang 是 #!/bin/sh、#!/usr/bin/env dash, 或目标 shell 是 posh/Alpine ash/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 年)。来源文章中的每个编号示例都保留在参考资料中——数量和编号与原文一致。

评论

支持 Markdown