fish-shell 贡献指南:从补全脚本到源码本地化的完整开发流程

原创2026-10-01 01:55:05329 阅读
文章标签:CLI开发工具

fish-shell 贡献指南:从补全脚本到源码本地化的完整开发流程

本文档以 fish-shell 仓库的 CONTRIBUTING.rst(由 doc_src/contributing.rst 直接 include)为骨架,结合仓库中的 xtask 工具链、测试体系与本地化源码,系统讲解如何为这个 GPLv2 协议下自由开源的"用户友好型命令行 Shell"贡献代码、补全脚本、文档与翻译。读完本文,你将掌握 fish-shell 的提交规范、补全脚本质量标准、文档构建命令、cargo xtask 自动化工具链、littlecheck/pexpect 测试方法,以及 gettext + Fluent 双轨本地化的完整实操流程。

一、总览:你能为 fish 贡献什么

fish-shell 欢迎多种形式的贡献,包括但不限于:

  • 修改核心 Rust 源码(如 src/ 下的解析器、内建命令、补全引擎);
  • 增强或新增补全脚本(share/completions 下已有 700+ 个工具的补全);
  • 改进官方文档(doc_src/ 下的 RST 源文件);
  • 参与各语言的翻译工作(localization/ 下的 PO 与 FTL 文件)。

无论贡献哪种类型,仓库都推荐使用一套统一的开发与验证流程:先在本地编译、格式化、跑测试,再通过 GitHub Pull Request(或公开邮件列表)提交。下文按贡献类型逐一展开。

二、环境准备:克隆、构建与前置依赖

1. 克隆仓库

首先需要一个 GitHub 账号,将 fish-shell 仓库 fork 到自己的账号下,然后克隆到本地:

git clone https://github.com/<USERNAME>/fish-shell.git

该命令会在当前目录创建 fish-shell/ 目录。除极简单、无需验证的改动外,大多数改动都建议先搭好编译与测试环境。

2. 编译与测试所需依赖

按贡献类型,依赖需求不同(详见 CONTRIBUTING.rst):

依赖 用途 是否必须
Rust(拿不准时用 rustup 安装) 编译核心源码 必须
CMake 构建系统(另见 CMakeLists.txt) 必须
PCRE2(头文件与库) 正则支持,缺失时会自动下载 可选
gettext(仅需 msgfmt 工具) 翻译支持 可选
Sphinx 构建文档 可选

例如只改文档,仅需 Sphinx;只改补全脚本,则几乎不需要完整编译环境。判断标准很简单:改动是否涉及需要验证的行为。

3. 提交渠道

  • Pull Request:改动完成后在 GitHub 上打开 PR;
  • 邮件列表:作为 GitHub 的替代方案,可将补丁发送到公开邮件列表 ~krobelus/fish-shell@lists.sr.ht(列表归档亦可查询)。

三、提交历史规范:线性 recipe-style 历史

fish 使用线性、recipe 风格的历史记录(每个提交像一条"菜谱",独立、自洽、可读):

  • 每个提交必须通过全部检查(包括格式化、lint、测试),提交历史中不应出现 "fixup" 补丁型提交;
  • 若 PR 中的某个提交存在问题,或收到修改反馈,应当直接重写相关提交(git rebase),而不是追加新提交来修补;
  • PR 准备合并时,维护者会将其 rebase 到 master 之上,因此大胆重写尚未进入 master 的分支历史是预期行为;将 PR rebase 到最新 master 以解决冲突同样受欢迎;
  • 使用 Git 时,可考虑用 jj(Jujutsu VCS)来简化历史重写;
  • 若某提交用于关闭某个 issue,应在提交描述末尾添加 Fixes #<issue-number> 行。

这套规范保证主分支历史始终整洁、可二分、每个节点都可独立测试。

四、贡献补全脚本:最普遍的贡献方式

补全脚本是 fish 社区最常见的贡献类型,仓库持开放欢迎态度——只要是公开可用命令的、写得好的补全脚本都会被接纳(私有工具或个人脚本除外,维护者保留其他合理拒绝权)。

1. 先考虑交给上游维护

动手前先思考:被你补全的那个工具的作者是否更愿意自己维护这份脚本?通常由工具作者维护更合理——他们能随新版本立即补充新选项,且无需为多个版本各维护一份脚本。若作者不再愿意维护,可通过 PR 移交给 fish 维护团队。这不是硬性要求:作者不愿维护、或你不想联系他们时,直接贡献给 fish 即可。

2. 补全脚本的六条质量准则

  1. 依赖越少越好:优先用 fish 内建命令(如 string)替代 grep、awk;用 python 解析 JSON 而非 jq(python 已是 fish 工具链的软依赖);
  2. 若使用常见 Unix 工具,须用 POSIX 兼容调用,理想情况下在 GNU/Linux、macOS、各 BSD 及其他系统上都能工作;
  3. 选项与参数描述保持简短——描述越短,fish 在补全弹出界面中能利用的列数越多;
  4. 函数名以 __fish 开头,且除非被别处复用,函数应保留在补全文件内;
  5. 用 fish_indent 格式化脚本;
  6. 慎用刚出现不久的新特性:fish 尽量保持补全脚本可向后移植。若某特性对正确性或性能有实质影响,可以放心使用;若只是语法便利,请保留旧写法。

3. 文件放置与测试

补全脚本放入 share/completions/name-of-command.fish;若有多个命令,需拆分为多个文件。仓库现有实例如 share/completions/curl.fish:它先用 __curl_complete_data 补全 @ 后的路径,再用 complete -c curl 逐项注册短选项(-s)、长选项(-l)与参数描述(-d),描述文本保持精简以便多列展示。

若想为补全添加测试,推荐使用 littlecheck 测试(见下文"测试体系"一节)。

五、贡献文档:RST + Sphinx

1. 文档组织

  • 文档存放于 doc_src/,使用 ReStructured Text(RST)编写,由 Sphinx 构建;
  • 内建命令与随 fish 附带的各类函数,在 doc_src/cmds/ 中逐一成文(如 abbr.rst、string.rst)。

2. 本地构建 HTML 文档的三种方式

方式一:使用 xtask(推荐,自动保证 fish_indent 可用):

cargo xtask html-docs

输出到 target/fish-docs/html。从源码看,该 xtask 在 crates/xtask/src/main.rs 中实现:若未指定 --fish-indent 路径,会先以 dev profile 构建 fish_indent 二进制,再将其软链接进临时目录并加入 PATH,随后调用 sphinx-build -j auto -q -b html,避免向全局 PATH 引入其他二进制。

方式二:使用 CMake:

cmake --build build -t sphinx-docs

输出到 build/cargo/fish-docs/html/。

方式三:直接调用 sphinx-build(可自选输出目录):

sphinx-build -j auto -b html doc_src/ /tmp/fish-doc/

构建完成后,用浏览器打开生成的 HTML 检查效果。

六、代码风格与格式化

1. 三种语言三种格式化器

语言 格式化工具
Rust rustfmt
fish 脚本 fish_indent(随 fish 自带)
Python ruff format

统一入口是 xtask(实现见 crates/xtask/src/format.rs):

cargo xtask format --all
cargo xtask format somefile.rs some.fish

xtask 会依次运行 fish_indent、Fluent FTL 内置格式化器、ruff format 与 rustfmt;--all 会覆盖 benchmarks、build_tools、etc、share 目录下的所有 fish 文件。--check 模式只报告未格式化文件而不改动内容;未加 --force 时,若检测到未提交改动,会交互式确认后再格式化。

2. fish 脚本风格指南

  1. 所有 fish 脚本(如 share/functions 与 tests 目录下的)必须用 fish_indent 格式化;
  2. 函数名全小写、单词以下划线分隔;私有函数以下划线开头;若函数为 fish 独有,首词应为 fish(如 fish_prompt、fish_add_path);
  3. 全局变量名首词通常为 fish(公有)或 _fish(私有),以最大限度避免与用户自定义变量命名冲突。

3. 编辑器配置

Vim:安装 vim-fish 插件,并在 ~/.vimrc 启用语法与文件类型功能:

syntax enable
filetype plugin indent on

在 ~/.vim/ftplugin/fish.vim 开启更友好的显示选项:

" 让 :make 用 fish 做语法检查
compiler fish

" 长行在注释内自动换行
setlocal textwidth=79

" 对 fish 块结构启用折叠
setlocal foldmethod=expr

Emacs:安装 fish-mode(可在 melpa / melpa-stable 获取),为其设置 (setq-default indent-tabs-mode nil)(通过 hook 或 use-package 的 :init 块),并可让保存时自动执行 fish_indent:

(add-hook 'fish-mode-hook (lambda ()
    (add-hook 'before-save-hook 'fish_indent-before-save)))

4. MSRV(最低支持 Rust 版本)政策

fish 至少支持 Debian Stable 中所带 rustc 版本,这决定了仓库可使用的 Rust 特性上限。

七、测试体系:单元测试、littlecheck 与 pexpect

fish 拥有规模庞大的测试集。任何改动都建议运行测试以确认行为一致、无回归;即使本地不跑,GitHub Actions 也会在 CI 中执行。修复 bug 时尤其鼓励补测试,防止未来重新引入。

1. 三种测试类型与位置

  • 单元测试:位于 Rust 源码文件内的内联子模块(mod tests {})中,紧挨实现;
  • littlecheck 系统测试:tests/checks 目录,由 littlecheck 驱动,覆盖非交互(脚本)行为;其中 tests/checks/tmux-* 例外,用于交互场景。这是新增测试的首选位置——最易编写与修改,比 pexpect 快得多且更稳定;
  • pexpect 交互测试:tests/pexpects 目录,用 Python 的 pexpect 模拟终端输入输出,适用于需要真实交互的测试;其运行器为 tests/pexpect_helper.py。

2. littlecheck 测试语法

littlecheck 测试本质是带期望输出注释的 fish 脚本。例如 tests/checks/abbr.fish:

#RUN: %fish %s

# Universal abbreviations are imported.
set -U _fish_abbr_cuckoo somevalue
set fish (status fish-path)
$fish -c abbr
# CHECK: abbr -a -- cuckoo somevalue # imported from a universal variable, see `help cmds/abbr`

# Test basic add and list of __abbr1
abbr __abbr1 alpha beta gamma
abbr | grep __abbr1
# CHECK: abbr -a -- __abbr1 'alpha beta gamma'
  • #RUN: %fish %s 声明以 fish 运行本脚本;
  • # CHECK: 注释声明标准输出的期望行;
  • # CHECKERR: 声明 stderr 的期望行;
  • 若测试有特定外部依赖,用 # REQUIRE: ... 加一段 POSIX sh 脚本表达。

若测试需要某个做"奇怪事情"的命令,可将其加入 tests/fish_test_helper.c 编译出的 fish_test_helper 二进制。

3. 本地运行测试

在仓库根目录执行:

cargo build
# 运行单元测试
cargo test
# 运行系统测试
tests/test_driver.py target/debug
# 运行某个具体系统测试
tests/test_driver.py target/debug tests/checks/abbr.fish

test_driver.py(见 tests/test_driver.py)的第一个参数指向包含 fish、fish_indent、fish_key_reader 二进制的目录;它会建立临时 $HOME 并将其作为当前目录,因此测试中无需自建临时目录。上面的例子是在工作区根目录执行了不带 --release 的 cargo build,即 debug 构建。

4. 一键全量检查

cargo xtask check

该命令封装 build_tools/check.sh,会依次执行:以 fluent-extract,gettext-extract 特性构建工作区、cargo deny 许可证检查、cargo xtask shellcheck、cargo xtask format --all --check、针对 --no-default-features/--all-features 的 clippy、cargo xtask gettext ... check、cargo xtask fluent check --from-source、cargo test、系统测试等。其中 FISH_CHECK_LINT=false 可跳过 lint 环节。

八、贡献翻译与本地化

fish 可本地化 Rust 源码与 fish 脚本中的消息;后者包含大量自动识别的消息(如 fish 函数描述)。翻译时优先处理 Rust 源码中的消息。

1. 双本地化体系:gettext 与 Fluent

fish 同时使用两套系统:

  • GNU gettext:用于所有来自 fish 脚本的消息;
  • Fluent:用于 Rust 源码消息,正在逐步替换 gettext,目前两者并行使用。

fish 使用自研工具做消息提取与 gettext 运行时本地化,因此没有对 gettext 库的运行时依赖,也意味着 gettext 的部分特性(如消息上下文、复数形式)不受支持。所有文件要求 UTF-8 编码。

2. 翻译文件组织与命名

其中 ll 是两位(偶为三位)ISO 639-1 语言代码(如 pt 表葡萄牙语),CC 是 ISO 3166 国家/地区代码(如 BR 表巴西)。合法示例如 pt_BR.po / pt_BR.ftl(巴西葡萄牙语)。当前仓库已含 de、es、fr、ja_JP、pl、pt_BR、sv、zh_CN、zh_TW 等语言文件。

某些情况下也会使用不带国家代码的 ll.po/ll.ftl。变体选择涉及回退行为的权衡:若懂该语言任意变体的人都应能理解你所加版本、且该语言尚无其他变体翻译,通常可省略国家代码;否则应为其下所有变体分别使用国家代码。拿不准可询问维护者。

3. 为全新语言添加翻译

gettext 侧(需要 gettext 工具,具体为 msguniq、msgmerge 与 msgattrib):

cargo xtask gettext new ll_CC

这会在 localization/po/ 创建包含全部可翻译消息的新 PO 文件;文件已存在时则更新。对应 xtask 实现见 crates/xtask/src/gettext.rs 的 New 子命令。

Fluent 侧:只需在 localization/fluent 目录创建以语言命名的空文件即可。

4. 让翻译生效

修改翻译文件后重新编译 fish 即可集成改动(要求装有 msgfmt,随 gettext 提供)。Fluent 消息在使用 debug 构建时无需重编译,重启 fish 即可看到新翻译。前提是启用 localize-messages Cargo 特性(默认已启用),也可显式执行:

cargo build --features=localize-messages

用环境变量指定语言:

LANG=pt_BR.utf8 fish

或在运行中的 fish 里:

set LANG pt_BR.utf8

也可用内建命令 status language(源码位于 src/builtins):

status language set pt_BR
status language list-available

list-available 可列出可用语言标识符,有助于确认新翻译被正确识别。

环境变量与 status language 的差异:

  • 环境变量启用回退行为:如设 LANG=de_DE.utf8 而无 de_DE 目录但有 de 目录,会显示后者的消息;
  • status language 只支持精确匹配,回退顺序完全可控;一旦使用,它覆盖环境变量配置;
  • 可用 LANGUAGE 变量提供按序回退的语言列表:
set LANGUAGE pt_BR de_DE

等价于:

status language set pt_BR de

即先尝试葡萄牙语,失败则德语,再失败显示默认英文。

5. 修改既有翻译

已有翻译的语言,直接编辑对应文件即可,无需其他改动;重新编译后即可看到效果。

6. 编辑 PO 文件

PO 文件可用命令行工具、GUI 程序或纯文本编辑器编辑。打开如 localization/po/sv.po,可见:

msgid "%s: No suitable job\n"
msgstr ""
  • msgid 是被翻译字符串的"名字",通常即英文原文;
  • msgstr 是翻译内容所在处。例如:
msgid "%s: No suitable job\n"
msgstr "%s: Inget passande jobb\n"

关键规则:

  • %s、%d 等是运行时格式化占位符,译文必须保持相同占位符、相同顺序;
  • \n 等转义字符必须保留,使译文行为一致;
  • 测试会运行 msgfmt --check-format /path/to/file 检查占位符是否匹配——不匹配会导致 fish 运行时报错;
  • 切勿手工更新 msgid,只能通过脚本完成(见下文)。

7. 编辑 FTL 文件

Fluent 的核心原则:每条消息有一个 ID,ID 在源码中指定;运行时 Fluent 按用户语言设置查找 FTL 文件,把 ID 映射为本地化消息。所有消息都有英文版本,因为源码中只有 ID 没有完整消息——localization/fluent/en.ftl 记录了当前使用的 ID 与对应英文消息,例如:

fish-version = { $package_name }, version { $version }
argparse-invalid-option-spec = Invalid option spec '{ $option_spec }' at char '{ $bad_char }'

部分消息携带参数(Fluent 中称变量),变量有名字、可重排,因此能按语言习惯调整顺序,格式为 { $variable_name }。

Fluent 生态不如 gettext 成熟、工具较少,因此 fish 自备工具。运行 cargo xtask fluent 可查看总览(实现见 crates/xtask/src/fluent.rs),对译者有用的三个命令:

cargo xtask fluent format
cargo xtask fluent show-missing
cargo xtask fluent check

分别用于:使 FTL 文件符合期望格式、列出尚无翻译的消息 ID、对 FTL 文件执行检查以发现错误。三者均可带可选路径参数,例如只检查某个文件:

cargo xtask fluent check localization/fluent/pt_BR.ftl

(在 localization/fluent 目录内则写作 cargo xtask fluent check pt_BR.ftl。)

编辑器内格式化:以下命令从 stdin 读 FTL 文本、向 stdout 输出格式化结果(格式化失败则原样输出):

cargo --quiet xtask fluent format -

Vim 中实现写入时自动格式化的配置:

function FormatFTL()
    let cursor = getpos('.')
    :%!cargo --quiet xtask fluent format -
    call setpos('.', cursor)
endfunction

augroup ftl
    autocmd!
    autocmd! BufWritePre *.ftl :call FormatFTL()
augroup END

NeoVim 等价 Lua 配置:

local augroup_ftl = vim.api.nvim_create_augroup("ftl", { clear = true })
vim.api.nvim_create_autocmd("BufWritePre", {
    group = augroup_ftl,
    pattern = "*.ftl",
    callback = function()
        local cursor = vim.fn.getpos(".")
        vim.cmd("%!cargo --quiet xtask fluent format -")
        vim.fn.setpos(".", cursor)
    end,
})

另有 Fluent 的 Vim 插件提供语法高亮。

8. 源码字符串变更(gettext 专属流程)

若源码中的字符串发生变化,旧翻译即失效。开发者增删改源码中的可翻译字符串后,需运行:

cargo xtask gettext update

把变更传播到所有 localization/po/*.po 文件(仅对修改 fish 源码或脚本的人相关)。注意:源码中已不存在的消息,其翻译会被从 PO 文件中删除;若源字符串的变更不应影响翻译,可考虑同步更新 PO 文件中的 msgid 以保留既有翻译。

9. 为翻译准备代码

Rust 源码(Fluent,新代码首选):所有面向用户的非调试消息都应标记为可翻译,使用 localize! 宏,例如:

localize!(
    "some-message-id" = "English version of the message. Must be a valid Fluent message definition. Example variables: { $var1 }, { $var2 }",
    var1 = "some string",
    var2 = 42,
);

第一组键值对是消息的 Fluent ID 与英文版本,其余键值对指定 Fluent 变量及其值,且必须与消息定义中使用的变量一致。仓库实例如 src/builtins/argparse.rs、src/builtins/shared/misc.rs。

跨所有 FTL 文件批量重命名消息 ID 或变量名时,可用:

cargo xtask fluent rename

Rust 源码中的定义是英文消息的"真相来源";工具会据此自动生成对应的 en.po 文件,但该文件完全自动生成、不支持手工修改(仅为兼容 Fluent 工具链、并用于检测源码定义变更)。消息定义变更后,工具会自动检测:部分情况可自动解决(如消息 ID 消失则删除翻译),无法自动解决时会给受影响翻译添加注释标记,只要存在此类标记检查就无法通过。解决方式为:

cargo xtask fluent resolve-outdated

或(若你懂该语言)手工更新翻译并移除注释。

Rust 源码(旧式 gettext):使用 wgettext! 或 wgettext_fmt! 宏,新代码应改用 Fluent:

streams.out.appendln(&wgettext_fmt!("%s: There are no jobs", argv[0]));

fish 脚本:显式本地化时,所有消息必须用单引号或双引号包裹(否则提取脚本找不到),且必须经命令替换翻译。因此以下写法无效:

echo (_ hello)
_ "goodbye"

应写成:

echo (_ "hello")
echo (_ "goodbye")

单双引号皆可,还允许在开括号后或闭括号前加空格。

九、更新依赖

依赖更新统一由脚本完成:

build_tools/update-dependencies.sh

见 build_tools/update-dependencies.sh。当前该脚本依赖 updatecli 等工具;仓库的 updatecli.d/ 目录存放了 docker、python、rust 的自动更新配置。

十、版本号生成

fish 版本由 build_tools/git_version_gen.sh 脚本构造:对开发者而言,版本为"分支名 + git describe --always --dirty 输出";版本主体通常是最近的 annotated tag,即最近的发布号(如 2.6.0)。脚本逻辑为:先读取 Cargo.toml 中的 version 字段,再尝试用 git describe --always --dirty 获取 git 版本;若 git 版本不是以该版本号开头,则拼成 version-g<git_version> 形式输出。

十一、AI 生成内容政策

fish 不接受由生成式人工智能工具创建的贡献:请勿提交由 Copilot、Zed AI、Cursor 的 AI 功能、其他编程助手或 ChatGPT 等工具创建或编辑的 Pull Request、GitHub issue、评论或美术作品。

结语:一次完整贡献的最小路径

综合全文,一次符合规范的贡献流程可概括为:fork 并克隆 → 按需安装依赖 → 在对应目录修改(补全脚本入 share/completions、文档入 doc_src、翻译入 localization、测试入 tests/checks)→ cargo xtask format 格式化 → 运行相关测试与 cargo xtask check → 以干净的线性历史提交(含 Fixes #N)→ rebase 到最新 master → 提交 PR。这条路径同时覆盖了代码、脚本、文档与翻译四种贡献形态,且每一步都有仓库内的自动化工具兜底验证。

登录后查看全文
fish-shell