Fish: 一条鱼

0 阅读13分钟

终端的光标在闪。屏幕上是一行还没敲完的命令,git push origin 后面跟了一个忘了名字的分支。手指悬在 Tab 键上方,按下去,什么也没发生。又按了一次,还是什么也没有。

在这个由字符构筑的黑色深海里,无数的终端行者都在寻找一艘称手的潜艇。Bash 像是一艘古老而沉重的破冰船,坚毅却晦涩,它的航线图(脚本)充满了历史遗留的暗礁——它从 1989 年游到现在,三十六岁了,依然不会告诉用户分支叫什么。Zsh 则像是一艘需要精密改装的核潜艇,虽然功能强大,但若不花上数个夜晚配置那些复杂的 .zshrc 配置文件,它甚至无法启动引擎。

旁边工位的同事敲了一行 docker run,按下 Tab,屏幕上弹出了一列参数,每个参数后面跟着一行说明文字——这个容器要分配多少内存,那个卷要挂载到哪里。他甚至没有装过任何补全插件。

问他用的什么 Shell,他回了四个字母:fish

fish,即Friendly Interactive Shell。就在这片充满神秘代码符号的海洋中,它没有背负沉重的历史包袱,也没有让人望而却步的配置门槛。fish就是一条鱼,生来就是为了游出最强的易用性。2005 年 2 月 13 日,一个叫 Axel Liljencrantz 的瑞典人把它放进了水里。二十年后,这条鱼从 C++ 游到了 Rust,GitHub 上攒了三万颗星,却始终只做一件事——让命令行不再需要配置就能用。

水里的色彩

Fish 有一双能看见颜色的眼睛。

在 Bash 里敲命令,所有字都是同一个颜色。敲对了是白色,敲错了也是白色,直到按下回车,系统报一段错,才知道刚才把 git 打成了 gir

Fish 不等回车。每敲一个字母,它都在实时检查——这个命令存在吗?参数合法吗?路径指向的文件真的在那儿吗?存在就标绿,不存在就标红,有效路径加下划线,无效路径什么也不加。还没按回车,屏幕已经把答案写在了脸上。

# 在 fish 中输入时:
git comm<光标>
# "git" 显示为绿色(命令存在)
# "comm" 显示为灰色(未完成的输入)
# 按 Tab 后自动补全为 "commit",并显示参数提示

这不是插件,不是配置项,不是某个发行版的特殊补丁。Fish 从源码层面把语法高亮焊进了交互引擎。2005 年的第一个版本就有这个功能,那时候 Bash 已经十六岁了,还是没有。

Zsh 能做到吗?能。需要装 zsh-syntax-highlighting 插件,在 .zshrc 里 source 一下,再重启终端。一共大约三步。Fish 是零步。

记忆的洋流

Fish 不会遗忘。

在终端里反复输入同一条命令是日常——每天 git status 几十次,docker compose up 几次,ssh 到同一台机器几次。Bash 的做法是按上箭头,一条一条往前翻历史,或者按 Ctrl+R 搜索,搜到了再搜一次,搜过头了再往回搜。

Fish 的做法是:什么都不做。

输入 gi 的时候,屏幕右侧浮现一行浅灰色的字——git status,这是上一次输入过的命令。按右箭头,整行接受。输入 doc 的时候,浮现 docker compose up -d。按右箭头。结束。

这不是补全,这是 autosuggestion。Fish 在背后维护一份完整的历史记录,每当输入框发生变化,它就在历史里找最近一次以当前输入为前缀的命令,用浅灰色显示在后面。灰色意味着"这不是当前正在输入的内容,只是建议",右箭头意味着"接受这个建议"。

Julia Evans 是加拿大一位知名的技术博主,用 Fish 超过十年。她在 2024 年 9 月的博客里写了一段话,大意是:Fish 最让她着迷的细节之一,是它能自动补全包含路径的命令。输入 cd 然后按 Tab,它不仅补全目录名,还会根据上下文把最可能的目标排在最前面。

Bash 和 Zsh 也能做到类似效果,但需要安装 fzf 之类的工具,配合 bash-preexec 或 Zsh 的钩子机制,再写几行配置脚本。Fish 把这件事做成了默认行为。

读懂手册的鱼

大多数 Shell 的 Tab 补全是这样的:装了一个工具,补全就有了;没装,就没有。补全规则是手写的,散落在各处,有的工具提供,有的不提供。

Fish 读书。

确切地说,Fish 会读系统里安装的所有 man page——手册页。它有一个叫 fish_update_completions 的内置命令,运行时扫描系统上所有的手册页,解析其中关于命令行参数的描述,自动生成对应的补全规则。每条补全还附带从手册里提取的说明文字。

输入 tar 按 Tab,屏幕上不只列出参数,还告诉每个参数是干什么的。输入 grep 按 Tab,-i 旁边写着 "ignore case",-r 旁边写着 "recursive"。这些信息不是 Fish 团队手写的,是从系统已有的手册里自动提取的。

这意味着一个奇妙的连锁反应:在系统上装一个新工具,只要它带了手册页,Fish 就能自动学会它的参数补全。不需要装补全插件,不需要改配置文件,不需要重启 Shell。下次打开终端,它已经会了。

Bash 需要靠 bash-completion 项目,Zsh 需要靠 _comps 体系和各种插件。它们都在追赶一个 Fish 从第一天就内建的能力。

缩写而非别名

Bash 用户习惯在 .bashrc 里写别名:

alias gs='git status'
alias gp='git push'
alias dc='docker compose'

别名的问题是:它是一个黑盒。输入 gs 回车,Bash 执行 git status,但屏幕上显示的还是 gs。如果 gs 碰巧和某个系统命令重名,排查起来就很麻烦。别名也不会被传递给子进程,在脚本里不生效。

Fish 提供了一个不同的东西,叫 abbreviation——缩写。它在输入时展开:

# 在 fish 配置中定义:
abbr -a gs git status

# 输入 gs 后按空格:
# 屏幕上自动变成 "git status"
# 然后继续输入参数

输入 gs,按下空格,gs 在眼前展开成 git status。回车之前,能清楚地看到将要执行的真实命令。展开发生在输入层面,不是执行层面,所以在脚本里 gs 不会生效——脚本里只认完整命令,没有歧义。

这个设计折射出 Fish 的一个核心理念:交互层和脚本层应该用不同的规则。交互层追求效率和便捷,脚本层追求确定性和可移植性。Bash 把两层混在一起,用同一套语法服务两个完全不同的场景,结果两头都不讨好。

多行粘贴的安全感

终端用户都有过这种经历:从网页上复制一段 Shell 脚本,粘贴到终端,其中包含多行命令。在 Bash 里,粘贴的每一行都会被立即执行,第一行还没看清,第三行已经跑完了。如果中间有 rm -rf 之类的危险命令,后悔都来不及。

Fish 的做法不同:多行粘贴时,它把所有行放在同一个输入缓冲区里,以一个完整的多行命令形式呈现。用户可以看到所有将要执行的内容,检查无误后再按回车。

这听起来是一个小功能,但它改变了一种根本性的心理状态——从"粘贴即执行"到"粘贴即审阅"。Julia Evans 在博客里专门提到了这一点,说这让她粘贴多行命令时安心得多。

一张对照表

三条 Shell 走了三条路。Bash 是守旧派,Zsh 是改造派,Fish 是另起炉灶派。

维度BashZshFish
诞生年份198919902005
语法高亮无(需插件)需插件内建
自动建议无(需插件)需插件内建
Tab 补全手动配置手动配置自动解析 man page
开箱即用程度中(依赖框架)
POSIX 兼容大部分
脚本可移植性低(需 fish 安装)
配置方式编辑 .bashrc编辑 .zshrc + 框架fish_config 网页界面
4.0 版语言CCRust

Bash 像一辆手动挡卡车——哪里都能开,零件哪里都有,但离合器要自己踩。Zsh 像一辆改装车——动力强劲,但得花一个下午装大包围、换避震、刷 ECU。Fish 像一辆电动车——坐进去就能开,不需要热车,不需要换挡,不需要去加油站。

不守规矩的鱼

Fish 有一个"原罪":它不兼容 POSIX。

POSIX 是 Unix 世界的一套标准,定义了 Shell 脚本应该怎么写。Bash 基本遵循 POSIX,Zsh 也大部分兼容。Fish 从一开始就拒绝遵守。

在 Fish 里定义变量不是 VAR=value,而是 set VAR value。命令替换不是 $(cmd),而是 (cmd)。条件判断不是 if [ ... ]; then ... fi,而是 if ... ... end。函数定义不是 function f() { ... },而是 function f ... end

这意味着所有为 Bash 写的脚本——系统启动脚本、软件安装脚本、CI/CD 流水线——都不能直接用 Fish 跑。把 Fish 设为默认 Shell,某些系统脚本可能会出问题。

这是 Fish 被批评最多的地方。但 Fish 的设计文档里有一句话,可以看作对这种批评的回应。原话是:Fish should be user-friendly, but not at the expense of expressiveness. Most tradeoffs between power and ease of use can be avoided with careful design.

翻译过来:Fish 应该对用户友好,但不以牺牲表达力为代价。易用性和功能之间的大多数权衡,都可以通过仔细的设计来规避。

Fish 团队的立场很明确:POSIX 是 1988 年制定的标准,它的语法设计承载了几十年的历史包袱。如果目标是写脚本,用 Bash。如果目标是交互使用,为什么非要背着三十多年前的包袱?

事实证明,这个判断是正确的。大多数开发者同时使用两种 Shell——用 Fish 做日常交互终端,用 Bash 跑脚本和自动化任务。两者各司其职,互不干扰。POSIX 不兼容在实践中几乎不是一个问题,因为没有人用 Fish 去跑 #!/bin/sh 的脚本。

网页里的控制台

Fish 有一个独一无二的功能:fish_config

在终端里输入这个命令,Fish 会启动一个本地 Web 服务器,自动打开浏览器。在浏览器里,可以通过图形界面选择提示符样式、配色方案、查看已定义的函数和变量、浏览历史命令记录。

这听起来"不 Unix"——Unix 用户习惯用文本文件配置一切,用命令行操作一切。但 Fish 的判断是:配置 Shell 的体验本身就应该友好。一个新手第一次打开 Shell,不应该面对一个空的 .bashrc 文件和一堆不知道语法的配置选项。他应该看到几个预设的提示符样式,点一下就能用,觉得不够再去看文本配置。

这又是那个设计哲学:零配置为起点,有配置为进阶。先让工具能用,再让工具可调。Bash 和 Zsh 的路径是"先配置才能用",Fish 的路径是"先能用,想配置再说"。

忒修斯之鱼

2024 年 12 月 28 日,Fish 团队发布了一篇博客,标题叫"The Fish of Theseus"——忒修斯之鱼。

忒修斯之船是一个古老的哲学问题:如果一艘船的木板被一块一块替换,直到所有木板都是新的,它还是原来那艘船吗?

Fish 团队用这个名字,是因为他们做了一件疯狂的事:把 Fish 的全部代码从 C++ 重写成了 Rust。不是一部分,是全部。0% C++,100% Rust。2025 年 2 月 27 日,Fish 4.0 正式发布。

重写的原因不复杂。C++ 是一门强大但复杂的语言,内存安全要靠开发者自己保证,构建系统(CMake)的配置文件越来越难以维护,新贡献者面对 C++ 代码库的门槛很高。Rust 提供了内存安全保证,构建工具(Cargo)开箱即用,类型系统更现代,社区也更活跃。

整个重写过程持续了大约一年。团队的策略是"全部重写完再发布",不做渐进式迁移——不会有半个 C++ 半个 Rust 的中间状态。这需要耐心,但也保证了最终产出的干净。

Fish 4.0 发布后,社区反应普遍正面。Lobste.rs 上一位用了 Fish 十三年的用户说:"对 Rust 重写没什么特别的感觉,但一切都很顺畅。"这正是重写的目标——用户不应该感受到任何变化,除非他们去看源码。

鱼为什么游得最快

回到最初的问题:为什么推荐 Fish?

不是因为它的语法高亮比 Zsh 的插件好看多少个色号,不是因为它的自动建议比 fzf 快多少毫秒,不是因为它的补全覆盖了多少个命令。这些具体优势都存在,但它们不是核心。

核心是 Fish 对"易用性"的理解和所有其他 Shell 不同。

Bash 认为:易用性是兼容性,是到处都能跑。Zsh 认为:易用性是可配置性,是给用户最大的自由。Fish 认为:易用性是开箱即用,是零配置就有全部功能,是把开发者从"配置工具"这件事里解放出来。

这三种理解都没有错,但它们面向的是不同的人。Bash 面向系统管理员和脚本作者,Zsh 面向喜欢折腾的极客,Fish 面向只想把命令跑起来的人。

而"只想把命令跑起来"这件事,恰恰是绝大多数开发者的日常。不是每个人都享受写 .zshrc,不是每个人都记得 bindkey '^R' history-incremental-search-backward 的语法。大多数人打开终端,只是想 cd 到项目目录、git 一下状态、docker 起一个容器、ssh 到服务器。

Fish 把这些日常操作的摩擦力降到了最低。不需要配置,不需要记忆快捷键,不需要安装插件框架。装上 Fish,打开终端,开始用。语法高亮在,自动建议在,Tab 补全在,缩写在,多行粘贴保护在,网页配置界面在。

一条鱼不需要学游泳。它生来就在水里。

这正是 Axel Liljencrantz 在 2005 年想要的东西。二十一年后,这个想法依然有效。