1. 为什么叫 Shell(壳)?
前几篇反复提到 "Shell 只是一个普通的用户态程序". 但为什么这个程序叫 "壳"?
这个名字来自 Unix 的设计哲学--分层:
|
**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 模式启动:
|
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 内容很简单:
|
真正的逻辑在 ~/.oh-my-zsh/oh-my-zsh.sh 里--它加载主题, 插件, lib 目录下的工具函数. 只需要在 ~/.zshrc 最后追加自己的自定义配置即可.
5. Fish 的理念: 开箱即用 vs 可定制
bash 和 zsh 的共同模式是: 裸 Shell 很朴素 → 通过各种配置/插件/主题变好看. fish 反其道而行: 出厂就好看,不需要配置.
|
但 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
|
/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/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为确定性
动手练习
- 确认当前 Shell: 运行
echo $0和echo $SHELL,确认两者是否一致. 不一致的话说明什么? - 查看可用 Shell: 运行
cat /etc/shells看看系统上有哪些 Shell 可用 - 临时切换 Shell: 启动 bash: 运行
bash,然后echo $0,然后exit回到 zsh - 查看 oh-my-zsh 配置:
cat ~/.zshrc,找到主题和插件设置 - Shebang 测试: 创建一个测试脚本
test.sh: 第一行#!/bin/bash,内容echo "Hello from $0". 用chmod +x test.sh && ./test.sh执行. 然后改第一行为#!/bin/zsh再执行,看输出变化 - 数组索引差异: 运行
echo $arr[1](在 zsh 里),然后启动bash再运行arr=(a b c); echo ${arr[1]}--验证数组索引差异