)
为 Effect CLI 修复包含引号、空格与特殊字符的 shell 补全Bash / Zsh / Fish 实现解析【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect导读本篇文章基于effect仓库中的变更集changeset文档深入解析一次针对 CLI shell 补全completion系统的修复当 choice 值包含引号、空格、词分隔符word-break 字符、Unicode 以及各类 shell 元字符时补全脚本会出错本次修复在 Bash、Zsh、Fish 三种 shell 上逐一解决了该问题。通过本文你将掌握Effect CLI 补全脚本的生成链路、choice 值在三种 shell 中的转义策略、以及 Bash 3.2 兼容性和 Fish「值型 flag 隐藏后仍保留值补全」等边界细节并了解对应源码与测试的位置便于你在此基础上复现或扩展自己的 CLI 补全逻辑。变更集原文本次变更集位于 .changeset/pre/escape-completion-choice-values.md全文如下--- effect: patch --- Fix shell completion for choice values containing quotes, spaces, word-break characters, Unicode, and shell metacharacters. Bash now quotes candidates for readline, keeps choice values intact when reconstructing words, and supports Bash 3.2 without associative arrays. Fish and Zsh escape choices across both parsing rounds, and Fish hides value-taking flags after use without suppressing their value completions.作为一次patch级别的修复它明确记录了三个 shell 各自要解决的核心问题。下文将分别结合源码与测试展开。背景Effect CLI 的补全生成架构Effect CLI 的补全能力位于 unstable CLI 模块中整体架构如下公共 API 入口是 packages/effect/src/unstable/cli/Completions.ts它定义了Shell类型bash | zsh | fish、CommandDescriptor/FlagDescriptor/ArgumentDescriptor等纯数据结构以及generate(executableName, shell, descriptor)入口按 shell 分发到三个内部生成器。命令树到描述符的转换由 packages/effect/src/unstable/cli/internal/completions/descriptor.ts 的fromCommand完成它把Flag.Literals/Argument.Literals声明的 choice 值通过Primitive.getChoiceKeys提取为{ _tag: Choice, values }把Flag.File/Flag.Directory映射为Path类型等。三个静态生成器分别位于 bash.ts、zsh.ts、fish.ts它们「从一个CommandDescriptor直接生成自包含脚本运行时不再重新调用 CLI」源码文件头注释明确说明了这一点。用户侧入口是内置全局 flag--completions bash|zsh|fish|sh定义在 packages/effect/src/unstable/cli/GlobalFlag.tssh会被归一化为bash。生成的脚本头会给出安装命令例如 Bash 脚本提示your-cli --completions bash ~/.bashrc。补全测试集中在 packages/effect/test/unstable/cli/completions/completions.test.ts其中定义了本次修复针对的「棘手 choice 值」集合见该文件trickyValues第 64–79 行const trickyValues [ its-fine, // 单引号 node:20, // 冒号zsh 中属于特殊字符 with space, // 空格 (whoami), // 命令替换类元字符 #tag, // 注释符 $HOME, // 变量展开 back\\slash, // 反斜杠 sayhi, // 双引号 a*b, // 通配符 a;b, // 分号 ~x, // 波浪号 foo, // 尾部单引号 a!b, // 感叹号 \u{1F680} // emoji多字节 Unicode ]Bash从「compgen 二次解析」到「面向 readline 的引号转义」问题根源旧的实现把 choice 值交给compgen -W处理。compgen -W会重新解析传入的单词列表含空格的值会被拆成多个候选、引号和元字符会被再次解释这正是「choice 值包含引号、空格、word-break 字符、Unicode、shell 元字符」时补全失真的根因。测试 completions.test.ts 明确断言assert.notInclude(script, compgen -W it)即 choice 补全不再走compgen -W的二次展开路径。修复方案choicesHelper 手工引号上下文匹配Bash 生成器在 bash.ts 中引入了choicesHelper第 59–110 行生成一个_name--choices辅助函数核心逻辑只转义 readline 会替换的那部分源码注释原文先由_init_completion拆出cur当前词与_comp_wordhelper 通过_head${_cur%$_word}求出「已提交」的前缀并检测当前词处于哪种引号上下文——*\视为单引号上下文*\视为双引号上下文。按引号上下文分别转义第 81–103 行单引号上下文词首打开的引号用 shell 拼接_match${_rest//\/\\\\\}词中打开的单引号上下文无法再包含单引号直接跳过含的候选。双引号上下文转义反斜杠、$、反引号与。无引号上下文用printf -v _match %q生成 shell 引用的候选并特判 Bash 3.2 下词首波浪号不会转义的问题第 100–101 行补一个反斜杠。readline 补充闭合引号若匹配值本身以引号结尾readline 会省略已在匹配中的闭合引号因此[[ -n $_open $_match *$_open ]] _match$_open主动补回第 104–105 行。调用处choiceCompletion第 112–113 行对每个候选做escapeForBash→\后整体包进单引号例如测试快照_deploy--choices $cur $_comp_word it\s-fine node:20 with space (whoami) #tag $HOME back\slash sayhi a*b a;b ~x foo\ a!b 保持 choice 值完整_init_completion -n $COMP_WORDBREAKSBash 补全默认会把COMP_WORDBREAKS如:、、等当作词边界切分。修复在生成的每个补全函数中调用_init_completion -n $COMP_WORDBREAKS || return-n参数把 word-break 字符从分隔符中剔除使node:20、sayhi这类值在重建单词时保持为一个整体。相关测试断言见 completions.test.ts 的splits words on whitespace only, so a value holding a word-break character stays one word。Bash 3.2 兼容不依赖关联数组脚本面向 Bash 3.2macOS 默认版本做了多处降级处理源码注释明确说明「Quoting these substitutions breaks quote matching on Bash 3.2」bash.ts 第 72–74 行因此_prefix/_committed的删引号替换刻意不加引号。已用 flag 过滤buildFlagGroupDeclarations第 31–50 行不使用关联数组而是为每个 flag 生成独立的_used_n标量变量再据此拼接_filtered_flags词表——即变更集中「supports Bash 3.2 without associative arrays」的实现。同时生成器自带_init_completion的内联 fallback 实现第 313–337 行在没有 bash-completion 包的环境也能工作测试uses no bash 4 syntax, so the script runs on the bash macOS shipscompletions.test.ts 第 369 行专门守护这一点。Zsh针对_arguments两轮解析的双重转义Zsh 生成器 zsh.ts 把 choice 值内联进_arguments的 actioncase Choice: return :value:(${type.values.map(escapeZshChoice).join( )})问题在于_arguments的 action 列表会被解析两轮第一轮是_arguments对 spec 字符串自身的解析第二轮是 action 内容作为候选值列表的重新展开。若只转义一次含空格、冒号、括号的值在第二轮解析时会再次失真。修复对应的escapeZshChoice第 18 行const escapeZshChoice (s: string): string s.replace(/[^A-Za-z0-9_.,/%-]/gu, \\$).replace(//g, \\)第一轮replace把所有非安全字符含空格、括号、冒号、#、$、*、分号、emoji 等多字节字符/u标志按码点处理逐一加反斜杠转义第二轮再处理单引号→\避免_arguments第二轮解析时引号破坏候选边界。测试escapes choice values for both the spec quoting and the action list re-parsecompletions.test.ts 第 458 行专门覆盖此双重转义。Fish两轮转义 值型 flag 的去重条件Fish 生成器 fish.ts 有两处关键处理。complete -a候选的转义第 15–18 行Fish 的complete -a候选列表同样存在「字符串层引用 候选列表再展开」两个阶段escapeFishChoice先对非安全字符逐码点加反斜杠再对反斜杠与单引号做字符串层转义const escapeFishChoice (s: string): string escapeFishString(s.replace(/[^A-Za-z0-9_.,/%-]/gu, \\$))choice flag 的输出形如-r -f -a 转义后的候选列表-r表示必须带值-f表示不回退到文件补全。测试快照completions.test.ts 第 621–622 行展示了with space、(whoami)、$HOME、emoji 等值在两轮展开后的完整转义形态。此外描述文本里的反斜杠也会在引号之前被预先转义escapeFishString测试见第 626–632 行。值型 flag 使用后隐藏、但不抑制值补全第 70–77 行变更集中「Fish hides value-taking flags after use without suppressing their value completions」对应valueFlagDedupConditionconst valueFlagDedupCondition (flag: Completions.FlagDescriptor): string begin; ${flagContainsOptCondition(flag)}; or contains -- (commandline -poc)[-1] ${forms.join( )}; end翻译成自然语言要么该 flag 还没被用过not __fish_contains_opt要么当前正在输入的就是该 flag 的取值contains -- (commandline -poc)[-1] flag 各形式。前者保证已用过的 flag 从候选列表里隐藏后者保证隐藏之后当前正在补全它的值这一行为不被误伤——这正是「隐藏值型 flag 却不抑制其值补全」的实现。测试断言可见第 684–702 行如-n begin; not __fish_contains_opt times; or contains -- (commandline -poc)[-1] --times; end。布尔型 flag 则仍走纯__fish_contains_opt去重。三种 shell 的统一安装与验证方式三种生成器产出的脚本都带###-begin-name-completions-###/###-end-name-completions-###标记可通过内置的--completions全局 flag 安装Shell安装方式来自生成脚本头注释Bashcli --completions bash ~/.bashrcZshcli --completions zsh ~/.zsh/completions/_cli再把~/.zsh/completions加入fpathFishcli --completions fish ~/.config/fish/completions/cli.fish一个值得注意的测试是 completions.test.ts 中的「tricky values」套件its-fine、node:20、with space、(whoami)、#tag、$HOME、back\slash、sayhi、a*b、a;b、~x、foo、a!b、分别覆盖了引号、冒号、空格、命令替换、注释符、变量展开、反斜杠、双引号、通配符、分号、波浪号、尾部引号、感叹号与多字节 Unicode 这几类棘手的转义场景是验证「choice 值转义修复」是否完备的最直接入口。总结本次 patch 在三个 shell 生成器层面分别解决了 choice 值的转义与词边界问题Bash 弃用compgen -W的二次解析、改用面向 readline 的引号上下文感知转义并通过_init_completion -n $COMP_WORDBREAKS与无关联数组实现保证了词完整性与 Bash 3.2 兼容Zsh 与 Fish 在各自的两轮解析模型下做双层转义Fish 额外用「隐藏 OR 正在取值」的条件保证值型 flag 隐藏后其值补全仍可用。相关的实现与回归测试均可直接在仓库的 bash.ts、zsh.ts、fish.ts 及 completions.test.ts 中查看与复现。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考