Zsh(Z Shell)与 Bash(Bourne Again Shell)是当今使用最广泛的两个 Unix Shell。虽然二者在语法层面高度相似,但在设计理念、默认行为、交互体验和配置体系上存在根本性差异。理解这些差异对于编写可移植脚本、配置高效开发环境至关重要。
一、核心哲学差异
flowchart BT
subgraph Bash["Bash 设计哲学"]
B1["POSIX 合规优先"]
B2["跨平台可移植性"]
B3["脚本执行效率"]
B4["最小资源占用"]
end
subgraph Zsh["Zsh 设计哲学"]
Z1["交互体验优先"]
Z2["功能丰富性"]
Z3["可定制性"]
Z4["开发者生产力"]
end
B1 --> B2
B2 --> B3
B3 --> B4
Z1 --> Z2
Z2 --> Z3
Z3 --> Z4
style Bash fill:#e6f3ff
style Zsh fill:#fff0e6
根本分歧:Bash 以“在任何地方都能运行”为目标,Zsh 以“让终端工作更愉悦”为目标。这一哲学差异决定了二者在几乎所有技术细节上的走向。
二、关键行为差异一览
| 对比维度 | Bash | Zsh |
|---|---|---|
| 数组索引起始 | 0-based | 1-based(可 KSH_ARRAYS 切换) |
| 默认词法切分 | 执行(按 IFS 拆分) | 不执行(需 $=var 或 SH_WORD_SPLIT) |
| 默认路径扩展 | 执行 Globbing | 不执行(需 $~var) |
| 扩展 Globbing | 需 shopt -s extglob |
默认启用 EXTENDED_GLOB |
| 浮点数运算 | 不支持(整数 only) | 原生支持 |
| 右侧提示符 | 不支持 | 原生支持 RPROMPT |
| 拼写纠正 | 不支持 | 内置 setopt CORRECT |
| 共享历史 | 需手动配置 | 内置 setopt SHARE_HISTORY |
| 选项设置 | set -o / shopt -s |
setopt / unsetopt |
| 启动时间(裸) | ~38 ms | ~82 ms |
| 内存占用(裸) | ~1.5 MB | ~3.2 MB |
| POSIX 合规性 | 完全合规 | 部分合规(emulate sh 模式) |
| macOS 默认 | 否(Catalina 后弃用) | 是(Catalina 起) |
| Linux 默认 | 是 | 否 |
三、词法切分:最经典的差异
这是从 Bash 迁移到 Zsh 时最常见的“坑”,也是 Zsh FAQ 中排名第一的问题。
3.1 Bash 的默认行为
# Bash
args="-a -l"
ls $args
# 实际执行:ls -a -l(词法切分后变成两个参数)
Bash 对未加引号的变量扩展默认执行词法切分(按 IFS 拆分)和路径扩展(Globbing)。
3.2 Zsh 的默认行为
# Zsh
args="-a -l"
ls $args
# 实际执行:ls "-a -l"(作为一个整体参数)
# 报错:ls: invalid option -- ' '
Zsh 默认不对未加引号的变量扩展执行词法切分和路径扩展。这在 Zsh 设计者看来才是正确的行为——Bash 的隐式拆分被 POSIX 标准强制要求,但本质上是一个设计缺陷。
3.3 兼容性处理
# 方法1:显式启用 Bash 风格词法切分
setopt SH_WORD_SPLIT
# 方法2:单次使用 $= 前缀
ls ${=args}
# 方法3:使用数组(推荐)
args=(-a -l)
ls "${args[@]}"
四、数组索引:0 vs 1
4.1 根本差异
# Bash
arr=(a b c)
echo "${arr[0]}" # 输出:a(第一个元素)
echo "${arr[1]}" # 输出:b
# Zsh
arr=(a b c)
echo "$arr[1]" # 输出:a(第一个元素)
echo "$arr[2]" # 输出:b
echo "$arr[0]" # 输出:a b c(整个数组!)
Zsh 的数组索引起始为 1,与绝大多数 Shell(csh、tcsh、fish、yash)一致。Bash 的 0-based 索引实际上是复制了 ksh 的设计。
4.2 Zsh 的设计理由
- 与
$@(位置参数从$1开始)保持一致 - 与
$argv(csh 风格)保持一致 - 数组长度 = 最后一个索引(非稀疏数组)
- 负索引
$arr[-1]表示最后一个元素(0-based 无法实现-0)
4.3 兼容性处理
# 启用 ksh/bash 风格数组
setopt KSH_ARRAYS
# 或始终使用偏移语法(跨 Shell 兼容)
echo "${arr[*]:0:1}" # 第一个元素(offset 始终 0-based)
五、Globbing 与路径扩展
5.1 Bash 的受限 Globbing
# Bash 基础通配符
ls *.txt # 匹配所有 .txt 文件
ls file?.txt # 匹配 file1.txt, fileA.txt 等
# 扩展 Globbing(需显式启用)
shopt -s extglob
ls !(README).txt # 排除 README.txt
ls @(foo|bar).txt # 匹配 foo.txt 或 bar.txt
5.2 Zsh 的强大 Globbing
# Zsh 默认启用 EXTENDED_GLOB
setopt EXTENDED_GLOB # 通常已默认启用
# 否定匹配
ls ^README.txt # 排除 README.txt
# 括号或
ls (foo|bar).txt
# Glob 限定符(Zsh 独有)
ls *.txt(N) # N: 无匹配时返回空列表(不报错)
ls *.txt(om) # om: 按修改时间排序(最旧在前)
ls *.txt(Om) # Om: 按修改时间排序(最新在前)
ls *.txt(Lk+100) # 匹配大于 100KB 的文件
ls *.txt(^D) # 排除目录
# 递归 Globbing
ls **/*.txt # 递归匹配所有子目录的 .txt
5.3 关键陷阱
# Bash 中无匹配时返回字面字符串
echo *.nonexistent # 输出:*.nonexistent
# Zsh 中无匹配时默认报错
echo *.nonexistent # zsh: no matches found: *.nonexistent
# Zsh 解决方案
setopt NO_NOMATCH # 无匹配时返回字面字符串
# 或
echo *.nonexistent(N) # 使用 N 限定符
六、算术运算
6.1 Bash:整数唯一
# Bash 仅支持整数运算
echo $((3 + 4)) # 7
echo $((3 / 2)) # 1(截断小数)
echo $((3.5 + 2)) # 语法错误!
# 浮点数需借助外部工具
echo "scale=2; 3.5 + 2" | bc # 5.5
6.2 Zsh:原生浮点支持
# Zsh 原生支持浮点数
echo $((3.5 + 2)) # 5.5
echo $((3 / 2.0)) # 1.5
# 声明浮点变量
typeset -F 2 num # 2位小数精度
num=3.14159
echo $num # 3.14
# 加载数学函数模块
zmodload zsh/mathfunc
echo $((sin(3.14))) # 0.00159...
七、提示符系统
7.1 Bash 提示符
# Bash 使用 PS1
PS1='\u@\h:\w\$ ' # user@host:dir$
# 颜色需手动添加 ANSI 转义
PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
# 无右侧提示符支持
7.2 Zsh 提示符
# Zsh 使用 PROMPT(或 PS1)
PROMPT='%n@%m:%~%# ' # user@host:dir%
# 丰富的转义序列
# %n = 用户名, %m = 主机名, %~ = 当前目录(~展开)
# %F{color} = 前景色, %K{color} = 背景色
# %B = 粗体, %U = 下划线
# 右侧提示符(Zsh 独有)
RPROMPT='%F{cyan}%D{%H:%M:%S}%f' # 右侧显示时间
# 拼写纠正提示符
setopt CORRECT
SPROMPT='zsh: correct '%R' to '%r' [nyae]? '
八、初始化文件体系
8.1 Bash 初始化文件
flowchart TD
A[Shell 启动] --> B{Login Shell?}
B -->|是| C["/etc/profile"]
B -->|否| D{Interactive?}
C --> E["~/.bash_profile<br>或 ~/.bash_login<br>或 ~/.profile"]
E --> F["source ~/.bashrc"]
D -->|是| G["~/.bashrc"]
D -->|否| H["BASH_ENV 变量"]
G --> I["/etc/bash.bashrc"]
style C fill:#e6f3ff
style E fill:#e6f3ff
style G fill:#e6f3ff
style I fill:#e6f3ff
| 文件 | 执行时机 | 用途 |
|---|---|---|
/etc/profile |
登录 Shell | 系统级环境变量 |
~/.bash_profile |
登录 Shell | 用户级环境变量、PATH |
~/.bashrc |
交互式非登录 Shell | 别名、函数、提示符 |
~/.bash_logout |
登录 Shell 退出 | 清理操作 |
8.2 Zsh 初始化文件
flowchart TD
A["Shell 启动"] --> B["/etc/zshenv"]
B --> C["~/.zshenv<br>始终加载"]
C --> D{"Login Shell?"}
D -->|是| E["/etc/zprofile"]
E --> F["~/.zprofile"]
D -->|否| G{"Interactive?"}
F --> G
G -->|是| H["/etc/zshrc"]
H --> I["~/.zshrc"]
I --> J{"Login Shell?"}
J -->|是| K["/etc/zlogin"]
K --> L["~/.zlogin"]
L --> M["退出时<br>~/.zlogout"]
style C fill:#fff0e6
style F fill:#fff0e6
style I fill:#fff0e6
style L fill:#fff0e6
| 文件 | 执行时机 | 用途 |
|---|---|---|
~/.zshenv |
始终 | 环境变量(PATH 等) |
~/.zprofile |
登录 Shell | 环境设置(Bash 的 .bash_profile 对应) |
~/.zshrc |
交互式 Shell | 别名、函数、提示符、选项 |
~/.zlogin |
登录 Shell(.zshrc 之后) | 登录后执行的外部命令 |
~/.zlogout |
登录 Shell 退出 | 清理操作 |
关键区别:Zsh 的 ~/.zshenv 在所有 Shell 调用时都会加载(包括非交互式脚本),而 Bash 没有对应文件。这使得 Zsh 的环境变量配置更集中。
九、选项系统
9.1 Bash 的双层选项
# POSIX 选项(set -o)
set -o errexit # 等价于 set -e
set -o nounset # 等价于 set -u
set -o pipefail
# Bash 特有选项(shopt)
shopt -s extglob # 启用扩展 Globbing
shopt -s nullglob # 无匹配时返回空
shopt -s dotglob # 包含隐藏文件
shopt -s globstar # 启用 ** 递归匹配
9.2 Zsh 的统一选项
# 所有选项通过 setopt/unsetopt 管理
setopt EXTENDED_GLOB # 扩展 Globbing(默认启用)
setopt NO_NOMATCH # 无匹配时不报错
setopt CORRECT # 拼写纠正
setopt SHARE_HISTORY # 跨会话共享历史
setopt AUTO_CD # 输入目录名自动 cd
setopt AUTO_PUSHD # cd 自动 pushd
setopt CHASE_LINKS # 解析符号链接
十、交互功能对比
10.1 Tab 补全
| 特性 | Bash | Zsh |
|---|---|---|
| 基础补全 | 支持 | 支持 |
| 菜单选择 | 需配置 | 默认支持 |
| 描述信息 | 不支持 | 支持(命令+描述) |
| 模糊匹配 | 不支持 | 支持 |
| 大小写不敏感 | 需配置 | 支持 |
| 补全 Git 分支 | 需插件 | 内置/插件丰富 |
# Zsh 补全示例
$ git che<TAB>
check-attr -- display gitattributes information
check-ref-format -- ensure that a reference name is well formed
checkout -- checkout branch or paths to working tree
checkout-index -- copy files from index to working directory
cherry -- find commits not merged upstream
cherry-pick -- apply changes introduced by some existing commits
10.2 历史管理
# Zsh 共享历史(跨所有终端会话)
setopt SHARE_HISTORY
# 历史相关选项
setopt HIST_IGNORE_DUPS # 忽略重复命令
setopt HIST_IGNORE_SPACE # 忽略以空格开头的命令
setopt HIST_REDUCE_BLANKS # 去除多余空格
setopt APPEND_HISTORY # 追加而非覆盖
Bash 默认每个终端会话维护自己的历史缓冲区,只有会话结束时才写入 .bash_history。
十一、脚本可移植性
11.1 最佳实践:Zsh 交互 + Bash 脚本
#!/usr/bin/env bash
# 所有脚本使用 Bash shebang,确保可移植性
set -euo pipefail
# ...
# ~/.zshrc
# 交互环境使用 Zsh,享受丰富功能
setopt CORRECT
setopt SHARE_HISTORY
# ...
11.2 Zsh 的 emulate 模式
# 在 Zsh 中临时模拟 sh/bash
emulate sh -c 'source my_bash_script.sh'
# 在函数内局部模拟
my_func() {
emulate -L bash # -L 表示局部选项
# 函数体使用 bash 语义
}
# 检查当前模拟模式
echo $(emulate) # 输出:zsh 或 sh 或 ksh
十二、性能对比
| 指标 | Bash | Zsh(裸) | Zsh + Oh My Zsh |
|---|---|---|---|
| 启动时间 | ~38 ms | ~82 ms | 500–1000+ ms |
| 内存占用 | ~1.5 MB | ~3.2 MB | 15–25 MB |
| 1000万次整数循环 | 14.2 s | 15.8 s | — |
| 10000次子 Shell 创建 | 4.1 s | 7.3 s | — |
结论:Bash 在启动时间和资源占用上优势明显,适合脚本和容器环境;Zsh 的交互功能带来的生产力提升远超其性能开销。
十三、迁移指南
13.1 Bash → Zsh
# 1. 复制基础配置
cp ~/.bashrc ~/.zshrc # 然后手动调整
# 2. 处理词法切分差异
# 在 .zshrc 顶部添加(如需兼容旧习惯)
setopt SH_WORD_SPLIT
# 3. 处理数组索引
# 或使用 KSH_ARRAYS,或改写为偏移语法
# 4. 安装 Oh My Zsh(可选)
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
13.2 Zsh → Bash
# 1. 复制配置
cp ~/.zshrc ~/.bashrc # 然后手动调整
# 2. 移除 Zsh 特有语法
# - $arr[1] → ${arr[0]}
# - ${=var} → $var
# - ${~var} → $var
# - **/*.txt → 启用 shopt -s globstar
# 3. 安装 bash-completion
sudo apt install bash-completion
十四、总结决策树
flowchart TD
A[选择 Shell] --> B{主要用途?}
B -->|编写脚本/CI/CD| C[选择 Bash]
B -->|日常交互/开发| D{追求极简?}
D -->|是| E[选择 Bash<br/>或 dash/ash]
D -->|否| F[选择 Zsh]
C --> G[#!/bin/bash<br/>set -euo pipefail]
F --> H[Oh My Zsh/Zinit<br/>+ Powerlevel10k]
style C fill:#ccffcc
style E fill:#ccffcc
style F fill:#ccccff
style G fill:#ccffcc
style H fill:#ccccff
最佳实践是双轨制:用 Zsh 作为交互 Shell 提升日常效率,用 Bash 编写所有需要分发的脚本确保可移植性。二者并非对立,而是互补。
原文 https://blog.csdn.net/2301_79518550/article/details/162583105