知识广场

按学科筛选:计算机科学 / 软件工程
清除筛选

«计算机科学 / 软件工程» 分类下共 42 篇帖子

fzf:把模糊查找塞进 shell 的每个角落

## 起因 终端里"找东西"老操作: - `cd ../../../some/path` —— 路径长 + 容易记错 - `Ctrl-R` 反向搜历史 —— 一次只能展示一条 - `git checkout <Tab>` —— 分支多了 tab 全列出来 - `kill <pid>` —— 先 `ps` 查 PID 再 kill `fzf` 给所有这些场景加了模糊搜索 + 实时筛选 + 多选。装一次,所有地方 都香。 ## 装 ```bash # macOS brew install fzf $(brew --prefix)/opt/fzf/install # 装 shell 集成 # Ubuntu/Debian apt install fzf # 或者源码(最新版) git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` `install` 脚本问你装不装 `Ctrl-T`(粘文件)/`Ctrl-R`(搜历史)/`Alt-C` (cd 目录)键绑定 → 全选 yes。 ## 基础体验 shell 里随便敲: ```bash $ fzf ``` 打开一个全屏列表(默认是当前目录所有文件),上下选 / 输入字符模糊 过滤 / 回车确认。 ```bash $ vim $(fzf) # 模糊找文件然后 vim 打开 $ cat $(fzf) ``` ## 必装的 shell 绑定 ```bash Ctrl-T # 触发"插入文件路径"模式(任何 cmd 后都能用) Ctrl-R # 反向搜命令历史,模糊匹配 Alt-C # cd 到模糊选中的子目录 ``` 例: ```bash $ git diff <Ctrl-T> # 弹列表选文件 → 插入路径 $ vim <Ctrl-T> # 同上 ``` `Ctrl-R` 把内建反搜替换成多行筛选,找老命令快 10 倍。 ## 与 ripgrep / fd 配合 `fd`(rust find)+ `rg`(ripgrep)+ `fzf` 三件套: ```bash # ~/.zshrc export FZF_DEFAULT_COMMAND='fd --type f --hidden --exclude .git' export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_ALT_C_COMMAND='fd --type d --hidden --exclude .git' ``` `Ctrl-T` 只列文件 + 隐藏文件 + 排除 .git,速度起飞(fd 比 find 10x 快)。 ## 实战 alias ```bash # kill 进程 with fzf alias fkill='ps -ef | fzf --multi | awk "{print \$2}" | xargs kill' # git checkout 分支模糊选 gco() { local branch=$(git branch --all | grep -v HEAD | sed 's/^..//' | fzf) git checkout "$branch" } # cd 到最近用过的目录 cdh() { local dir=$(dirs -p | uniq | fzf) cd "$dir" } # kubectl pod fzf kpod() { kubectl get pods --all-namespaces | fzf } # tmux session 切换 tms() { local session=$(tmux ls -F '#{session_name}' | fzf) tmux switch-client -t "$session" } ``` 每条都是日常高频,5 个字符内启动 + 模糊选 + 操作。 ## preview 窗口 最强 feature:右侧实时显示选中项预览。 ```bash fzf --preview 'cat {}' # 文件用 bat(高亮) fzf --preview 'bat --color=always {}' --preview-window right:60% # git log fzf git log --oneline | fzf --preview 'git show --color {1}' # JSON file 用 jq fzf --preview 'jq -C . {}' ``` `{}` 是 placeholder 选中项。`{1}` 是第一字段(空格分割)。 ## 多选 `--multi` / `-m` 模式,`Tab` 选 / `Shift-Tab` 反选。 ```bash # 选多个文件 git add git add $(git status --short | awk '{print $2}' | fzf -m) # 选多个 PR 关闭 gh pr list | fzf -m | awk '{print $1}' | xargs -I{} gh pr close {} ``` ## fzf 嵌入工具 很多 CLI 把 fzf 当 picker: - `forgit`:git 操作 + fzf 选 hunk / commit - `fz`:autojump 替代,fzf 选历史目录 - `peco`:fzf 的另一选择(go 写) - `sk`(skim):rust 写的 fzf 兼容 ## fzf as filter(programmable) 不只是交互工具,也是 Unix filter: ```bash echo -e "apple\nbanana\norange" | fzf # 交互选 echo -e "apple\nbanana\norange" | fzf -f "ap" # 非交互过滤 echo -e "apple\nbanana\norange" | fzf -f "ap" --print-query ``` `--filter`/`-f` 非交互模式,可以塞 pipeline。 ## tmux 集成 ```bash fzf-tmux -p 70%,70% # 在 tmux popup 里跑 fzf ``` 不会污染当前窗口。tmux 3.2+ 才有 popup 支持。 ## 性能 fzf 是 Go 写的,单线程能扫几百万行(typical < 100ms)。 benchmark: | 文件数 | 启动时间 | |---|---| | 10k | 30ms | | 100k | 100ms | | 1M | 800ms | 100w 文件项目(monorepo)也 sub-second。 ## 与替代品对比 - **peco**(go):同代,UI 略不同,更新少 - **sk / skim**(rust):兼容 fzf,速度更快,preview 略弱 - **selecta**(ruby):老 + 慢,过时 fzf 仍是事实标准。 ## 我的 .zshrc 完整片段 ```bash # fzf [ -f ~/.fzf.zsh ] && source ~/.fzf.zsh export FZF_DEFAULT_COMMAND='fd --type f --hidden --exclude .git' export FZF_DEFAULT_OPTS='--height 50% --layout=reverse --border --preview-window right:50%' export FZF_CTRL_T_OPTS="--preview 'bat --color=always --line-range :100 {}'" export FZF_ALT_C_OPTS="--preview 'eza --tree --color=always {} | head -50'" export FZF_CTRL_R_OPTS="--preview 'echo {}' --preview-window down:3:wrap" # alias alias gco='git_checkout_fzf' alias gst='git_status_fzf' git_checkout_fzf() { git branch --all --color=always | grep -v HEAD | \ fzf --ansi --preview 'git log --color {-1} -10 --oneline' | \ sed 's/.* //' | xargs git checkout } ``` ## 踩过的坑 1. **shell 启动慢**:fzf 装多个集成 → zsh 启动 +200ms。 `time zsh -i -c exit` 测一下,找 culprit。 2. **`Ctrl-R` 跟其它工具冲突**:tmux / vim / readline 都用 Ctrl-R。 fzf 集成默认覆盖 bash/zsh 的内建,要小心副作用。 3. **`fd` 在 Debian 名字是 `fdfind`**:装时 `apt install fd-find` → 命令叫 `fdfind`。alias 一下 `alias fd=fdfind`。 4. **preview 卡**:preview 命令慢(如 git show 大 commit)→ fzf 看着卡。 `--preview-window noborder` + 优化 preview 命令。 5. **macOS 与 zsh 5.9 兼容**:老 zsh 可能 fzf widget 报错。升 zsh / brew upgrade zsh。

tmux 配置 prefix + 鼠标 + 状态栏(终端复用的最小可用版)

tmux 让你在一个终端窗口里挂多个 session / window / pane,断开 SSH 后 进程继续跑。任何远程开发都该用 tmux 或 screen。 ## 安装 ```bash # Debian / Ubuntu sudo apt install -y tmux # macOS brew install tmux tmux -V # 确认 >= 3.0 ``` ## 最小配置 `~/.tmux.conf` ```bash # prefix 从 Ctrl-B 改成 Ctrl-A(更顺手;和 GNU readline 冲突的时候 set-key -r 解决) unbind C-b set -g prefix C-a bind C-a send-prefix # 256 色 + 真彩 set -g default-terminal 'tmux-256color' set -ga terminal-overrides ',xterm-256color:Tc' # 鼠标(点击切 pane、滚轮翻历史、拖边界改大小) set -g mouse on # 窗口编号从 1 开始(数字键近一点) set -g base-index 1 set -g pane-base-index 1 set -g renumber-windows on # 历史滚动行数 set -g history-limit 100000 # 切 pane 用 vim 风格 hjkl bind h select-pane -L bind j select-pane -D bind k select-pane -U bind l select-pane -R # 切窗口用 Alt-数字 bind -n M-1 select-window -t :1 bind -n M-2 select-window -t :2 bind -n M-3 select-window -t :3 bind -n M-4 select-window -t :4 bind -n M-5 select-window -t :5 # split 用 | 和 -(更直观) bind | split-window -h -c "#{pane_current_path}" bind - split-window -v -c "#{pane_current_path}" unbind '"' unbind % # 重载配置 bind r source-file ~/.tmux.conf \; display "Config reloaded" # 拷贝模式用 vi 键位 setw -g mode-keys vi bind -T copy-mode-vi v send-keys -X begin-selection bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'xclip -in -selection clipboard' # escape 时延(vim 模式必备,否则 ESC 后等半秒才响应) set -sg escape-time 10 # 状态栏:左边 session 名,右边时间 + 主机 set -g status-style 'bg=#2b2d3a fg=#cdd6f4' set -g status-left '#[fg=#a6e3a1,bold] #S #[default]' set -g status-right '#[fg=#89b4fa]%H:%M #[fg=#fab387]%Y-%m-%d #[fg=#cba6f7]#h ' set -g status-left-length 30 set -g status-right-length 50 # 当前窗口高亮 setw -g window-status-current-style 'fg=#1e1e2e bg=#a6e3a1 bold' setw -g window-status-current-format ' #I:#W ' setw -g window-status-format ' #I:#W ' ``` ## 日常用法 | 按键 | 动作 | |---|---| | `tmux` | 新建一个 session | | `tmux new -s mywork` | 新建命名 session | | `tmux ls` | 列所有 session | | `tmux a -t mywork` | attach 到指定 session | | **Prefix + c** | 新 window | | **Prefix + ,** | 重命名当前 window | | **Prefix + n / p** | 下一个 / 上一个 window | | **Prefix + 1..9** | 跳到第 N 个 window | | **Prefix + w** | 列 window(可选) | | **Prefix + \|** | 横分 pane(按配置) | | **Prefix + -** | 纵分 pane | | **Prefix + hjkl** | 切 pane | | **Prefix + x** | 关掉当前 pane | | **Prefix + d** | detach(session 继续跑) | | **Prefix + [** | 进入滚动 / copy 模式 | | **q** | 退出 copy 模式 | | **Prefix + ?** | 当前所有快捷键 | ## SSH + tmux 远程机器: ```bash ssh server tmux new -s work # 干活... # Ctrl-a d (detach),断开 SSH ``` 下次: ```bash ssh server tmux a -t work # 之前的所有进程还在 ``` 更进一步——SSH 自动 attach: ```bash # ~/.bashrc on server if [[ -z "$TMUX" && -n "$SSH_TTY" ]]; then tmux a -t main 2>/dev/null || tmux new -s main fi ``` ## tmux + 项目 每个项目一个 session,每个 session 多个 window(如 "vim"、"shell"、"logs"): ```bash tmux new -s myproj -d # 后台建 tmux rename-window -t myproj:0 'vim' tmux send-keys -t myproj:0 'nvim .' Enter tmux new-window -t myproj:1 -n 'shell' tmux new-window -t myproj:2 -n 'logs' tmux send-keys -t myproj:2 'tail -f log/app.log' Enter tmux a -t myproj ``` 这套脚本化的 session 启动用 `tmuxinator` / `tmuxp` 包管,写一份 YAML 配 所有 project layout。 ## 拷贝粘贴 进入 copy 模式(Prefix + `[`),用 vi 键位 hjkl 移光标,`v` 开始选择, `y` 拷贝。拷贝目标是 X11 clipboard(macOS 上 `pbcopy`、Wayland 上 `wl-copy`): ```bash # Wayland 版本: bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'wl-copy' # macOS 版本: bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'pbcopy' ``` OSC 52 escape sequence 让 tmux 通过终端把内容传到本地剪贴板, 不需要 X11 forward: ```bash set -g set-clipboard on ``` iTerm2 / Alacritty / WezTerm / kitty / 现代 Windows Terminal 都支持。 ## 重命名 / 整理 ```bash tmux rename-session -t old new tmux move-window -s myproj:3 -t myproj:1 tmux kill-session -t old tmux kill-window -t myproj:3 ``` ## 踩过的坑 - 修改 `.tmux.conf` 后没 reload:`tmux source-file ~/.tmux.conf` 或者 prefix-r(如果配置了)。 - 配置中颜色不显示:终端不支持真彩色(macOS 自带 Terminal); 换 iTerm2 / Alacritty / WezTerm。 - `escape-time` 不调,vim 里按 ESC 进 normal 模式有半秒延迟, 极其难用。设 10ms。 - pane 拖动会触发 mouse;想用鼠标选文字复制到本地反而困难。 按住 Shift 拖能临时绕过 tmux 的 mouse capture,让终端模拟器原生处理。

mise(前 rtx):单工具管理 Node / Python / Go / Ruby 版本

## 起因 每个项目用不同版本的 runtime / SDK: - 项目 A:Node 18 + Python 3.11 - 项目 B:Node 20 + Python 3.12 - 项目 C:Go 1.22 + Bun 1.1 老办法各种 version manager: - nvm(Node) - pyenv(Python) - rbenv(Ruby) - gvm / goenv(Go) 每个工具一套 shell 集成 / shim 机制 → 启动慢 + 配置碎。 `mise`(Rust 写的,前身 rtx,前前身 asdf 的现代 fork)是**一个工具 管理所有版本**。`.mise.toml` 文件描述项目所需版本,进目录自动切。 ## 装 ```bash # macOS brew install mise # Linux curl https://mise.run | sh # 集成 shell echo 'eval "$(mise activate zsh)"' >> ~/.zshrc ``` ## 项目用 ```bash cd ~/projects/myapp # 给项目指定 Node 20 + Python 3.12 mise use node@20 [email protected] # 自动写到 .mise.toml cat .mise.toml # [tools] # node = "20" # python = "3.12" # 装这俩 mise install ``` 进入这目录 → mise 自动激活对应版本: ```bash $ cd ~/projects/myapp $ node --version v20.18.0 $ python --version Python 3.12.4 $ cd ~/projects/other $ node --version v18.20.0 # 自动切换 ``` ## .mise.toml 完整例 ```toml # .mise.toml(项目根,commit 进 git) [tools] node = "20.18.0" # 精确版本 python = "3.12" # 主版本 go = "1.22" bun = "latest" # 最新 "npm:pnpm" = "9" # 通过 npm 装 pnpm # 环境变量 [env] NODE_ENV = "development" PYTHONUNBUFFERED = "1" _.file = ".env" # 加载 .env 文件 # 项目 task(取代 justfile / Makefile,简单场景) [tasks.dev] run = "node server.js" [tasks.test] run = "pnpm test" ``` `mise install` 装全部 → 进目录自动用。 ## 比 asdf 快多少 mise 是 asdf 的 Rust rewrite + 改进: | | asdf | mise | |---|---|---| | 语言 | Bash | Rust | | shell hook 延迟 | 50-200ms | < 5ms | | 工具机制 | shim | env-based + shim | | 配置 | .tool-versions | .mise.toml | | 任务系统 | 无 | 有(取代 just) | | 装速度 | 慢 | 快 | cd 到项目目录,asdf 的 shell hook 拖 100ms+。mise 几乎无感。 ## 多 runtime + 同时 ```toml [tools] node = ["20", "22"] # 两个版本都装 # 默认用第一个 → node 是 20 # 调 22:mise exec node@22 -- node --version ``` ## 共享 mise.toml + 个人 override ```toml # .mise.toml(commit) [tools] node = "20" python = "3.12" # .mise.local.toml(gitignore,个人覆盖) [tools] python = "3.13" # 我本地试 3.13 ``` `.mise.local.toml` 覆盖 `.mise.toml`,团队不受影响。 ## 全局默认 ```bash mise use -g node@22 mise use -g [email protected] ``` 写到 `~/.config/mise/config.toml`。没有 `.mise.toml` 的目录用全局 版本。 ## 工具源 mise 用 asdf 插件 + 内置后端: ```bash # 列出可装 mise ls-remote node mise ls-remote python # 通过 npm / pip / cargo 后端装 mise use "npm:typescript@5" mise use "pipx:black@24" mise use "cargo:ripgrep@13" ``` 也支持 GitHub release / ubi / pre-compiled binary 等。 ## task runner ```toml [tasks.lint] description = "Run linters" run = """ ruff check . pnpm tsc --noEmit """ [tasks.test] depends = ["lint"] run = "pytest" ``` ```bash $ mise tasks # 列出 $ mise run test # 跑(先跑 lint) ``` 替代 justfile 的简单场景。复杂的还是 just 强。 ## 环境变量 + secrets ```toml [env] DATABASE_URL = "postgres://localhost/myapp" _.file = ".env.local" # 也加载这文件 _.path = ["./bin", "./node_modules/.bin"] # 加 PATH ``` 进目录自动 export → 写脚本不用 source。 `.env.local` 在 .gitignore,本地 secrets。 ## CI 用 mise ```yaml # GitHub Actions - uses: jdx/mise-action@v2 with: install: true - run: mise run test ``` CI 装的版本 = `.mise.toml` 里的版本 = 本地版本。一致性保证。 ## 与 Docker dev container 对比 | | mise | Docker dev container | |---|---|---| | 隔离 | 进程级 | 完全隔离 | | 启动 | 即时 | 启动 container 几秒 | | IDE | 原生(无远程) | VS Code Remote | | 文件 IO | 原生 | bind mount 可能慢(mac) | | 生产一致性 | 中(runtime 一致 OS 可能不同) | 高 | 小项目 / 单人 → mise 够用 + 更快。 大项目 + 多 OS 团队 → devcontainer 更靠谱。 我自己 95% 项目 mise,少数客户项目 devcontainer。 ## 从 nvm 迁移 ```bash # 卸 nvm rm -rf ~/.nvm # 从 .zshrc 删 nvm source # 装 mise + 集成 # 老的 .nvmrc 自动识别(兼容) echo "v20" > .nvmrc mise install ``` `.nvmrc` / `.python-version` / `.tool-versions` mise 全识别。 不强制迁到 `.mise.toml`,但建议(功能更强)。 ## 踩过的坑 1. **shell 没集成**:`node --version` 返回老版本。mise 必须 shell hook 才能切。`mise activate` 加 rc 文件 + 重开终端。 2. **CI 装很慢**:mise 第一次装 Python 3.12 编译几分钟。 `MISE_PYTHON_COMPILE=0` 用预编译 binary(uv 同样做法)。或者 cache `~/.local/share/mise/installs`。 3. **跟 asdf 冲突**:装了 asdf 又装 mise → PATH 混。卸一个。 4. **某些工具没插件**:罕见工具 mise 没现成插件。可以 `mise plugin install <git-url>` 加 asdf 插件用。 5. **`mise use` 不写版本会写 latest 到 .mise.toml**:commit 进 git 后别人装的可能是更新版本。建议明确版本号。

Helix 编辑器:开箱即用的 modal editor 替代 vim/neovim

## 起因 我用 Neovim 几年,写过 ~200 行 Lua 配置。换电脑要折腾环境;同事抄 config 也要解释半天为什么这个插件做这个事。 听说 Helix 是 "Vim 改进版" 而且**零配置**就有 LSP / Treesitter / fuzzy 搜索,试一周。 ## 与 Vim 的区别 Helix 的设计借鉴 Kakoune:**先选后操**(selection-first)。 Vim 是动词→名词:`d w` = delete word(先 delete 后选)。 Helix 是名词→动词:`w d` = select word(看到 selection 高亮)然后 delete。 效果差异: - Vim:你想象 → 输命令 → 看结果(有时不对要 undo) - Helix:每一步看到 selection 高亮 → 确认后再操作 对于复杂选择(`d a (` vs `d i {`),Helix 的"先选"模式让人少出错。 ## 装 + 跑 ```bash # macOS brew install helix # Debian / Ubuntu sudo add-apt-repository ppa:maveonair/helix-editor sudo apt install helix # Arch sudo pacman -S helix hx --version ``` ```bash hx myfile.py # 或: hx . # 打开文件夹(文件浏览器) ``` ## 默认能力(零配置) 打开任何文件就有: - 语法高亮(Treesitter,所有主流语言) - LSP(自动找 pyright / rust-analyzer / gopls / tsserver 如果系统装了) - 自动补全 - 跳转定义 / 引用 - diagnostics 显示 - 模糊文件搜索(Space-f) - 全局 grep(Space-/) - 多文件 buffer - multiple cursor 多光标编辑 - 主题(30+ 内置) 不需要装插件,不需要写 config。 ## 我用了一周记下来的常用键位 normal 模式(默认): ``` hjkl 移动 w / b 下/上一词 W / B 下/上 WORD (含标点) gh / gl 行首 / 行尾 gg / ge 文件首 / 末 G 指定行号 % 配对括号 # 选择 v 进 selection extend 模式 x 选整行 ) 扩选下一句 m m i 选当前 ( ... ) 内(match mode) m m a 选当前 ( ... ) 含括号 # 多光标 C 下一行加光标 , collapse 多光标到主光标 A-C 上一行加光标 * 把当前 selection 作搜索 word # 操作(先选后改) d delete c change (delete + insert mode) y yank p paste ~ 切大小写 u undo U redo # LSP gd 去定义 gr 引用 gi 实现 K hover info SPC a code actions SPC r rename # 文件 / buffer SPC f 模糊找文件 SPC b buffer 列表 SPC / grep 全工程 SPC s symbol(当前文件) SPC S workspace symbol # 命令模式 : : write / : help / : config-reload / 等 ``` 按 Space 进入 picker 是 Helix 一大亮点:fuzzy 文件 / buffer / symbol / grep / diagnostic 都在 Space-x 系列下。 ## 配置:可选但很少需要 `~/.config/helix/config.toml`: ```toml theme = "catppuccin_mocha" [editor] line-number = "relative" mouse = false cursorline = true auto-format = true bufferline = "always" [editor.indent-guides] render = true character = "│" [editor.lsp] display-messages = true display-inlay-hints = true [editor.cursor-shape] insert = "bar" normal = "block" select = "underline" ``` 跟 Neovim 几百行 Lua 比,30 行 TOML 解决全部。 ### 自定义 keymap ```toml [keys.normal] "C-s" = ":w" "C-q" = ":q" "esc" = ["collapse_selection", "keep_primary_selection"] [keys.normal.space] "e" = "file_picker" # 改 SPC e 为文件搜索(个人偏好) ``` ## LSP / formatter 装系统 Helix 不管 LSP server 安装。系统装好后自动用: ```bash # Python pipx install pyright ruff # 或 npm i -g pyright # TypeScript / JS npm i -g typescript typescript-language-server # Go go install golang.org/x/tools/gopls@latest # Rust(rustup 自带) rustup component add rust-analyzer ``` `hx --health python` 看 python LSP / formatter / debugger 检测状态: ``` Configured language servers: ✓ pyright: /home/me/.local/bin/pyright-langserver ✓ ruff: /home/me/.local/bin/ruff Configured debug adapter: ✓ debugpy Configured formatter: ✓ ruff Treesitter parser: ✓ Highlight queries: ✓ ``` 哪一项不通 / 哪个 binary 缺都明确告诉你。 ## 跟 Neovim / VSCode 对比 | | Helix | Neovim | VSCode | |---|---|---|---| | 启动速度 | ~30ms | ~50-300ms(看配置) | 1-3s | | 配置量(默认能用) | 0 行 | 200-500 行 Lua | 0 行 + extension | | LSP 集成 | 内置 | nvim-lspconfig | 内置 | | 插件生态 | 极少(按 plugin 不支持) | 极丰富 | 极丰富 | | modal editing | ✅(kakoune 风) | ✅(vi 风) | extension | | 内置 fuzzy / grep | ✅ | 装 telescope 等 | ✅ | | debugger | DAP 集成(粗糙) | DAP(telescope-dap) | 极强 | | 鼠标 / GUI | terminal only | terminal / neovide | full GUI | **Helix 适合**:想要 modal editing 但不想花周末写 config 的人。 **Neovim 适合**:极致定制 / 已经投入了配置 / 需要某些插件。 **VSCode 适合**:debugger 重度 / 需要图形 / 团队混用。 ## 缺点 / 注意 - **插件系统**:Helix 不支持 plugin(design 选择)。Neovim 上的 copilot / lazygit 集成 / Treesitter playground 等都没有 Helix 等价物。 Plugin support 在 roadmap 上但还没。 - **debugger 弱**:内置 DAP 客户端粗糙,debug 体验不如 VSCode / nvim-dap。 - **Vim 用户初期不适**:动词名词顺序反 + 一些常用键改了。1-2 周适应。 ## 一周体验 - 启动从 Neovim ~150ms → Helix 30ms(lazyloading 自然瞬时) - 写代码体验差不多(LSP + Treesitter 一样齐) - 不需要每年大改 config 跟 plugin breaking change - 同事看我屏幕能立刻上手(key 高亮 + status bar 提示下一步) 正在评估全量切到 Helix。对深度 plugin 依赖(如 Org-mode)的同事保留 Neovim。 ## 踩过的坑 1. **`y` / `p` 不进系统剪贴板**:默认是 Helix 内部 register。 要系统:`"+y` / `"+p`(用 `+` register)。可以 keymap 覆盖让 `y` 默认走系统。 2. **Treesitter parser 缺**:第一次开某语言文件 `hx --health <lang>` 看哪些缺。`:treesitter-build` 重 build。 3. **LSP 自动启但不响应**:`:log-open` 看 LSP 日志。常见原因是 project root 找不到(缺 `pyproject.toml` / `package.json`)。 4. **Multi-cursor 撤销不一致**:`u` 一次 undo 所有 cursor 的最近改动。 习惯就好。 5. **没 file tree**:`SPC f` fuzzy 找文件代替(实际更快)。强烈想要 file tree → 暂时只能等 plugin support。

bat / eza / fd / dust / btop:把 6 个常用命令一次性现代化

## 起因 `cat` / `ls` / `find` / `du` / `top` / `df` 是 1970 年代的设计, 在终端不支持彩色 + 不假设大字符集时的产物。2020 后有一批 Rust / Go 写 的现代替代,UX 显著好。下面是我桌面 + 服务器都装的 6 个。 ## 1. bat 替代 cat ```bash sudo apt install bat # Debian/Ubuntu (有时命令叫 batcat) brew install bat # macOS ``` ```bash bat README.md # 显示行号 + 语法高亮 + git 改动 marker + 自动 less 分页 ``` 更进一步: ```bash bat src/*.py # 多文件 bat --diff README.md # 只显示有 git 改动的行 bat -A weird-file # 显示控制字符 / Unicode 不可见 bat --plain | less # 关掉装饰,纯 cat 行为 ``` 集成给其它工具用: ```bash export PAGER='bat --plain' # man 命令用 bat 当 pager alias cat='bat --plain --paging=never' # 替代 cat 但不分页 # 或保留 cat 原行为,bat 命令独立 ``` ## 2. eza(前 exa)替代 ls ```bash brew install eza cargo install eza # Debian: 二进制 release ``` ```bash eza -lah # 长格式 + 隐藏 + 友好大小 eza --tree -L 2 # 树状显示 eza -la --git # 加 git 状态列(M / A / ! / ?) eza -l --sort=size --reverse # 按大小排 eza --icons # 文件类型 icon(需 Nerd Font) ``` 我的 alias: ```bash alias ls='eza --icons --group-directories-first' alias ll='eza -lah --icons --group-directories-first --git' alias lt='eza --tree -L 3 --icons' ``` `--group-directories-first` 让目录排在前面,比标准 ls 更可读。 ## 3. fd 替代 find ```bash sudo apt install fd-find # 命令叫 fdfind brew install fd cargo install fd-find alias fd=fdfind # Debian 上 ``` ```bash # 找文件(自动 fuzzy + 默认尊重 .gitignore + 默认彩色) fd config # 在特定目录 fd config ~/projects # 按扩展名 fd -e py -e pyx # 找目录 fd -t d build # 找后执行 fd -e log -x rm # 删所有 .log fd -e py -x ruff check ``` 比 `find` 简洁太多: ```bash find . -name '*.py' -not -path './node_modules/*' # vs fd -e py # 自动跳过 .gitignore 里的 node_modules ``` fd 比 find 也快得多(并行)。 ## 4. dust 替代 du ```bash cargo install du-dust brew install dust ``` ```bash dust # 当前目录大小,树状显示 dust -d 3 # 限制深度 dust ~/projects # 指定目录 ``` 输出例: ``` 0B ┌── empty.txt 12M ├── data.json 512M ├─┬ logs │ └── 2024 1.2G ├─┬ node_modules │ ├── react ... 2.8G ┌── . (current) ``` 带 ASCII 条形图 + 自动按大小排,秒级看出"哪个目录吃磁盘"。 比 `du -h | sort -h` 直观 100 倍。 ## 5. duf 替代 df ```bash sudo apt install duf brew install duf ``` ```bash duf ``` 输出: ``` ╭─────────────────────────────────────────────────────────────────╮ │ 4 local devices │ ├─────────────────────────────────────────────────────────────────┤ │ MOUNTED ON SIZE USED AVAIL USE% TYPE FILESYSTEM │ │ / 456.5G 234.2G 198.3G 51% ext4 /dev/nvme0n1p2 │ │ /boot 1.0G 245M 755M 24% ext4 /dev/nvme0n1p1 │ │ /home 912.0G 423.1G 442.1G 46% ext4 /dev/sda2 │ ╰─────────────────────────────────────────────────────────────────╯ ``` 颜色 + 表格 + 进度条,一眼看出哪个分区危险。 ## 6. btop 替代 top ```bash sudo apt install btop brew install btop ``` ```bash btop ``` 全屏 TUI,比 top / htop 又好看又信息量大: - 上方 CPU 每核 + 占用进程 - 中部内存 + swap - 下方进程列表 + 网络 + 磁盘 IO - 鼠标 + 键盘都能交互 - 跨平台(Linux / macOS / FreeBSD) 按 `m` 切换显示模式,`+/-` 缩放面板,`q` 退出。 ## 一次性都装上 我的服务器初始化脚本: ```bash sudo apt update sudo apt install -y bat eza fd-find duf btop ripgrep zoxide fzf # Debian 上 fd 叫 fdfind,bat 叫 batcat echo 'alias fd=fdfind' >> ~/.bashrc echo 'alias bat=batcat' >> ~/.bashrc # 或一个 cargo 大餐: cargo install bat eza fd-find du-dust ripgrep starship zoxide ``` ## 工具对照表 | 老 | 新 | 主要优点 | |---|---|---| | `cat` | `bat` | 语法高亮、行号、git 改动 | | `ls` | `eza` | 颜色、git、icon、tree | | `find` | `fd` | 简洁、快、尊重 .gitignore | | `grep` | `rg` | 快 10x、跳过 .git、smart-case | | `du` | `dust` | 直观、条形图 | | `df` | `duf` | 表格、颜色 | | `top` | `btop` | 信息更全、好看 | | `cd` | `z` | 模糊跳 | | `man` | `tldr` | 例子优先而非全文档 | | `make` | `just` | 简洁、跨平台 | ## tldr 顺便提一下 ```bash brew install tldr tldr tar # tar # Archiving utility. # Create an archive from files: # tar -cf target.tar file1 file2 file3 # Create a gzipped archive: # tar -czf target.tar.gz file1 file2 file3 # ... ``` 不需要看 200 行 man,直接给你常用的几条示例。 ## 效果 - 日常 ls/cat 命令视觉信息量翻倍,找东西更快 - "哪个目录占空间" 类问题秒回(dust) - 服务器 troubleshoot 三件套:`btop` + `dust` + `duf` 一屏诊断 - 新机器 5 分钟初始化完所有工具 - 同事被安利后没人愿意回去用原生 ls ## 踩过的坑 1. **Debian 命令名前缀 fd-find / batcat**:跟其它工具冲突历史原因。 alias 一行解决,但脚本里调用要小心,别在脚本里依赖 alias (脚本默认 non-interactive 不读 alias)。 2. **服务器没有 Nerd Font 装不了 icon**:`eza --icons` 显示豆腐块。 服务器上去掉 `--icons` 即可(仍有颜色)。 3. **bat 默认 pager 是 less**:`less -R` 才能正确显示 ANSI。 `export BAT_PAGER='less -R'` 显式设。 4. **fd 不显示隐藏文件**:默认尊重 `.gitignore` + 隐藏文件。 `fd -H` 显示隐藏;`fd -I` 忽略 ignore 规则;`fd -HI` 全开。 5. **btop CPU 占用高**:默认刷新 2Hz,加 `--update 5` 改 5Hz 不影响判断 但更稳。

direnv:进项目目录自动加载环境变量 / venv(出去自动卸载)

每个项目都有自己的环境变量(API key / DB URL / venv 路径 / Node 版本)。 手动 `source .env` 麻烦且容易跨项目污染。direnv 让 shell 在 cd 进项目 目录时自动加载 `.envrc`,离开时自动卸载。 ## 安装 ```bash sudo apt install -y direnv # 或 brew install direnv direnv version ``` ## 集成 shell ```bash # bash echo 'eval "$(direnv hook bash)"' >> ~/.bashrc # zsh echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc # fish echo 'direnv hook fish | source' >> ~/.config/fish/config.fish ``` 重新加载 shell。 ## 第一次用 ```bash cd ~/projects/myapp echo 'export DATABASE_URL=postgresql://localhost/myapp' > .envrc echo 'export STRIPE_KEY=sk_test_xxxxx' >> .envrc echo 'export FLASK_DEBUG=1' >> .envrc # direnv 提示: # direnv: error /home/me/projects/myapp/.envrc is blocked. # Run `direnv allow` to approve its content direnv allow # 之后每次 cd 进来: # direnv: loading ~/projects/myapp/.envrc # direnv: export +DATABASE_URL +STRIPE_KEY +FLASK_DEBUG env | grep DATABASE_URL # DATABASE_URL=postgresql://localhost/myapp ``` cd 离开目录: ```bash cd .. # direnv: unloading env | grep DATABASE_URL # 没了 ``` ## allow 机制 direnv 不会自动加载未授权的 `.envrc`(防止 git clone 别人仓库就执行 恶意代码)。每次 `.envrc` 内容变了都要重新 `direnv allow`。 ```bash direnv allow . # 授权 direnv deny . # 撤销 direnv reload # 强制重新加载 ``` ## stdlib:常用 helper direnv 自带一组 helper 函数: ```bash # .envrc use python 3.12 # 自动用 pyenv 切到 3.12 layout python python3.12 # 创建 .direnv/python-3.12 venv 并激活 dotenv # 自动读 .env 文件里的 KEY=VAL PATH_add bin # 把项目 bin/ 加进 PATH(自动相对路径) source_up # 也加载上一层目录的 .envrc ``` 完整:`direnv stdlib | less`。 ## 真实例子:Python 项目 ```bash # .envrc layout python python3.12 dotenv # 加 project 本地 bin 到 PATH PATH_add bin PATH_add scripts # Django settings export DJANGO_SETTINGS_MODULE=myapp.settings.dev export DJANGO_SECRET_KEY=$(cat .secret-key 2>/dev/null || echo 'dev-key') ``` cd 进去时: 1. 自动创建 / 激活 `.direnv/python-3.12/` venv 2. 读 `.env` 注入 KEY=VAL 3. 把 bin/ 加进 PATH `pip install ...` 装的依赖就在这个 venv 里,不污染全局。 ## Node 项目 ```bash # .envrc use node 20 # 配合 nvm / fnm / asdf 切版本 PATH_add node_modules/.bin # 让 npx 命令直接 in PATH ``` ## Rust 项目 ```bash # .envrc PATH_add target/debug PATH_add target/release ``` ## 多版本工具切换(asdf / mise) ```bash # .envrc use mise # 让 direnv 触发 mise 的环境 ``` mise 会按 `.tool-versions` 自动切 Node/Python/Go/etc 版本, direnv 让这套在 cd 时自动应用。 ## .env vs .envrc | 文件 | 作用 | |---|---| | `.envrc` | direnv 配置(bash 脚本,可写逻辑) | | `.env` | 简单 `KEY=VAL` 列表,被 `dotenv` 读 | | `.envrc.local` | 个人覆盖(建议加 `.gitignore`) | ```bash # .envrc dotenv .env dotenv_if_exists .env.local # 个人覆盖 source_env_if_exists .envrc.local ``` ## 与 IDE 集成 VSCode 不会自动用 direnv。装插件 `mkhl.direnv` 让 VSCode 在打开项目 时执行 `.envrc` 并把变量塞给 terminal / debugger。 JetBrains 系也有 direnv 插件。 ## 安全注意 `.envrc` 是 bash 脚本,能执行任意命令。git clone 后 direnv 不会自动 load, 必须 `direnv allow`——这是 feature。 但养成习惯:clone 完之后 **先 cat .envrc 看一眼** 再 allow。 某些恶意 `.envrc` 在你 allow 时执行 `rm -rf /` 你没法找回来。 ## 调试 ```bash direnv status # 当前目录的 direnv 状态 direnv exec . env # 看 direnv 实际注入了哪些变量 DIRENV_LOG_FORMAT=... # 让加载日志更详细 ``` ## 踩过的坑 - 在 docker / SSH session 里没生效:`direnv hook` 没运行,rc 文件不被 source。`bash -ic '...'` 可以强制 interactive。 - `.envrc` 里 `cd ..` —— 别这么写,direnv 钩到 cd,会无限循环。 - 在 git 钩子 / cron 里 cd 进项目目录,direnv 不会触发(cron 没 shell hook), 环境变量不会注入。要么显式 `source .envrc`,要么 `direnv exec . command`。 - venv 在 macOS 上启动慢:每次 cd 进出都重新激活。如果不需要 venv 隔离, 用 `dotenv` 而非 `layout python` 跳过创建 venv。

git worktree:在同一个仓库同时开多个分支 work

## 起因 经常遇到这场景: - 当前 feature 分支开发到一半 - 紧急 hotfix 需要在 main 改一行 - 又要 review 同事的 PR 在 third-branch 老办法:`git stash` + `git checkout main` + 改 + commit + `checkout feature` + `stash pop`。每次切换几分钟(编辑器要重 index、deps 不一样还要重装、 本地服务要重启)。 `git worktree` 解决:**一个仓库,多个工作目录,分别 checkout 不同分支**。 ## 基本用法 ```bash cd ~/projects/myapp # 主 worktree(main 分支) # 创建新 worktree 在 ../myapp-hotfix,checkout hotfix-x 分支 git worktree add ../myapp-hotfix -b hotfix-x # 创建在 ../myapp-review,checkout 现有 PR 分支 git worktree add ../myapp-review feature/pr-42 # 列出所有 git worktree list # /home/u/projects/myapp abcd1234 [main] # /home/u/projects/myapp-hotfix ef567890 [hotfix-x] # /home/u/projects/myapp-review 12345abc [feature/pr-42] # 完成后删 git worktree remove ../myapp-hotfix ``` 每个 worktree 是**独立目录**:自己的 working tree、index、stash、 hooks 状态。但共享 `.git/`(共享对象库、refs、config)。 磁盘只增加 working tree 文件大小,不重复存历史。 ## 我的目录约定 ``` ~/projects/myapp/ # 主 worktree(main) ~/projects/myapp-wt/ # 所有附加 worktree 都进这里 ├── feature-search/ ├── hotfix-401/ └── review-bob-pr/ ``` `-wt/` 在 `.gitignore` 里加(实际上 git 不会管它),也加到编辑器的 排除列表,避免 indexing 重复。 ## 效果 切换分支从"清理 + checkout + 重装 deps + 重启服务" → "cd 到另一个目录"。 - 编辑器在每个目录独立打开 - 每个 worktree 跑自己的 dev server(端口不同) - venv / node_modules 独立(cargo / go 缓存可以共享) 实测:从 hotfix 切回主 feature 从平均 5 分钟 → 5 秒。一周省 1 小时。 ## 自动化 每次 review 同事 PR 都建 worktree,写个 alias: ```bash # ~/.zshrc function wt-pr() { local pr=$1 gh pr checkout $pr # 拉到本地分支 local branch=$(git branch --show-current) cd .. git -C myapp worktree add "myapp-pr-$pr" "$branch" cd "myapp-pr-$pr" } ``` `wt-pr 42` → 自动 fetch + 建 worktree + cd 过去。 ## venv / node_modules 怎么办 每个 worktree 独立装: ```bash cd ../myapp-hotfix uv sync # 装到本地 .venv/ pnpm install # 装到本地 node_modules/ ``` uv / pnpm 用 hard link / 全局 cache → 实际硬盘开销小。 n × 同 deps 不会真的 n 倍磁盘。 ## 共享 hooks `.git/hooks/` 是共享的(git 内部目录共用)。 每个 worktree 都跑同样的 pre-commit。一般是好事。 个别 worktree 想跳过 hook → `--no-verify`。 ## 配 IDE VS Code:每个 worktree 当成独立 workspace 打开。 WebStorm / IntelliJ:同样。 Cursor:可以在多个窗口打开不同 worktree 同时操作。 `.vscode/settings.json` 通过 git ignore 的方式各自配置。 ## 与 stash / checkout 对比 | 场景 | stash + checkout | worktree | |---|---|---| | 切换速度 | 慢(要重 index / restart) | 快(cd 即可) | | 心智模型 | 一个目录多状态 | 多目录多状态(更直觉) | | 编辑器 indexing | 重做 | 不需要 | | 磁盘 | 少 | 多 working tree(但对象库共享) | | 临时草稿 | stash 容易丢 | 文件实存 | 90% 场景 worktree 胜。 ## 与 git clone 对比 git clone 多次克隆同 repo: - 优点:完全独立,无干扰 - 缺点:每个独立 `.git/`(重复存对象库,几百 MB → 几 GB)+ 不同 clone 之间分支不互通 worktree 共享 `.git/`,省空间 + 一处 fetch 全部能看到新 remote refs。 ## main worktree 不能删 ```bash git worktree remove . # error: cannot remove main working tree ``` 主 worktree 是创建仓库时所在的目录。要换主 worktree 要 `git worktree move`(少用)。 ## 踩过的坑 1. **同分支不能两个 worktree checkout**:保险机制防止冲突。 要看同一分支就建一个 detached HEAD worktree: `git worktree add ../tmp HEAD`。 2. **worktree 删除后 git 不会清理元数据**:`git worktree prune`。 或者用 `remove` 命令而不是手动 `rm -rf`。 3. **submodule + worktree**:早期 git submodule 在多 worktree 不太稳定。git 2.40+ 改善很多但仍偶有奇怪行为。 4. **CI 不支持 worktree**:CI clone 出来是单 worktree。worktree 是本地开发优化,不影响 CI。 5. **跨平台 path 差异**:worktree path 存在 `.git/worktrees/*/gitdir` 绝对路径。把仓库目录改名 / 移动 → worktree 失效。 用 `git worktree repair` 修。

tldr / cheat / navi:再也不读 man 全文

## 起因 `man tar` 输出 300 行,我只想知道"怎么解压一个 .tar.gz"。 man 是参考手册,新人 / 偶尔用户需要的是"3 个常用例子"。 `tldr` 项目("Too Long; Didn't Read")是社区维护的 CLI 命令速查, 每个命令给 5-10 个常见用法。 ## tldr ```bash # 装 brew install tldr # macOS sudo apt install tldr # Debian/Ubuntu npm i -g tldr # 跨平台 # 或者 Rust 实现(快得多) cargo install tealdeur alias tldr=tldr # rust 二进制叫 tldr ``` ```bash tldr tar ``` 输出: ``` tar Archiving utility. - Create an archive from files: tar cf {{target.tar}} {{file1 file2 file3}} - Create a gzipped archive: tar czf {{target.tar.gz}} {{file1 file2 file3}} - Extract a gzipped archive in the current directory: tar xzf {{source.tar.gz}} - Extract an archive into a target directory: tar xf {{source.tar}} -C {{directory}} - List the contents of an archive: tar tvf {{source.tar}} ``` 5 个例子覆盖 90% 用法。`man tar` 几百行的实际有用部分浓缩成 30 秒 能读完。 ### 更新 cache ```bash tldr --update ``` 定期跑更新到最新版社区贡献。 ### 离线工作 第一次跑 `tldr --update` 后所有 cheat sheet 缓存到本地(~30 MB), 之后无网络也能查。 ### 不同 OS 的命令变体 tldr 自动检测你的平台显示对应版本: ```bash tldr -p linux ls tldr -p osx ls tldr -p windows dir ``` Linux 上 `find` vs macOS BSD 版 `find` 语法不同,tldr 给你正确的。 ## cheat 类似 tldr 但更"自己加"友好: ```bash # 装(Go 二进制) go install github.com/cheat/cheat/cmd/cheat@latest cheat tar # 显示 tar 的 cheatsheet cheat -e tar # 用编辑器修改本地 tar cheatsheet(写自己常用的) cheat -l # 列所有 cheatsheets ``` `~/.config/cheat/cheatsheets/community/` 是社区版本; `~/.config/cheat/cheatsheets/personal/` 是你自己加的(优先显示)。 适合"团队内部的命令速查"——给项目特定的命令写 cheat。 ## navi:interactive cheatsheet + 直接执行 ```bash brew install navi cargo install navi ``` ```bash navi # TUI 弹出,浏览所有 cheats,选中后回车直接执行 ``` cheat 文件格式: ``` % docker # stop all containers docker stop $(docker ps -aq) # remove all stopped containers docker rm $(docker ps -aq) # list images sorted by size docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | sort -k 2 -h ``` navi 会让你 fuzzy 搜("docker stop all"),找到后直接执行。 对"我知道有这条命令但记不清完整"特别适合。 参数化命令: ``` % find # find files by name (case insensitive) find <dir> -iname "*<pattern>*" ``` navi 选中后弹小框让你填 `<dir>` 和 `<pattern>`,然后执行。 ### 同步团队 cheat ```bash navi --finder fzf --query 'docker' navi --tldr 'tar' # 直接调用 tldr navi repo browse # 浏览社区 cheat 仓库 navi repo add <git-url> # 加自定义 repo ``` 公司内部 cheat 仓库 + `navi repo add [email protected]/cheats.git` → 全员共享团队最佳实践。 ## bro pages(命令请求帮助 + 评分) ```bash # 装(不再积极维护,但仍可用) gem install bropages bro tar # 显示社区贡献的 examples,按点赞排序 bro thanks # 给当前显示的 example 点赞 bro add tar # 自己贡献一个 example ``` 社区驱动 vs tldr 的"官方维护"差异: bro 更草根,tldr 更整洁。 ## 自己装"代码 snippet" 我个人的 `~/notes/cli.md`: ```markdown ## ssh 隧道 # Local port -> remote service through bastion ssh -L 8080:internal-host:80 -N bastion # Reverse tunnel: expose local port to remote ssh -R 9000:localhost:9000 -N remote ## ffmpeg # extract audio ffmpeg -i in.mp4 -vn -acodec copy out.aac # convert to mp3 ffmpeg -i in.wav -codec:a libmp3lame -b:a 192k out.mp3 # trim by time ffmpeg -i in.mp4 -ss 00:01:30 -t 00:00:30 -c copy out.mp4 # add subtitle ffmpeg -i in.mp4 -vf subtitles=in.srt out.mp4 ``` 配合 fzf 让自己的笔记可搜: ```bash mynote() { grep -E '^#' ~/notes/cli.md | fzf | xargs -I {} grep -A 5 '^{}' ~/notes/cli.md } ``` 打 `mynote` → fzf 出来所有标题 → 选一个看内容。 ## 与 ChatGPT / Claude 的关系 LLM 也能"我想做 X 命令怎么写"。优劣: - **LLM**:自由表达、不限范围;可能编造(小心 hallucination) - **tldr / cheat**:人工验证、可靠;范围限于已收录 我现在混用: - 先 `tldr cmd` → 如果想要的功能在 5 个例子里直接用 - 否则问 LLM → 验证后加进自己的 `~/notes/cli.md` ## 效果 - 不再 `man tar` / `man find` 滚屏几分钟找 example - 团队"上次那个 ffmpeg 命令是啥来着" 类问题 → 自己 cheat 速查 - 新人 onboarding 速查内部命令大幅简化(navi + 团队 cheat repo) ## 踩过的坑 1. **tldr 例子里 `<placeholder>` 别照抄**:`{{file}}` 是占位符, 替换成真值再跑。 2. **cheat 跨用户同步**:`~/.config/cheat/cheatsheets/personal/` 进自己 dotfiles repo + symlink,多机器同步。 3. **navi 选 cheat 后误执行**:危险命令(rm -rf)navi 默认会先填参数 但仍会执行。一行 cheat 写法注意防滑指。 4. **bropages 维护停了**:不推荐新装,迁 tldr / navi。 5. **`tldr` 命令冲突**:有 Node / Python / Rust / shell 多个实现。 PATH 里只留一个,按需 alias。

zellij vs tmux:终端多路复用器选哪个

## 起因 ssh 远程开发 + 本地多窗口工作流,需要: - 多终端面板(split / 切换) - detach 重连(断网不丢 session) - 持久化布局 `tmux` 是事实标准 20 年。`zellij`(Rust,2021+)是现代挑战者。 最近重度用 zellij 一个月。下面对比。 ## tmux 经典 + 普遍可用。 ```bash brew install tmux apt install tmux ``` `~/.tmux.conf` 配色 / 键位 / 插件。 ### 操作 ``` prefix = Ctrl-b(默认) prefix + c 新 window prefix + n 下个 window prefix + % 横分 pane prefix + " 竖分 pane prefix + d detach session prefix + [ 进 copy mode(scroll) ``` `tmux ls` 列 session,`tmux attach -t <name>` 接回。 ### 优势 - **最广泛可用**:所有 Linux distro / macOS / *BSD 包管理都有 - 极成熟(2007 至今) - 插件生态丰富(tpm + 几千插件) - 资源占用小(< 5 MB) - session 持久化 + tmux-resurrect 保存恢复 ### 劣势 - 默认配色 / 键位丑陋复古,要花时间配 - 学习曲线陡(前 1 小时挫败感强) - 配置文件语法奇特 - 无内置浮窗(3.2+ popup 比较基础) ## zellij ```bash brew install zellij cargo install zellij ``` 启动直接 `zellij`。 ### 操作 底部有状态栏显示当前 mode 和键位 → **不用记**! ``` Ctrl + p pane mode(split / resize) Ctrl + t tab mode(多 tab) Ctrl + s scroll mode Ctrl + o session mode(detach 等) Ctrl + n resize mode Ctrl + h move mode ``` 进 mode 后状态栏显示可用键,按数字 / 字母执行。 ### 优势 - **新手友好**:状态栏永远显示能做什么 - 默认配色现代 + 美观 - 内置 layout(KDL config) - 内置 floating pane / sticky pane - WASM 插件(Rust / Go 写插件) - 性能极好(Rust) ### 劣势 - 还年轻(兼容性 / 生态比 tmux 弱) - 老服务器没有(apt 包要等 24.04) - 远程 ssh 渲染偶有小问题 - 内存占用更大(30-80 MB vs tmux 5 MB) ## 配色 / layout 对比 **tmux 默认**: ``` [0] 0:bash- 1:vim* "myhost" 14:23 25-Apr-24 ``` 朴素绿色 bar,要配 powerline / status-utf8 才好看。 **zellij 默认**: ``` zellij_session TAB: 1 Bash 2 Vim ───────────────────────────────────── (panes) ───────────────────────────────────── Ctrl+p PANE Ctrl+t TAB Ctrl+s SCROLL Ctrl+o SESSION ``` 底部 hint bar 直接告诉你下一步。 ## 配置文件 **tmux** `.tmux.conf`: ```tmux set -g prefix C-a unbind C-b bind C-a send-prefix set -g mouse on set -g history-limit 50000 # split with intuitive keys bind | split-window -h bind - split-window -v # vim-style pane nav bind h select-pane -L bind j select-pane -D bind k select-pane -U bind l select-pane -R # plugin manager set -g @plugin 'tmux-plugins/tpm' set -g @plugin 'tmux-plugins/tmux-resurrect' run '~/.tmux/plugins/tpm/tpm' ``` **zellij** `~/.config/zellij/config.kdl`: ```kdl keybinds { normal { bind "Ctrl g" { SwitchToMode "Locked"; } } } theme "nord" simplified_ui false default_layout "compact" mouse_mode true ``` KDL 比 tmux 自定义语法人类友好得多。 ## layout file(强项) zellij: ```kdl // dev.kdl layout { tab name="editor" { pane command="nvim" } tab name="server" { pane split_direction="vertical" { pane command="pnpm" { args "dev"; } pane command="docker" { args "compose" "logs" "-f"; } } } tab name="test" { pane command="pnpm" { args "test" "--watch"; } } } ``` 启动: ```bash zellij --layout dev.kdl ``` 一键开 3 tab / 多 pane 各跑特定命令。 tmux 等价是 shell script + tmux send-keys → 又长又脆。 ## 远程 ssh 注意 tmux:远程跑 tmux + 本地 ssh 进去 attach。断网 → 进程不丢。 zellij:同样可以,但 zellij 用更多 ANSI escape → 慢网络上重绘 更明显。 我远程**仍用 tmux**。本地 zellij。 ## 我现在的工作流 - 远程服务器:tmux(兼容性 / 速度 + 几乎所有服务器都装好) - 本地 macOS:zellij(视觉好 + layout file 项目自动布局) - 跨 ssh 项目:zellij 本地嵌套(zellij outer + tmux inner on remote) ## 内存 / 启动 ```bash # tmux: ~3 MB resident # zellij: ~30 MB resident # zellij 启动 ~ 100ms vs tmux ~ 20ms ``` 500 倍 memory 差距听起来吓人,绝对数都很小。除非 1 GB 小机器 (树莓派 / 老 VPS)不是问题。 ## 共享 session(远程 pairing) tmux:`tmux a -t name`(多终端 attach 同 session,看到同样画面)。 zellij:`zellij attach <session>` 同样支持。 zellij 还有 `zellij --new-session-with-layout`,新人加入直接套布局。 ## 替代品快览 - **screen**:祖宗,纯 80 年代风格,今天没人新装 - **wezterm**:终端 emulator + 内置 multiplexer(可替代 zellij/tmux) - **kitty**:终端 emulator,有窗口管理但不是 multiplexer 如果你想"一个工具搞定终端 + multiplex" → wezterm。 传统派 → tmux/zellij 跟终端解耦。 ## 决策 - **新人 / 个人** → zellij(学习曲线低) - **远程 ssh / 老 server** → tmux(兼容) - **重 layout 配置** → zellij(KDL layout file) - **重 plugin** → tmux(生态成熟) - **极简资源** → tmux ## 踩过的坑 1. **zellij + 24-bit color 终端报错**:老终端不支持 truecolor。 `TERM=xterm-256color` 或者用 alacritty / kitty / wezterm。 2. **zellij 复制到系统剪贴板**:默认 OSC 52 escape。某些终端不支持 → 要配 `copy_command "pbcopy"` (mac) / `xclip`。 3. **tmux 2.x vs 3.x**:服务器装的 tmux 2.x 没 popup / 某些新 feature。 ssh 远程时小心。 4. **嵌套 multiplex 键冲突**:本地 zellij + 远程 tmux → prefix 键冲突。 把 inner 的 prefix 改成不同(`C-a` vs `C-b`)。 5. **`.tmux.conf` 编辑要 source**:改完不会自动重载。 `tmux source ~/.tmux.conf`,或者 `prefix + r` bind 一下。

atuin:让 shell 历史跨机同步 + 模糊搜索 + 上下文感知

## 起因 我有 3 台机器(笔记本 + 两台服务器),每台 bash 历史互相不通。 "那条神秘的 ffmpeg 命令我半年前在哪台机器上跑过…" 永远找不回来。 而且原生 `Ctrl-R` 只能子串匹配,搜索体验差。 atuin 把 shell 历史存 SQLite + 加密同步 + 替换 `Ctrl-R` 为模糊搜索 TUI。 ## 安装 ```bash # 一行装 bash <(curl --proto '=https' --tlsv1.2 -sSf https://setup.atuin.sh) # 或包管理器 brew install atuin cargo install atuin # 注册到 shell atuin init bash >> ~/.bashrc atuin init zsh >> ~/.zshrc atuin init fish | source >> ~/.config/fish/config.fish # 重启 shell ``` ## 第一次跑 `Ctrl-R` 不再是 bash 原生 reverse-i-search,而是 atuin 的全屏 TUI: ``` > ffmpeg 2026-05-20 14:32 ffmpeg -i input.mp4 -vcodec libx264 -crf 23 out.mp4 2025-12-01 09:15 ffmpeg -i src.mov -ss 00:00:05 -t 30 clip.mp4 2025-11-10 21:00 ffmpeg -i in.wav -ab 128k out.mp3 ↑↓ 选 | Enter 执行 | Tab 编辑后执行 | Ctrl-D 退出 ``` 模糊匹配(不需要前缀完整)、按时间最近排序、显示具体执行时间。 ## 同步到云 atuin 提供免费托管: ```bash atuin register -u myuser -e [email protected] -p 'long-password' # 服务端发邮件确认 atuin login -u myuser -k <密钥> # 加密 key,重要! atuin sync ``` 之后每台机器: ```bash atuin login -u myuser -k <密钥> atuin sync ``` 历史立刻同步过去(端到端加密 —— 服务端只能存 ciphertext)。 **密钥(key)丢了等于所有历史丢了**——保存到 password manager。 ## 自托管 server 不想用官方 host: ```bash # server 端 docker run -d \ -e ATUIN_DB_URI=postgres://user:pass@host/db \ -p 8888:8888 \ ghcr.io/atuinsh/atuin server start # 客户端 ~/.config/atuin/config.toml sync_address = "https://atuin.your-server.com" ``` ## 配置 `~/.config/atuin/config.toml`: ```toml # 搜索模式:prefix / fulltext / fuzzy(推荐 fuzzy) search_mode = "fuzzy" # 启动时显示历史的 filter filter_mode = "global" # global | session | host | directory # 模式:自动按上下文切(按 Tab 切) filter_mode_shell_up_key_binding = "directory" # 默认搜索结果数 show_help = true inline_height = 20 # 不记录某些命令(密码、敏感的) history_filter = [ "^secret-cmd", "^export.*PASSWORD", ] # 不记录这些目录 cwd_filter = [ "/tmp", ] ``` ## context-aware 搜索 最强的 feature:按 Tab 切换 filter scope: - `global`:所有机器所有目录 - `host`:仅本机 - `session`:仅本 shell session - `directory`:仅本目录 ``` > docker [filter: directory] ``` 进 `~/projects/myapp` 目录搜 `docker` 只显示在这个目录跑过的 docker 命令。这比 bash `Ctrl-R` 强 10 倍——找上次"在哪个项目里跑的什么命令" 秒级。 ## 时间过滤 ```bash atuin search --before '2 days ago' docker atuin search --after '1 week ago' kubectl ``` ## 统计 ```bash atuin stats # Total commands: 12847 # Unique commands: 4321 # Top commands: # 1. ls (1234) # 2. cd (987) # 3. git status (654) # ... atuin stats --period 7d # 最近 7 天 ``` 发现自己 80% 时间在 cd 和 ls,提示我多用 z + 文件管理器。 ## 导入老历史 ```bash atuin import auto # 自动检测 bash/zsh/fish/atuin ``` 之前几年的 `~/.bash_history` / `~/.zsh_history` 全导入 atuin DB。 ## 不想被记录的命令 ```bash # 单条命令前面加空格(bash HISTCONTROL=ignorespace 兼容) secret-command --token xyz ``` 或者用 atuin 的 history_filter regex。 ## CLI 用法 ```bash atuin history list --limit 50 atuin history list --cwd ~/projects/myapp atuin search 'docker run' # 把某条历史拿出来重新跑 atuin search 'ffmpeg' --format '{{ .command }}' --limit 1 | bash ``` ## 跟 fzf 配合 `Ctrl-R` 默认是 atuin TUI。如果你更喜欢 fzf: ```bash # atuin 输出 + fzf 渲染 fh() { atuin history list --format '{{ .timestamp }} | {{ .command }}' \ | fzf --tac --no-sort --tiebreak=index \ --bind 'enter:become(echo {3..})' \ | xargs -I {} bash -c '{}' } ``` 但 atuin 自带的 TUI 其实 fzf-like,绝大多数场景不需要替换。 ## 性能 10 万条历史的 atuin DB ~ 30 MB;查询 < 50ms。本地纯 Rust + SQLite, 无网络请求。 ## 效果 - 3 台机器历史共享,"半年前那条命令" 永远找得回来 - 搜索体验从 grep-字符串 升到 fuzzy + context-aware - 在项目目录 `Ctrl-R` 自动过滤到本项目历史,省去大量"误中其它项目命令" - 跨机迁移 / 重装系统 / 新员工 onboarding 都瞬间继承"老司机经验" ## 踩过的坑 1. **加密密钥丢失** = 所有同步历史丢失。注册后立刻 `atuin key` 看 key,存进 password manager 三份(云 + 本地 + 打印)。 2. **某些 ZSH 主题与 atuin 冲突**:oh-my-zsh 的 `Ctrl-R` 被绑给其它 插件。`bindkey '^R' atuin-search` 强制覆盖。 3. **同步把敏感命令也带走**:包含 `export PASSWORD=xxx` / `mysql -p xxx` 的命令也加密同步。提前用 `history_filter` 过滤。 4. **服务端不可达时 `atuin sync` 卡**:网络问题导致同步 hang。 `atuin sync --force` 或者 `kill` 后下次再 sync。 5. **新 shell session 启动慢**:atuin init 加了几个 hook。如果发现 shell 启动 > 100ms,看看是不是 atuin 同步在做(关 `auto_sync`,手动 `atuin sync`)。

Git 子项目共享代码:submodule / subtree / monorepo 怎么选

## 起因 我们有 5 个微服务都用一份"共享 utility 代码"(如 protobuf 定义 / 通用 model / shared logger)。三种主流方式: A. 各项目 copy-paste(同步靠人工,最差) B. 抽成 npm/pip 包发包(增加发版流程) C. Git submodule 共享一个 repo D. Git subtree 把共享 repo 嵌入主 repo E. monorepo 把所有项目放一起 各自适用场景不同。 ## A. Git submodule ```bash cd myservice git submodule add https://github.com/myorg/shared-libs.git lib/shared git commit -m 'add shared submodule' ``` `.gitmodules` 文件 + lib/shared 目录(指向 shared-libs 的某个 commit)。 ```bash # clone 主 repo 时要 init submodule git clone --recurse-submodules https://github.com/myorg/myservice.git # 或事后 cd myservice git submodule update --init --recursive ``` 更新 submodule: ```bash cd lib/shared git pull origin main cd ../.. git add lib/shared git commit -m 'bump shared submodule to abc123' ``` 主 repo 记录 "我指向 shared 的哪个 commit"。 **优点**: - 共享 repo 独立版本控制 - 每个项目可固定某个版本(不同步升级) **缺点**: - 命令繁琐(`git submodule update` 经常忘) - 新成员第一次 clone 总踩坑(忘 `--recurse-submodules`) - 改 submodule 内代码后要 push 共享 repo + 更新主 repo 引用 = 两个 PR ## B. Git subtree ```bash cd myservice git subtree add --prefix=lib/shared https://github.com/myorg/shared-libs.git main --squash ``` 效果:把 shared-libs 整个内容 copy 到 lib/shared/ 目录,作为本 repo 的真实 commit(不是引用)。 ```bash # 拉 shared 上游更新 git subtree pull --prefix=lib/shared shared-remote main --squash # 把本地 shared 改动推回上游 git subtree push --prefix=lib/shared shared-remote feature-x ``` **优点**: - 普通 clone 拿到所有文件(无 submodule 双步骤) - 本地直接改 shared 代码 + commit(无需先 push 上游) - 简单:只是普通 git 文件 **缺点**: - `git log` 看到 shared 的所有历史(噪音) - subtree push 偶尔合并冲突复杂 - 大多数开发者不熟,命令易记错 ## C. monorepo 把所有服务和 shared 放一个 repo: ``` myorg-monorepo/ ├── services/ │ ├── api/ │ ├── worker/ │ └── frontend/ ├── packages/ │ ├── shared-models/ │ └── shared-utils/ └── tools/ ``` shared 代码就是 packages/ 下的子目录,import 直接相对路径或 workspace (pnpm / yarn workspace / npm workspace / Cargo workspace / Go workspace / etc)。 **优点**: - 改 shared + 改用它的 service 一个 commit + 一个 PR - 所有服务的 history / branch / CI 在同处 - 重构跨服务时方便(IDE refactor 全 monorepo 生效) **缺点**: - repo 越来越大(clone 慢;shallow clone 缓解) - CI 要 path-based filter(不能"改 README 跑全部测试") - 工具支持差异(Bazel / Nx / Turborepo 等 monorepo build 工具学习成本) ## D. 单独发包 shared 抽成 npm / pip 包发到内部 registry: ```bash # shared 项目 npm publish --registry https://npm.internal.example.com # 使用方 npm i @myorg/shared ``` **优点**: - 标准化语言生态 - 严格版本管理 - 不同服务可用不同 shared 版本 **缺点**: - 改 shared 要发版 → 更新 → 测试 → 多步骤 - 内部 registry 要维护 - 不适合 quick iteration ## 决策矩阵 | 场景 | 推荐 | |---|---| | 5-10 个服务 + 1 个公司 + 同一团队 | **monorepo** | | 开源项目想用别人的 OSS lib | submodule(少改) | | 想 vendor 别人代码 + 偶尔修改 | subtree | | 多公司 / 跨组织共享 lib | 单独发包 | | 多团队 + 服务有独立发版周期 | 单独发包 | | 想保持简单 + 改动频繁 | monorepo | 实际我的经验:**绝大多数场景 monorepo 是最佳选择**。submodule 是 "避免 monorepo 的 hack",长期维护成本高。 ## monorepo 工具 JavaScript / TypeScript: ```bash # package.json { "private": true, "workspaces": ["packages/*", "services/*"] } # 用 npm workspaces / pnpm workspace / yarn workspaces ``` ```bash pnpm install # 装所有 pnpm -r build # 在所有包里跑 build pnpm --filter api dev # 只对 api 包 ``` 更高级:Turborepo / Nx,加 task caching + 增量 build。 Python:用 uv workspace: ```toml # pyproject.toml [tool.uv.workspace] members = ["packages/*", "services/*"] ``` Go:go workspace: ```bash go work init go work use ./service-a ./service-b ./pkg/shared ``` Rust:Cargo workspace 在 `Cargo.toml` `[workspace]` 里。 ## CI 优化(monorepo 必备) GitHub Actions path filter: ```yaml on: pull_request: paths: - 'services/api/**' - 'packages/shared-models/**' - '.github/workflows/api.yml' jobs: test-api: ... ``` 只在 api 或它依赖的 shared 改动时跑 api 测试。 或者用 `dorny/paths-filter` action 做动态: ```yaml - uses: dorny/paths-filter@v3 id: changes with: filters: | api: 'services/api/**' web: 'services/web/**' shared: 'packages/shared-*/**' - if: steps.changes.outputs.api == 'true' || steps.changes.outputs.shared == 'true' run: pnpm --filter api test ``` 避免改一行触发全部 CI。 ## 我们的真实迁移 从 6 个独立 repo + Git submodule 到 monorepo: | | submodule before | monorepo after | |---|---|---| | 新人 clone 步骤 | 5 步 | 1 步 | | 跨 service 改动 | 6 个 PR | 1 个 PR | | build 时间(CI 全跑) | 18min | 12min(with cache: 4min) | | repo 大小 | 6 × 50MB | 280MB | | 全部签新版本 | 几小时 | 单 PR + tag | monorepo 显著简化。 ## 踩过的坑 ### submodule 类 1. **改 submodule 内代码忘 push**:在 lib/shared 改了 + 主 repo commit 更新引用 + 推主 repo。同事 clone 后 lib/shared 拿到的 commit 在 origin 不存在 → fail。**先 push submodule,再 push 主 repo**。 2. **`git checkout` 切分支 submodule 不变**:默认 git 不自动更新 submodule 到目标分支记录的版本。`git config submodule.recurse true` 让所有命令自动 recurse。 ### subtree 类 3. **subtree push 偶尔失败**:主 repo 内 commit 太散,subtree 算 patch 出错。先 squash 合并相关 commit 再 push。 ### monorepo 类 4. **某 service 重 build 全 monorepo**:必须做 path-based filter + build cache。否则 monorepo 反而比独立 repo 慢。 5. **依赖循环**:service A 用 shared B,shared B 用 service A 的某 helper(不该)→ 循环。monorepo workspace tool 应该报错;强制依赖 方向 packages/* → services/*,不反向。

bash 脚本 5 个安全前缀:让 silent failure 显形

## 起因 线上发生过这个 bug:deploy 脚本第 3 步 `cp` 失败(路径变了), 但脚本继续跑第 4 步 `restart`,导致服务用了老配置 + 看着 deploy 成功。 半小时后才发现版本没真换。 bash 默认行为:命令失败继续往下跑。这是脚本灾难的根源。 下面是我每个生产脚本都加的 5 个前缀。 ## 5 个安全选项 放每个 bash 脚本开头: ```bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' ``` 逐个解释: ### 1. `set -e`:任一命令 fail 立即退出 ```bash set -e cp /tmp/foo /backup/foo # 失败 echo "this won't print" # 不再执行 ``` 唯一例外:被 `if` / `while` 测试的命令 / 用 `||` `&&` 链接的命令 / `!` 否定的命令。 ```bash set -e if cp /tmp/foo /backup/; then echo ok fi # cp 失败时 if 走 else 分支,脚本继续 cp /tmp/foo /backup/ || echo "cp failed" # || 让 cp 失败不中断脚本 ``` ### 2. `set -u`:用未定义变量立即报错 ```bash set -u echo "$undef" # bash: undef: unbound variable # 立即退出 ``` 防"变量名打错"导致 `rm -rf $WRONG_NAME/file` 跑成 `rm -rf /file` (如果 `WRONG_NAME` 没定义就是空 → `rm -rf /file` 极危险)。 要允许"可能未定义": ```bash echo "${MY_VAR:-default}" # 未定义时用 default echo "${MY_VAR:?must be set}" # 未定义时报错且自定义消息 ``` ### 3. `set -o pipefail`:pipeline 任一段失败 → 整段失败 ```bash set -o pipefail curl https://example.com/data.json | jq .users | head # curl 失败时(如 404),jq 仍正常处理(处理 empty),head 正常 # 没 pipefail:echo $? = 0(最后一段 head 成功) # 有 pipefail:echo $? = curl 的退出码 ``` pipeline 中间失败默认被忽略。pipefail 让它显形。 ### 4. `IFS=$'\n\t'`:把"按空格分词"改成按换行 / tab ```bash # 默认 IFS=" \t\n" files="hello world.txt" for f in $files; do echo "$f" done # hello # world.txt # 一个文件名被当成两个 IFS=$'\n\t' files=$(ls) for f in $files; do echo "$f" # 包含空格的文件名也作为一个 done ``` 文件名 / 输入含空格时分词错误是经典 bug。`IFS=$'\n\t'` 减少踩坑。 (更彻底用数组 + `for f in "${arr[@]}"` 引号包裹。) ### 5. `set -x`:调试时显示每条命令(可选) ```bash set -x cp a b # 输出:+ cp a b rm b # 输出:+ rm b ``` 不要生产开(输出 noise + 可能 log 敏感数据)。临时 debug 加 / 移。 ## 完整模板 ```bash #!/usr/bin/env bash # 描述:部署 X 服务的脚本 # 用法:./deploy.sh [staging|prod] set -euo pipefail IFS=$'\n\t' # 1. 校验参数 if [ $# -lt 1 ]; then echo "Usage: $0 [staging|prod]" >&2 exit 64 fi ENV=$1 case "$ENV" in staging|prod) ;; *) echo "unknown env: $ENV" >&2; exit 64 ;; esac # 2. 环境变量校验 : "${API_TOKEN:?API_TOKEN must be set}" : "${TARGET_HOST:?TARGET_HOST must be set}" # 3. 工作目录 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # 4. 临时文件清理 TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT # 5. 主逻辑 echo "=== building ===" make build echo "=== deploying to $ENV ===" rsync -avz dist/ "$TARGET_HOST:/srv/myapp/" echo "=== restarting ===" ssh "$TARGET_HOST" 'systemctl restart myapp' echo "=== verifying ===" sleep 5 HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "https://$TARGET_HOST/") if [ "$HTTP_CODE" != "200" ]; then echo "post-deploy check failed: HTTP $HTTP_CODE" >&2 exit 1 fi echo "=== done ===" ``` ## 其它常用 pattern ### trap:清理 + 异常处理 ```bash TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT # 正常退出 / 错误退出 / Ctrl-C 都执行清理 trap 'echo "ERROR: line $LINENO"; cleanup' ERR # set -e 触发的退出会执行 ERR trap ``` ### 函数化 + log ```bash log() { echo "[$(date -Iseconds)] $*" >&2; } die() { log "FATAL: $*"; exit 1; } main() { log "start" do_step_1 || die "step 1 failed" do_step_2 || die "step 2 failed" log "ok" } main "$@" ``` ### check command exists ```bash require() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1" } require jq require rsync require ssh ``` ### 安全的 mktemp ```bash TMPFILE=$(mktemp) # 比 TMPFILE=/tmp/foo$$ 安全(mktemp 用 random + safe perms) trap 'rm -f "$TMPFILE"' EXIT ``` ### 不要 cd 后忘记 cd 回来 ```bash # ❌ cd /tmp && do_stuff # 之后的命令在 /tmp 跑 # ✅ 用 subshell 包 (cd /tmp && do_stuff) # 退出 subshell 自动回原目录 # 或 pushd / popd pushd /tmp >/dev/null do_stuff popd >/dev/null ``` ## 静态检查:shellcheck ```bash brew install shellcheck shellcheck deploy.sh ``` shellcheck 几百条规则查"潜在 bug + 反模式"。比如: - 没 quote 的变量(`$var` vs `"$var"`) - 用 `[[ ]]` 还是 `[ ]` - 多余的 `cat` (Useless Use of Cat) - 未定义的变量 pre-commit / CI 跑 shellcheck 强制无错。 ## bash vs sh vs zsh - `#!/bin/sh`:POSIX shell,最 portable,功能少 - `#!/bin/bash`:bash 特性可用(`[[ ]]` / `<()` / 数组等) - `#!/usr/bin/env bash`:找 PATH 里的 bash(Mac brew bash 比系统 3.2 新) 生产脚本写 `bash` 而非 `sh`,能用现代特性。 极小 alpine container 没装 bash → 用 `sh` 或装 bash。 ## 几个真实例子 ### A. `rm -rf "${BUILD_DIR}/"` 删错 `BUILD_DIR` 没定义 → `rm -rf /`。 `set -u` 阻止: ``` bash: BUILD_DIR: unbound variable ``` 或者用 `: "${BUILD_DIR:?}"` 显式断言。 ### B. `curl | bash` 中间断 下载安装脚本时,pipefail + curl 校验: ```bash URL=https://example.com/install.sh curl -fsSL "$URL" -o /tmp/installer.sh sha256sum -c expected.sha256 bash /tmp/installer.sh # 比 curl | bash 安全:能验签 + 失败时不执行 ``` ### C. ssh 远程脚本 ```bash # 远端的环境变量 / set -e 都要在 ssh 里设 ssh server <<'EOF' set -euo pipefail cd /srv/app git pull systemctl restart myapp EOF ``` 注意 `<<'EOF'`(单引号)防止 local 解释 `$var`;本地变量要传 `<< EOF` (无引号)。 ## 效果 我们 ops 团队把所有 shell 脚本套用这套规范 + pre-commit shellcheck 强制后: - "中途失败但脚本继续"类 bug 归零 - 调试时间下降明显(trap + log 一眼看到失败行) - 新人脚本上线 review 时间 -50%(shellcheck 帮 review) ## 踩过的坑 1. **`set -e` + function 退出码**:function 里 `return 1` 不立即退出 外层(只有命令)。要 `local var; var=$(cmd) || return 1` 显式判断。 2. **`[[ var = "x" ]]` 单 `=` 在 sh 里**:bash 单 `=` 也行;纯 sh 用 `[ "$var" = "x" ]` 单括号 + 单 `=`。 3. **pipefail + 期望 pipeline 部分失败**: ```bash set -o pipefail grep foo huge.log | head -1 # head 提前关闭 stdin → grep 收 SIGPIPE → 失败 ``` `|| true` 容忍这种情况。 4. **subshell 里 set -e 不继承**: ```bash set -e ( false; echo "still here" ) # subshell 里 set -e 仍有效 echo "main" # 这行根据 () 退出码决定 ``` 实际 () 子 shell 默认继承 -e,但有些版本 / 行为不一。验证。 5. **`echo` 不可靠**:含 `-` / `\` 的字符串可能被 echo 解释。 生产用 `printf '%s\n' "$var"`。