阶段二 · Shell 入门

Shell 是什么:bash / zsh / fish 对比

1. 为什么叫 Shell(壳)?

前几篇反复提到 "Shell 只是一个普通的用户态程序". 但为什么这个程序叫 "壳"?

这个名字来自 Unix 的设计哲学--分层:

┌───────────────────────────────────────┐
│ 用户程序 (浏览器, 编辑器...) ← 最外层 │
└───────────────────────────────────────┘


┌──────────────────────────────────────┐
│ 系统调用接口 (syscall) ← 用户态/内核态分界线 │
└──────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Shell (bash/zsh/fish) ← 壳:包裹着内核 │
└──────────────────────────────────────────┘
┌─────────────────────────────┐
│ 内核 (Kernel) ← 核:管理硬件 │
└─────────────────────────────┘
┌───────────────────────────────┐
│ 硬件 (CPU, 内存, 磁盘...) ← 最底层 │
└───────────────────────────────┘

**Kernel(内核)**是操作系统的核心--管理硬件, 调度进程, 处理文件系统. 但它不直接跟人对话. Shell(壳)包裹在内核外面,是人与内核之间的翻译层--输入人类可读的命令,Shell 把它翻译成系统调用.

把操作系统想象成一颗鸡蛋: 内核是蛋黄,Shell 是蛋壳.
只能通过蛋壳接触蛋黄--Shell 是与内核之间的接口.
当然现在我们知道这不是唯一接口(GUI 应用也发系统调用),但 "壳" 这个名字从 1970 年代沿用至今.

2. Shell 进化史

每天使用的 zsh,不是一天设计出来的. 它是一棵跨越 50 年的进化树上的最新枝叶.

1971 · Thompson sh -- 第一个 Unix Shell. 只有最基本的管道和重定向,没有脚本语言.

1979 · Bourne sh -- 第一个真正的脚本语言. 变量, 循环, 条件. POSIX 标准的基础.

1989 · Bash -- Bourne Again Shell. GNU 项目出品. 兼容 sh + 交互增强.

1990 · Zsh -- 兼容 bash + 极致可定制. oh-my-zsh 推动其流行.

2005 · Fish -- 不兼容任何旧 Shell. 开箱即用,语法完全不同.

2.1 Thompson shell(1971) - 第一个 Shell

Ken Thompson 在 PDP-7 上写了第一个 Unix Shell. 它极其简陋--没有变量, 没有脚本, 没有函数. 但它的核心概念--把命令的输出重定向到文件,用管道连接两个命令--奠定了 Unix 的一切. 这个设计后来被提炼成一句口号: "一切皆文件,小工具组合干大事".

2.2 Bourne shell / sh(1979) - 脚本语言的诞生

Stephen Bourne 写的 sh 是 Unix V7 的标配 Shell. 它引入了完整的脚本语法--变量($VAR), 条件(if...fi), 循环(while...done), 函数. 这个语法框架至今仍在使用. POSIX 标准 POSIX.1 的 Shell 部分(IEEE 1003.1)就是以 Bourne shell 为蓝本的.

2.3 C shell / tcsh(1978–1980s) - 另一起点

csh(C Shell)来自 BSD 阵营,语法类似 C 语言(if (expr) then 而不是 if ... fi). 它引入了命令历史(!!, !$)和作业控制. tcsh 是它的增强版. csh 的脚本语法被认为是有缺陷的(变量作用域, 重定向有坑),如今很少用于脚本,只作为历史注脚.

2.4 Bash(1989) - GNU 时代的 sh

Bash(Bourne Again Shell)由 GNU 项目的 Brian Fox 编写. 它的设计目标是:

  • 与 sh 完全兼容--所有 sh 脚本都能在 bash 下运行
  • 加入交互增强--readline 行编辑, 命令补全, 更丰富的提示符
  • 加入脚本增强--数组, [[ ]] 条件测试, $() 命令替换

Bash 是 Linux 的默认 Shell,也是 macOS 10.3 之前和大多数 CI 环境的默认 Shell. 如果写一个 .sh 脚本要跑在别人的机器上,用 bash 语法是最安全的选择.

2.5 Zsh(1990) + oh-my-zsh(2009) - 交互的顶点

Zsh(Z Shell)由 Paul Falstad 在普林斯顿大学写出. 它的定位是: 兼容 bash,但在交互体验上做到极致. 核心杀手特性:

  • 自动补全远超 Tab: ls <Tab> → 菜单式选择,支持模糊匹配
  • 拼写纠正: sl <Enter> → "Did you mean ls? [y/n]"
  • 通配符增强: ls **/*.go 递归匹配所有 .go 文件
  • 右提示符(RPROMPT): 可以在终端右侧显示信息
  • 插件生态: oh-my-zsh(2009 年)提供了 300+ 主题和插件,彻底引爆了 zsh 的流行

zsh 是 macOS 10.15 之后和 iTerm2 搭配使用的默认组合.

2.6 Fish(2005) - 不兼容的勇气

Fish(Friendly Interactive Shell)做了一个大胆的决定: 不兼容任何旧 Shell 语法. 它的设计理念是: 如果不写脚本, 只做交互,那 Shell 可以做得好用得多. 特点:

  • 开箱即用: 语法高亮, 自动补全, 历史建议(灰色 ghost text)全部默认开启,不需要装 oh-my-zsh
  • 不同语法: set var value(不是 var=value),没有 [ ] 而是 test
  • 代价: 所有 sh/bash/zsh 脚本在 fish 下都不能直接运行

如果从不写 Shell 脚本,只把终端当交互工具,fish 可能是体验最好的选择. 但如果需要写脚本, 需要兼容服务器环境,bash/zsh 更实际.

3. POSIX sh: 兼容性的分水岭

所有 Shell 分为两大类: POSIX sh 兼容的不兼容的.

兼容 POSIX sh 不兼容
sh(原始 Bourne) csh / tcsh(语法不同)
bash fish(刻意不兼容)
zsh PowerShell(Windows 世界的 Shell)
dash, ash(嵌入式 Linux)

POSIX sh 只定义了约 100 条命令和基本语法. Bash 和 zsh 都超出了这个集合--但它们都能以 sh 模式启动:

# bash 以 POSIX sh 模式运行
$ bash --posix

# 如果 /bin/sh 是 bash(常见于 Linux),它看到自己叫 "sh" 时会自动进入 posix 模式
$ ls -l /bin/sh
lrwxr-xr-x 1 root wheel 8 Jan 1 2024 /bin/sh -> bash # Linux 常见
# 或
-rwxr-xr-x 1 root wheel 1.3M Jan 1 2024 /bin/sh # macOS 的 /bin/sh 是 bash 但很老了(3.2)

macOS 的 /bin/sh vs /bin/bash

Mac 的 /bin/sh 是 bash 3.2(Apple 曾因 GPLv3 许可证问题不再升级). 10.15 起 /bin/zsh 是 zsh 5.x. 不要在 Mac 上写依赖 bash 4+ 特性的脚本然后标 #!/bin/sh--它在 Mac 上是 bash 3.2 在跑,在 Linux 上可能是 dash 在跑,行为不同.

4. Bash vs Zsh: 每天在用的两个

这是最实际的问题. Bash 和 zsh 在交互使用脚本编写两个场景下差异程度不同.

4.1 交互体验: zsh 远比 bash 好用

特性 Bash Zsh(无插件) Zsh + oh-my-zsh
Tab 补全 基本文件名 菜单式, 模糊匹配 插件增强(git, docker...)
语法高亮 需 readline 配置 需插件 fish-like 高亮
历史建议 需插件 灰色 ghost text
拼写纠正 内置 内置
递归通配 **(bash 4+) **/*.go 支持
RPROMPT 内置 oh-my-zsh 主题利用它显示 git 信息

4.2 脚本语法: 核心相同,边缘不同

90% 的日常语法在 bash 和 zsh 中完全相同. 变量, 条件, 循环, 管道, 重定向, 函数--这些在两者之间几乎没有差异. 但细节上:

场景 Bash Zsh
数组索引 ${arr[0]}(从 0 开始) $arr[1](从 1 开始!)
数组定义 arr=(a b c) 相同
字符串替换 ${var/old/new} 相同
条件测试 [[ $a == $b ]] 相同
通配展开 不匹配时保原文 不匹配时报错
不存在的变量 $UNDEFINED → 空字符串 默认报错(可关)

数组索引的坑

这是 bash 和 zsh 之间最坑的差异: arr[1] 在 bash 里是第二个元素,在 zsh 里是第一个元素. 如果写脚本要跨 Shell 运行,要么不用数组,要么显式用 #!/bin/bash 锁定解释器.

4.3 配置文件与启动流程

这是最容易被忽略但最实用的知识. 打开一个终端窗口时,Shell 到底读了哪些配置文件?

场景 Bash 读取顺序 Zsh 读取顺序
登录 Shell /etc/profile~/.bash_profile~/.bash_login~/.profile(只读第一个找到的) /etc/zprofile~/.zprofile
交互非登录 /etc/bashrc~/.bashrc /etc/zshrc~/.zshrc
非交互脚本 $BASH_ENV(如果设置了) 什么都不读

登录 Shell vs 非登录 Shell: "登录 Shell" 是在登录系统时(TTY 登录, SSH 登录, macOS 打开新终端窗口)第一次启动的 Shell. "非登录交互 Shell" 是在已登录的会话中另外开的子 Shell.

在 macOS 上,Terminal.app 和 iTerm2 默认打开的每个新窗口都启动一个登录 Shell(所以 ~/.zprofile~/.zshrc 都被读取).

oh-my-zsh 的配置文件是什么? 它和 ~/.zshrc 是什么关系?

oh-my-zsh 会覆盖 ~/.zshrc(安装时备份原文件为 ~/.zshrc.pre-oh-my-zsh). oh-my-zsh 的 ~/.zshrc 内容很简单:

# ~/.zshrc(oh-my-zsh 版)
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="robbyrussell"
plugins=(git brew docker)

source $ZSH/oh-my-zsh.sh
# 自定义配置放在这行下面

真正的逻辑在 ~/.oh-my-zsh/oh-my-zsh.sh 里--它加载主题, 插件, lib 目录下的工具函数. 只需要在 ~/.zshrc 最后追加自己的自定义配置即可.

5. Fish 的理念: 开箱即用 vs 可定制

bash 和 zsh 的共同模式是: 裸 Shell 很朴素 → 通过各种配置/插件/主题变好看. fish 反其道而行: 出厂就好看,不需要配置.

# fish 开箱自带的效果(不需要任何配置):
$ ls -l /tm⇆ # 输错了 ⇆ 是红色的(语法高亮)
# tab 补全出现菜单,可以 ↑↓ 选
$ git # git 命令是紫色,后面自动建议灰色 ghost text "commit -m"

但 fish 的代价是真实存在的:

  • 不兼容 POSIX sh: 任何使用 VAR=value cmd(临时环境变量)或 $(...) 的脚本都不能在 fish 下跑
  • 配置方式不同: 没有 ~/.fishrc,而是通过 fish_config 命令打开 Web UI 配置界面
  • 函数取代别名: fish 不用 alias,用 function 定义快捷命令

实操建议: 把 fish 设为默认交互 Shell(体验好),但保持 /bin/sh 指向 bash 或 dash,脚本仍然用 #!/bin/bash 写. 两不耽误.

6. 如何查看, 切换, 理解 Shell

# 当前运行的是哪个 Shell?
$ echo $SHELL
/bin/zsh
# 注意:$SHELL 是用户的默认 Shell,不一定是当前运行的.
# 更准的方法:
$ echo $0
-zsh

# 系统上安装了哪些 Shell?
$ cat /etc/shells
/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/zsh
/opt/homebrew/bin/fish # 如果是 Homebrew 装的

# 改变默认 Shell
$ chsh -s /bin/zsh # 改成 zsh(下次登录生效)

# 临时用另一个 Shell
$ bash # 在当前终端里启动一个 bash 子进程
$ exit # 退出回到 zsh

/bin/sh 和 /bin/bash 到底是什么关系?

/bin/sh 是一个符号链接(或硬链接, 或独立二进制). 在不同系统上指向不同的东西:

系统 /bin/sh 实际是
Debian/Ubuntu(2006 年后) dash(Debian Almquist Shell,比 bash 更小更快但功能少)
RHEL/CentOS/Fedora bash(以 POSIX 模式运行)
macOS bash 3.2(古老版本,Apple 因 GPLv3 问题不再更新)
Alpine Linux ash(BusyBox 内建 Shell)

这就是为什么 #!(shebang)很重要--写在脚本第一行的解释器路径决定了哪个 Shell 来执行.

7. Shebang: #! 的正确写法

脚本第一行的 #!(hash + bang,所以叫 shebang)告诉系统用哪个解释器来执行这个文件:

#!/bin/sh           # POSIX sh--最大兼容性,但功能最少
#!/bin/bash # Bash--Linux 默认,功能丰富
#!/bin/zsh # Zsh--如果用了 zsh 特有语法
#!/usr/bin/env bash # 可移植写法:在 PATH 中查找 bash
#!/usr/bin/env fish # 可移植的 fish 脚本
#!/bin/false # 故意无法执行(用于 "只能 source" 的脚本)
写法 优点 缺点
#!/bin/bash 确定性强--用哪个 bash 说了算 bash 可能不在 /bin 下(如 FreeBSD 在 /usr/local/bin)
#!/usr/bin/env bash 可移植--从 PATH 找 bash 多一层 env 进程; 用户可以通过 PATH 注入任意 "bash"
#!/bin/sh 最大兼容性--所有 Unix 都有 /bin/sh 不知道它实际是 dash, bash 还是 ash,功能不确定

选择建议: 个人项目用 #!/usr/bin/env bash,分发给别人的脚本用 #!/bin/sh 并只写 POSIX 语法.

快速回顾

  • Shell = 内核之外的那层 "壳",人与内核之间的翻译器
  • Shell 进化树: Thompson sh → Bourne sh(POSIX 蓝本) → bash → zsh / fish
  • POSIX sh 是兼容性分水岭--bash, zsh, dash 都兼容,fish 和 csh 不兼容
  • Bash vs Zsh: 90% 语法相同,交互上 zsh(+oh-my-zsh)远超 bash,脚本上 bash 兼容性更好
  • Zsh 数组从 1 开始,bash 从 0 开始--跨 Shell 脚本最坑的差异
  • Fish 开箱即用但不兼容任何旧语法,适合只做交互的用户
  • 登录 Shell 读 ~/.zprofile,交互非登录读 ~/.zshrc,脚本不读配置
  • Shebang 决定了哪个 Shell 执行脚本--选 #!/bin/sh 为兼容性,选 #!/bin/bash 为确定性

动手练习

  1. 确认当前 Shell: 运行 echo $0echo $SHELL,确认两者是否一致. 不一致的话说明什么?
  2. 查看可用 Shell: 运行 cat /etc/shells 看看系统上有哪些 Shell 可用
  3. 临时切换 Shell: 启动 bash: 运行 bash,然后 echo $0,然后 exit 回到 zsh
  4. 查看 oh-my-zsh 配置: cat ~/.zshrc,找到主题和插件设置
  5. Shebang 测试: 创建一个测试脚本 test.sh: 第一行 #!/bin/bash,内容 echo "Hello from $0". 用 chmod +x test.sh && ./test.sh 执行. 然后改第一行为 #!/bin/zsh 再执行,看输出变化
  6. 数组索引差异: 运行 echo $arr[1](在 zsh 里),然后启动 bash 再运行 arr=(a b c); echo ${arr[1]}--验证数组索引差异