坑边闲话:Zsh 的配置笔者用了许多年,丰富的生态和现代化的配置,使得 Zsh 的体验非常良好。2023 国庆之后,我把 Zsh 的配置用模块化的方式重新维护了一遍,更加贴合日常习惯。

Linux zsh 配置指南·

毫无疑问,Bash 依旧是 linux shell 编程的首选。但是,Zsh 的强大生态和丰富插件却吸引了越来越多用户去使用。

选择 Shell 类语言进行编程不是个好的方案,因为它在流程控制方面有先天的弱势,计算能力更是趋近于无。在进行编程时,我习惯用 Python 编码,其中会调用短的系统命令完成任务执行,但是将流程控制交给 Python 解释器。因此对我而言,选择什么 Shell 不是难题,因为我不用 Shell 编程。

Shell 是用户与 Linux 内核进行交互的重要中间件,Shell 一般通过 exec 系列命令生成新的进程执行 shell script,进而完成用户的意图。Shell 程序可以从命令行中获取参数、命令,也可以通过 stdin (一般是文件)获取输入,两者在功能上是等价的。

Zsh、Bash 等,均是 Shell 思想的具体实现,他们或许在某些语法上非常相似,但是彼此并不能互相完全兼容。比如 Zsh 中执行的 echo 命令,一般是内置的快速实现,而非 /bin/echo,这些小差异带来了许多选择上的麻烦。

Bash 风靡了这么多年,毫无疑问是所有机器上必装的软件,因此用 Bash 语法编写 shell script 是最稳妥的。这里不再赘述。本文想介绍日常交互式使用场景下的更佳选择:zsh.

本文以 Debian 12 作为基础系统进行讲解。

1. Zsh 环境配置:轻量版·

将所有 Zsh 的配置放到一个文件里非常高效,尽管会面临一些管理上的弊端,但是这个方案在某些场景下还是很有用的,毕竟复制粘贴一次就够了。为此,笔者将先介绍如何配置基础的 Zsh 环境。

1.1 基础组件·

首先安装基础组件。

1
2
sudo apt update
sudo apt install zsh git curl wget

1.2 安装 oh-my-zsh·

Zsh 官网

oh-my-zsh 是一套开源的 zsh 配置,内置多种主题,样式比较美观。

使用如下命令可以从官方仓库在线安装 oh-my-zsh.

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

如果你使用的是 docker 环境,也可以使用 oh-my-zsh,但是需要安装较多的基础组件,容易导致镜像文件变大。

1
sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.5/zsh-in-docker.sh)"

一般的 docker image 缺乏全面的 locale 支持,因此强行使用默认的 oh-my-zsh 脚本安装会导致许多显示问题,使用上述为 docker 优化的配置,相对比较完美。

1.3. 配置 zsh 功能·

1.3.1 智能识别系统类型·

可以使用 unamecat /etc/os-release 等方式查看系统类型,并将其设置为环境变量,方便后续操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if [[ `uname -s` == "Linux" ]]; then
os_type=$(cat /etc/os-release | grep -i '^NAME')

# A. TrueNAS SCALE
if [[ `uname -r` == *"truenas"* ]]; then
os_type="TrueNAS_SCALE"

# B. OpenWrt
elif [[ $os_type == *"OpenWrt"* ]]; then
os_type="OpenWrt"

# C. Debian
elif [[ $os_type == *"Debian"* ]]; then
os_type="Debian"

# D. RedHat
elif [[ $os_type == *"Red Hat Enterprise Linux"* ]]; then
os_type="RedHat"

# E. Ubuntu
elif [[ $os_type == *"Ubuntu"* ]]; then
os_type="Ubuntu"

# F. FreeBSD
else [[ $os_type == *"FreeBSD"* ]]
os_type="FreeBSD"

fi
elif [[ `uname -s` == "Darwin" ]]; then
os_type="macOS"
else
os_type="unknown"
fi

1.3.2 设置合适的别名·

通过 alias 命令为常用的组合设置别名,可以有效增加日常使用便捷性。但是此处不推荐对较为大众化的命令进行别名覆盖,否则容易出现意想不到的问题。但是,alias ip=ip -color 这种无伤大雅的别名覆盖并不会存在太多隐患。

1
2
3
4
5
6
7
8
9
10
11
alias ll='ls -lFh'
alias la='ls -al'
alias l='ls -1'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
alias dir='ls'
alias ip='ip -color'
if [[ $os_type != "OpenWrt" ]]; then
# alias vim='nvim'
fi

1.3.3 安装 zsh 插件·

zsh 可以安装非常多好用的插件,本着保守的原则,这里推荐两个好用的插件:

Zsh 的插件通过 git 或某些包管理器下载,如果是手动管理就需要自己使用 git 了。假设你的 oh-my-zsh 按照上述步骤,安装在了 ~/.oh-my-zsh 目录,则可以先下载插件到合适的子目录:

1
2
git clone https://github.com/zsh-users/zsh-autosuggestions.git     ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ~/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting

随后将这两个插件的名字添加到 ~/.zshrc,大概如下所示:

1
2
3
4
5
6
7
plugins=(
# ...
zsh-autosuggestions
zsh-syntax-highlighting
# ...
)

1.3.4 设置合适的环境变量·

可以将常访问的目录作为环境变量,后续访问结合 zsh-autosuggestions 插件,可较为迅速地实现跳转。

为了提高脚本的适用性,这里使用之前定义的 os_type 环境变量进行了分支跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if [[ $os_type == "TrueNAS_SCALE" ]]; then
# 补全 PATH 变量,否则普通 sudo 用户无法使用 zpool 命令
export PATH=$PATH:/sbin
export PATH=$PATH:/mnt/Intel_750_RAID-Z1/newton/bin

# 存储池相关目录
export doc='/mnt/Intel_750_RAID-Z1/Documents'

export soft='/mnt/Samsung_PM983A_RAID-Z1/Software'

export download="/mnt/Toshiba_MG06S_RAID-Z1/Downloads"
export xunlei="${download}/Xunlei_Downloads"
export byr="${download}/BYR_Download"
else
export app_data='/home/newton/epyc-truenas-app_data'
export doc='/home/newton/epyc-truenas-documents'

export soft='/home/newton/epyc-truenas-software'

export download="/home/newton/epyc-truenas-downloads"
export xunlei="${download}/Xunlei_Downloads"
export byr="${download}/BYR_Download"
fi

1.3.5 TL;DR 懒人福利·

如果你只想拥有一个快速使用的环境,那么直接把下面的脚本拷贝到合适的位置即可。不过要手动安装 zsh 插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# * DESCRIPTION: Oh-My-Zsh / zsh Configuration File
# * Author: 刘鹏 / Peng Liu
# * Email: [email protected]
# * Date Created: 2020, Jun. 27
# * Data Updated: 2023, Sept. 10





autoload colors; colors

#======================== oh-my-zsh 设置 BEGIN ========================
# (1) zsh 安装位置
export ZSH=$HOME"/.oh-my-zsh"

# (2) 获取系统类型
if [[ `uname -s` == "Linux" ]]; then
os_type=$(cat /etc/os-release | grep -i '^NAME')

# A. TrueNAS SCALE
if [[ `uname -r` == *"truenas"* ]]; then
os_type="TrueNAS_SCALE"

# B. OpenWrt
elif [[ $os_type == *"OpenWrt"* ]]; then
os_type="OpenWrt"

# C. Debian
elif [[ $os_type == *"Debian"* ]]; then
os_type="Debian"

# D. RedHat
elif [[ $os_type == *"Red Hat Enterprise Linux"* ]]; then
os_type="RedHat"

# E. Ubuntu
elif [[ $os_type == *"Ubuntu"* ]]; then
os_type="Ubuntu"

# F. FreeBSD
else [[ $os_type == *"FreeBSD"* ]]
os_type="FreeBSD"

fi
elif [[ `uname -s` == "Darwin" ]]; then
os_type="macOS"
else
os_type="unknown"
fi

# zsh 主题,软件默认的是 robbyrussell
ZSH_THEME="agnoster"

# 自动补全大小写敏感设置为 false
CASE_SENSITIVE="false"

# 开启自动更新
export UPDATE_ZSH_DAYS=1
DISABLE_AUTO_UPDATE="true"

# 开启自动纠正错误
ENABLE_CORRECTION="true"
#======================== oh-my-zsh 设置 END ========================





#======================== 插件配置 BEGIN ========================
plugins=(
bundler
dotenv
git
macos
rake
rbenv
ruby
zsh-autosuggestions
zsh-syntax-highlighting
)
#======================== 插件配置 END ========================





#======================== 启用 oh-my-zsh 配置 BEGIN ========================
source $ZSH/oh-my-zsh.sh
#======================== 启用 oh-my-zsh 配置 END ========================





#======================== pip 设置 BEGIN ========================
# (1) pip 默认设置为 pip3
alias pip='pip3'
alias ipython='ipython3'
# (2) pip 自动补全
function _pip_completion {
local words cword
read -Ac words
read -cn cword
reply=( $( COMP_WORDS="$words[*]" \
COMP_CWORD=$(( cword-1 )) \
PIP_AUTO_COMPLETE=1 $words[1] ) )
}
compctl -K _pip_completion pip3
# (3) 设置 Python3-pip 的自动更新函数
function _pip_update () {
pip3 list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 sudo -H pip3 install -U
}
#======================== pip 设置 END ========================





#======================== 别名 BEGIN ========================
alias ll='ls -lFh'
alias la='ls -a'
alias l='ls -1'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
alias dir='ls'
alias ip='ip -color'
alias dirs='dirs -lpv'
#======================== 别名 END ========================





#======================== Terminal Color BEGIN ========================
LS_COLORS=$LS_COLORS:'di=0;36' ; export LS_COLORS
#======================== Terminal Color END ========================





#======================== TeXLive 配置 BEGIN ========================
# TeX Live 的相关设置:
# (1) 导入系统的参考手册
# (2) TeX Live 配置与系统类型有关,与 Linux 发行版无关,因此只需用 uname 查询内核即可
is_texlive_installed="false"
function _set_texlive () {
# (1) 判断 texlive 是否已安装
texlive_folder='/usr/local/texlive/'
if [ -d ${texlive_folder} ]; then
max_texlive_version=`ls -1 -r ${texlive_folder} | grep -E '20*' | head -n 1`
if [[ -n ${max_texlive_version} ]]; then

# A. 更改环境变量
is_texlive_installed="true"

# B. 设置操作系统对应的环境变量
if [[ $os_type == "macOS" ]]; then
export MANPATH=/usr/local/texlive/${max_texlive_version}/texmf-dist/doc/man:${MANPATH}
export INFOPATH=/usr/local/texlive/${max_texlive_version}/texmf-dist/doc/info:${INFOPATH}
export PATH=/usr/local/texlive/${max_texlive_version}/bin/universal-darwin:${PATH}
elif [[ $os_type == "Linux" ]]; then
export MANPATH=/usr/local/texlive/${max_texlive_version}/texmf-dist/doc/man:${MANPATH}
export INFOPATH=/usr/local/texlive/${max_texlive_version}/texmf-dist/doc/info:${INFOPATH}
export PATH=/usr/local/texlive/${max_texlive_version}/bin/x86_64-linux:${PATH}
else
# 抛出异常,等待日后处理此类系统类型
echo "系统类型无法识别"
fi
fi
fi
}
# (2) 执行 set_texlive 函数
_set_texlive;
#======================== TeXLive 配置 END ========================





#======================== UPDATE 设置 BEGIN ========================
# 系统自动更新,该函数不区分系统
function os-update () {
# (1) 更新 TeX Live
print -P "%F{cyan}Step 1/3 tlmgr update%f"
if [[ $is_texlive_installed == "true" ]]; then
sudo tlmgr update --self --all
elif [[ $is_texlive_installed == "false" ]]; then
print -P "%F{white}INFO: No installation of texlive was detected, please check it%f"
fi

# (2) 更新 miniconda Python,不使用系统 Python
print -P "%F{cyan}Step 2/3: Updating Miniconda%f"
print -P "%F{white}NOTE: Only the base virtual environment will be updated.%f"
conda activate base
conda upgrade -n base --yes --all

# (3) 更新系统包管理器下的软件
case $os_type in
macOS)
print -P "%F{cyan}Step 3/3: Updating macOS Homebrew%f"
brew update
brew upgrade
;;
Ubuntu)
if command -v apt-get >/dev/null 2>&1; then
print -P "%F{cyan}Step 3/3: Updating Ubuntu%f"

cmds=(
"sudo apt-get update"
"sudo apt-get upgrade --just-print"
"sudo apt-get upgrade"
"sudo apt-get dist-upgrade"
"sudo apt-get -y autoremove"
"sudo apt-get -y autoclean"
)

for cmd in "${cmds[@]}"; do
echo
print -P "%F{yellow}> $cmd%f"
eval $cmd
done
else
print -P "%F{red}ERROR: System package manager not supported.%f"
fi
;;
Debian)
if command -v apt-get >/dev/null 2>&1; then
print -P "%F{cyan}Step 3/3: Updating Debian%f"

cmds=(
"sudo apt-get update"
"sudo apt-get upgrade --just-print"
"sudo apt-get upgrade"
"sudo apt-get dist-upgrade"
"sudo apt-get -y autoremove"
"sudo apt-get -y autoclean"
)

for cmd in "${cmds[@]}"; do
echo
print -P "%F{yellow}> $cmd%f"
eval $cmd
done
else
print -P "%F{red}ERROR: System package manager not supported.%f"
fi
;;
RedHat)
if command -v yum >/dev/null 2>&1; then
echo $fg[cyan]Step 3/3: Updating RedHat$reset_color
sudo yum update
else
print -P "%F{red}ERROR: System package manager not supported.%f"
fi
;;
*)
print -P "%F{red}ERROR: Unsupported operating system: $os_type.%f"
;;
esac
}
#======================== UPDATE 设置 END ========================





#======================== zsh Key-Binding BEGIN ========================
# 防止 SSH Shell 某些键失效
bindkey "\e[1~" beginning-of-line
bindkey "\e[4~" end-of-line
bindkey "\e[5~" beginning-of-history
bindkey "\e[6~" end-of-history

# for rxvt
bindkey "\e[8~" end-of-line
bindkey "\e[7~" beginning-of-line
# for non RH/Debian xterm, can't hurt for RH/DEbian xterm
bindkey "\eOH" beginning-of-line
bindkey "\eOF" end-of-line
# for freebsd console
bindkey "\e[H" beginning-of-line
bindkey "\e[F" end-of-line
# completion in the middle of a line
bindkey '^i' expand-or-complete-prefix

# Fix numeric keypad
# 0 . Enter
bindkey -s "^[Op" "0"
bindkey -s "^[On" "."
bindkey -s "^[OM" "^M"
# 1 2 3
bindkey -s "^[Oq" "1"
bindkey -s "^[Or" "2"
bindkey -s "^[Os" "3"
# 4 5 6
bindkey -s "^[Ot" "4"
bindkey -s "^[Ou" "5"
bindkey -s "^[Ov" "6"
# 7 8 9
bindkey -s "^[Ow" "7"
bindkey -s "^[Ox" "8"
bindkey -s "^[Oy" "9"
# + - * /
bindkey -s "^[Ol" "+"
bindkey -s "^[Om" "-"
bindkey -s "^[Oj" "*"
bindkey -s "^[Oo" "/"
#======================== zsh Key-Binding END ========================





#======================== 环境变量 BEGIN ========================
export EDITOR='nvim'
if [[ $os_type == "TrueNAS_SCALE" ]]; then
# 补全 PATH 变量,否则普通 sudo 用户无法使用 zpool 命令
export PATH=$PATH:/sbin
export PATH=$PATH:/mnt/Intel_750_RAID-Z1/newton/bin

# 存储池相关目录
export doc='/mnt/Intel_750_RAID-Z1/Documents'

export soft='/mnt/Samsung_PM983A_RAID-Z1/Software'

export download="/mnt/Toshiba_MG06S_RAID-Z1/Downloads"
export xunlei="${download}/Xunlei_Downloads"
export qbit="${download}/qBittorrent_Downloads"
export byr="${qbit}/byr-finished"

export jellyfin="${media}/db_jellyfin"
export tmm="${jellyfin}/tmm"
else
export appdata="/home/newton/epyc-truenas-app_data"
export doc='/home/newton/epyc-truenas-documents'

export soft='/home/newton/epyc-truenas-software'

export download="/home/newton/epyc-truenas-downloads"
export xunlei="${download}/Xunlei_Downloads"
export qbit="${download}/qBittorrent_Downloads"
export byr="${qbit}/byr-finished"

export jellyfin="${media}/db_jellyfin"
export tmm="${jellyfin}/tmm"

# 其他相关
export harbor="app-registry.proxy.littlenewton.cn"
export PATH=${HOME}/bin/:$PATH
fi
#======================== 环境变量 END ========================


# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
__conda_setup="$('/home/newton/bin/miniconda3/bin/conda' 'shell.zsh' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
eval "$__conda_setup"
else
if [ -f "/home/newton/bin/miniconda3/etc/profile.d/conda.sh" ]; then
. "/home/newton/bin/miniconda3/etc/profile.d/conda.sh"
else
export PATH="/home/newton/bin/miniconda3/bin:$PATH"
fi
fi
unset __conda_setup
# <<< conda initialize <<<





#======================== K8S BEGIN ========================
function enable_k8s_autocompletion {
if command -v kubectl >/dev/null 2>&1; then
if kubectl get node $(hostname) -o=jsonpath='{.metadata.labels}' | grep -q 'node-role.kubernetes.io/control-plane'; then
source ~/.zsh/kubectl_auto_completion.sh
source ~/.zsh/kubeadm_auto_completion.sh
fi
fi
}
current_hostname=$(hostname)

if [[ "$current_hostname" == "epyc-debian" ]]; then
enable_k8s_autocompletion;
fi
#======================== K8S END ========================

2. Zsh 环境配置:高级版·

2.1 全自动化的配置·

首先,要保证家目录有 .zshenv 文件。编辑文件 vim ~/.zshenv 并配置 XDGDesktop 相关的环境变量。可通过如下命令完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
echo '# XDG 规范的路径
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_DATA_HOME="$HOME/.local/share"
export XDG_CACHE_HOME="$HOME/.cache"

# Zsh related config file.
export ZDOTDIR="$XDG_CONFIG_HOME/zsh"
export HISTFILE="$ZDOTDIR/.zhistory" # History filepath
export HISTSIZE=10000 # Maximum events for internal history
export SAVEHIST=10000 # Maximum events in history file

# Zim related config file.
export ZIM_HOME="$XDG_DATA_HOME/zim"' > ~/.zshenv && source ~/.zshenv

然后配置 Zsh:

1
2
sudo chsh -s $(which zsh)
git clone https://github.com/LittleNewton/zsh-config.git ~/.config/zsh

配置 Tmux,流程全部自动化:

1
git clone https://github.com/LittleNewton/tmux-config.git ~/.config/tmux

随后,创建一个 Tmux session,使用快捷键 prefix + I 安装额外插件。

2.2 重点讲解·

高级版对比普通版的配置,主要是在模块化方面做了优化。但是正如高级语言所面临的问题,Zsh 脚本进行模块化也面临同样的问题,那就是命名空间和时序关系。笔者的配置文件需要加载十几个模块:

  1. 或许系统类型并导出为环境变量。因为 Zsh 脚本需要处理不同的操作系统,当然主要是 macOS 和 Linux,但是为了泛化脚本的可用性,这一步不可避免。
  2. 初始化一些环境变量。比如 TrueNAS 依赖 ${HOME}/bin 里的脚本做存储管理。
  3. 设置别名。常见的命令,比如 iproute2 系列,有别名会更加方便。
  4. Zsh 插件的初始化。先安装 Oh-My-Zsh,然后初始化 Zim,随后初始化 Oh-My-Zsh. 这个顺序不能乱。该结果由实际测试得来,具体原因未知。
  5. 按键映射
  6. conda 环境变量映射 (os-type)
  7. latex 环境变量映射 (os-type)
  8. os-update 函数定义 (os-type)
  9. Kubernetes 补全 (os-type)

总结·

本文以 Debian 12 为例,展示了如何配置 ~/.zshrc,并提高自己日常使用 Linux 的效率。Linux 的高级境界就是给自己写脚本,比如:

  • 写 Linux 脚本以简化系统级工作流程
  • 写 VimScript / VimLua 脚本简化、加速文本编辑、程序调试速度

数字时代,开发能力才是决定性的技能。