Fish: 一条鱼

终端的光标在闪。屏幕上是一行还没敲完的命令,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 是另起炉灶派。

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

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 年想要的东西。二十一年后,这个想法依然有效。

posted @ 2026-08-14 23:33  减瓦~  阅读(17)  评论(0)    收藏  举报