POSIX、sh、bash、zsh:什麼可攜、什麼不可攜¶
Shell 家族的實戰指南 — POSIX 到底規範了什麼、爲什麼這個語言又醜又活得很好、
以及這個 repo 在哪些情況下刻意挑 sh / bash / zsh。具體 file layout
請看 shells/architecture.md;bash + ble.sh + OMB
init 順序請看 shells/bash.md;alias inventory 請看
shells/aliases.md。
POSIX 到底是什麼¶
POSIX 是一組標準,不是某個 shell 也不是某個 OS。最新版是 POSIX.1-2024 / IEEE Std 1003.1-2024 / The Open Group Base Specifications Issue 8。包含三層:
POSIX
├── C / system API: fork、exec、pipe、signal、filesystem...
├── Utilities: cp、mv、grep、sed、awk、make、find...
└── Shell language: sh 語法、pipeline、redirect、變數、
if / for / case...
平常大家說「POSIX shell」,指的幾乎都是 Shell Command Language 那一章 —
也就是 /bin/sh 方言 — 不是 bash 也不是 zsh。bash 和 zsh 都是 POSIX
superset:合法的 POSIX sh script 都是合法的 bash script,反之不成立。
Shell 家族對照¶
| Shell | 它是什麼 | 你會在哪遇到 |
|---|---|---|
sh |
Bourne / POSIX-sh 規範 | #!/bin/sh 腳本;最低共同分母 |
bash |
GNU Bourne-Again Shell — sh superset,多了 array、[[ ]]、pipefail、process substitution |
多數 Linux 預設、macOS /bin/bash(base 是 3.2,brew 裝 5.x) |
zsh |
強大的互動 shell,sh-ish 但不是 sh | macOS Catalina 起的預設;本 repo 偏好的互動 shell |
dash |
Debian Almquist Shell — 小、快、嚴格 POSIX | Ubuntu /bin/sh 指向 |
ash |
Almquist 家族 — 更小 | Alpine / BusyBox 的 /bin/sh |
ksh |
Korn shell — array、[[ ]] 等「現代」feature 的源頭 |
Solaris、AIX、部分 BSD |
fish |
Friendly Interactive SHell — 刻意不 POSIX | 純互動使用 |
nushell |
結構化資料 pipeline(record / list / table) | 現代資料密集任務的選項 |
shebang 的選擇 = 你在宣告 intent contract:
#!/bin/sh # 「我希望這在 dash、ash、bash --posix... 都能跑」
#!/usr/bin/env bash # 「我需要 bash feature(array、pipefail、[[ ]]...)」
#!/usr/bin/env zsh # 「我需要 zsh-only 行爲(glob qualifier、ZLE、compdef...)」
zsh 雖然有 emulate sh / emulate ksh 模式可以切過去,但不要假設 zsh
腳本能默默當 /bin/sh 用。最常見的兩個地雷是 word splitting(zsh 預設不
切未加引號的展開)與 ** glob(zsh 把它當特殊字元處理,不需要
globstar)。
Shell 爲什麼還活著¶
- 到處都是。 Linux server、macOS、BSD、Docker image、CI runner、
嵌入式 router、NAS、cluster login node — 都有某種
/bin/sh。即使 5 MB 的 BusyBox image 也有ash。Shell 是最低共同分母。 - 它是 command glue。 Shell 不是好的通用語言,但它是把 process 串起來 最強的語言。語法跟 Unix process model 幾乎同構:
cmd arg1 arg2 # fork + exec
cmd1 | cmd2 # pipe(2)
cmd > out 2> err # dup2 fd
cmd & # fork without wait
wait # waitpid
$? # exit status
- CI / Docker / Make / install script 跑不掉它。 每一行
RUN apt-get install …、每個 GitHub Actions 的run: |、每個Makefilerecipe,最後都把字串丟給/bin/sh -c(或SHELL指定的)。 你可以拿工具糊上去,但走不出來。
結論:shell 流行不是因爲語法漂亮,是因爲它站在 Unix process model 的正 中央,而且已經裝好了。
關鍵語法 — 以及 POSIX 在哪邊結束¶
每一段都標出 POSIX-sh 寫法 vs bash/zsh 才有的延伸 — 後者在 dash /
ash 直譯器裡會直接爆。
變數與 quoting¶
name="Alice" # POSIX。`=` 兩邊**不能**有空格。
echo "$name" # 展開時永遠加雙引號。
echo '$HOME' # 單引號不展開。
echo "$HOME" # 雙引號會展開。
export PATH="$HOME/.local/bin:$PATH"
FOO=bar command arg # 一次性的 env 注入。
可攜性最大的地雷是沒加引號的展開:
file="hello world.txt"
rm $file # 錯 — 拆成 `rm hello world.txt`(兩個 arg)。
rm -- "$file" # 對。`--` 防止以 `-` 開頭的檔名被當 option。
原則:任何不是刻意要 word-split / glob 展開的 $var,都加雙引號。
ShellCheck 會強制這條。
Command substitution¶
Exit status、&&、||¶
經典誤用 — 把 && / || 當成 if/else:
兩個分支語意上不一樣時就用真的 if:
[ ] vs [[ ]]¶
[ -f "$file" ] # POSIX。`[` 是個 command — 空格必要。
[[ -f "$file" && -n "$x" ]] # bash/zsh only — 更安全(不做 word split,
# 支援 `&&` / `||` / `==` / `=~`)。
[[ ]] 嚴格地比較好用 — 但不是 POSIX。#!/bin/sh 就只能用 [ ]。
Loop¶
# POSIX
for f in *.txt; do echo "$f"; done
# bash/zsh only — C-style
for ((i=0; i<10; i++)); do echo "$i"; done
一行一行讀檔案¶
IFS= 保留前後空白;-r 保留 backslash 不被特殊處理。這兩個 flag 幾乎
永遠是你要的。
Function¶
Redirection¶
cmd > out # stdout
cmd >> out # stdout append
cmd 2> err # stderr
cmd > all 2>&1 # stdout + stderr — POSIX,順序很重要
cmd &> all # bash/zsh 簡寫 — 不是 POSIX
cmd < input # stdin
Pipe 與 pipefail¶
pipefail 是大部分「可攜」install script 默默變成 bash script 的最大
原因。
Heredoc¶
cat > config.ini <<'EOF' # 引號 EOF:**不**展開(保留字面 $var)
[server]
host = $LITERAL_LEFT_ALONE
EOF
cat > config.ini <<EOF # 沒引號 EOF:$var 會展開
host = $HOSTNAME
EOF
大家都會踩的痛點¶
1. POSIX 幾乎沒有 array¶
bash 和 zsh 有 array:
POSIX sh 沒有。你能用的「list」只有 positional parameter($@)、
換行分隔字串、或暫存檔。這是可攜 script 一旦稍微複雜就秒變 bash script
的最大原因。
2. Quoting 不寬容¶
地球上每個 dotfile bootstrap 都至少出過一次 rm -rf $UNSET/ bug。
ShellCheck 抓得住幾乎全部。
3. set -e 有反直覺的破洞¶
-e(出錯就退)在這幾種情境不會觸發:if、while、&&、||、
某些 shell 的 command substitution、被檢查 return value 的命令。bash
manual 列得出例外清單 — 但反直覺。穩健寫法:
-E 讓 ERR trap 可以繼承到 function / subshell / command
substitution。以上全部不是 POSIX。
4. /bin/sh 不是同一個 shell¶
| OS | /bin/sh 實際是 |
|---|---|
| Ubuntu / Debian | dash |
| Alpine、嵌入式、Docker scratch | BusyBox ash |
| macOS | bash POSIX 模式(base 是舊的 3.2) |
| RHEL / Fedora | bash POSIX 模式 |
| FreeBSD / NetBSD | 自家 sh |
在 Mac 「跑得起來」的腳本到 Ubuntu 可能直接爆,因爲 [[、source、
arr=(a b c)、echo {1..10}、&> 全是 dash 不認的 bash-ism。宣稱
可攜的話就用 dash 測 — apt install dash 然後 dash ./script.sh。
5. 全部都是文字流¶
很優雅 — 直到第 N 欄位移、process name 有空格、locale 改了日期欄位
格式。處理結構化資料的正解是不要用 awk/sed 硬切 — pipe 給 jq /
yq / dasel / Python:
Nushell 從根本重新設計(pipeline 流的是 record / list / table 而不是 bytes),但它不會、也永遠不會是最低共同分母。
Alternative — 什麼時候該離開 shell¶
ShellCheck — 不是替代,是必裝¶
如果 shell 寫超過大概 10 行就跑 ShellCheck。這個 repo 的 pre-commit
hook 有它;參考 tools/pre-commit.md。
Python — 任何有結構的東西¶
需要:真正的資料結構、JSON / YAML / TOML 解析、retry、logging、test、 跨平台路徑處理 — 切到 Python。Shell 版:
vs Python:
from pathlib import Path
import json
for path in Path(".").glob("*.json"):
data = json.loads(path.read_text(encoding="utf-8"))
if isinstance(name := data.get("name"), str):
print(name)
Python 版處理得了缺檔、JSON 壞掉、Unicode 名字而不會出意外。本 repo
的 PEP 723 inline-deps Python script(uv
run --script)就是爲此而生 — 看 scripts/dotfiles_init.py 和
scripts/redact_secrets.py。
just / make — 「我只是想記住那條長命令」¶
如果 shell script 大多只是「跑這條長命令」的 alias,用 task runner。
本 repo 用 just:
make 通用性最高;just 對現代專案語法更舒服。
chezmoi / Ansible / Nix — 管狀態,不要管命令¶
如果你的 bootstrap script 是 apt install …; mkdir …; ln -s …;
systemctl enable …,你在重發明 configuration management。本 repo 用
chezmoi 管 dotfile + Ansible 管系統套件 — 看
this_repo/architecture.md 解釋為何如此。
Nix 是更嚴謹(也更複雜)的選項。
fish、nushell — 純互動使用¶
fish 是很好的日用 shell 但刻意不 POSIX
相容。不要拿
fish 寫 install.sh。
nushell 結構化 pipeline 優先;做臨時資料探索很棒,不適合當 /bin/sh。
本 repo 怎麼選¶
| 場景 | Shell 選擇 | 原因 |
|---|---|---|
chezmoi run_* 腳本 |
bash template | 嚴格模式、pipefail、array、[[ ]] 都值得;chezmoi 跑的環境保證有 bash |
dot_config/shell/*.sh.tmpl(3-tier 共用層) |
POSIX 子集 | zsh 和 bash 都 source — 看 shells/architecture.md。Source 時用 $ZSH_VERSION / $BASH_VERSION 分派可以;ZLE / compdef / bind -x 不行 |
dot_config/zsh/**/*.zsh |
zsh | ZLE widget(aisuggest、tools picker、sesh、television)、compdef、glob qualifier |
dot_config/bash/*.bash |
bash | bind -x / ble-bind、OMB plugin array、ble.sh 整合 — 看 shells/bash.md |
scripts/*.sh(本 repo CLI) |
bash + 嚴格模式 | 跑在 maintainer 的機器;bash 必備 |
| user 機器的互動 shell | zsh(預設)或 bash(opt-in) | primaryShell chezmoi prompt — 只控制 chsh,兩個 shell 都會部署。看 AGENTS.md |
AGENTS.md 的硬規則:shell-agnostic 的 helper 放 dot_config/shell/
(POSIX)。只有真的需要那個 shell 的 feature 才放 dot_config/zsh/ 或
dot_config/bash/。
Template¶
可攜 POSIX sh¶
#!/bin/sh
set -eu
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
main() {
command_exists git || die "git is required"
repo_dir="${1:-}"
[ -n "$repo_dir" ] || die "usage: $0 REPO_DIR"
mkdir -p -- "$repo_dir"
cd -- "$repo_dir"
printf 'working in %s\n' "$PWD"
}
main "$@"
要點:每個展開都加引號、-- 防 dash-prefix 名字、command -v 是 POSIX
測試 executable 的方式(which 沒被標準化、type 各 shell 輸出不同)。
沒有 array、[[ ]]、pipefail、local — 在 dash、ash、busybox sh 都跑。
Bash + 嚴格模式¶
#!/usr/bin/env bash
set -Eeuo pipefail
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
trap 'die "failed at line $LINENO"' ERR
main() {
local repo_dir="${1:-}"
[[ -n "$repo_dir" ]] || die "usage: $0 REPO_DIR"
mkdir -p -- "$repo_dir"
cd -- "$repo_dir"
printf 'working in %s\n' "$PWD"
}
main "$@"
要點:set -Eeuo pipefail + trap … ERR 是 bash 嚴格模式的典型寫法。
local 維持函式作用域;[[ ]] 比 [ ] 安全;array 與 pipefail 隨需
取用。
推薦的分界線¶
選新檔案要用什麼語言時的實戰啟發:
| 長度與形狀 | 用什麼 |
|---|---|
| ≤10 行、command glue | POSIX sh |
| 10–100 行、自動化 | bash + ShellCheck + 嚴格模式 |
| >100 行、有資料結構或錯誤處理 | Python(PEP 723 inline-deps) |
| 互動 shell config | zsh(或 bash + ble.sh) |
| 狀態管理(套件、服務、dotfile) | chezmoi + Ansible,不要 shell |
| JSON / YAML / TOML 處理 | jq / yq / dasel / Python |
Shell 是好膠水;不是好通用語言。知道分界線在哪裏 — 並把對的檔案寫在分界 線正確的那一邊 — 就佔了 dotfile repo 可維護性的大部分。
延伸閱讀¶
shells/architecture.md— 本 repo 的 3-tiershell/zsh/bash佈局shells/bash.md— bash bootstrap、ble.sh、oh-my-bash init 順序shells/aliases.md— 本 repo 定義的所有 alias 與 shell function,按 tier 標籤- POSIX.1-2024 Shell Command Language — 真正的標準
- ShellCheck wiki — 每個 code 都有 解釋
- Bash Pitfalls (Greg's wiki) — 「看起來對但其實錯」的經典清單