Shell 中的冒號 (:) 毫無作用。但還是用它吧
Shell 中的冒號 (:) 毫無作用。但還是用它吧
Introduction
Shell 中的冒號 (:) 是一個內建的空指令 (null-command),它會評估其參數並捨棄結果。它本身不執行任何操作,但當它放在參數展開 (parameter expansion) 之前時,可以讓展開動作發生,而不會讓 shell 嘗試將產生的字串作為指令來執行。
Validating required arguments
使用 : 搭配 ${VAR:?diagnostic} 展開,提供了一種單行的方式來確保變數已設定且不為空。
: "${1:?missing argument, aborting.}"
echo "Hello $1!"
如果 $1 未設定或為空,shell 會將診斷訊息輸出到 stderr 並以非零狀態退出;否則,展開會產生 $1 的值並繼續執行腳本。同樣的技術也適用於具名變數,例如 : "${GREET_NAME:?missing argument, aborting.}",這會在錯誤訊息中包含變數名稱。
Setting defaults with :=
${VAR:=default} 展開會在 VAR 未設定或為空時賦予預設值。在前面加上 : 會在執行賦值動作的同時捨棄該賦值結果。
: "${DATA_DIR:=/var/data}"
: "${RETRIES:=3}"
執行這些行之後,如果 DATA_DIR 和 RETRIES 尚未設定,它們將持有預設值。
Truncating and testing files
在 : 後面使用重定向 (redirection) 會將檔案截斷為零長度,因為空指令不會產生任何輸出。
: > error.log # truncate error.log
: > error.log > access.log # truncate both files
同樣地,一個僅包含重定向和 : 的子 shell (subshell) 可以用來測試可讀性或可寫性。
( : < dataset.json ) && echo YES # dataset.json readable?
( : >> result.json ) && echo YES # result.json writable?
之所以需要冒號,僅僅是因為子 shell 需要一個指令;重定向本身才是執行工作的核心。
Using colon in trap and conditionals
trap 內建指令需要一個指令;: 在不執行任何操作的情況下滿足了這一需求。
trap : INT
sleep 60 # sleep remains interruptible
在 if-else 結構中,如果 then 分支必須包含一個指令,: 可以作為佔位符。
if some-command; then : # command required else echo "command failed" fi
Frequently asked questions (from the article)
為什麼我需要這個空指令?沒有冒號展開不也會發生嗎?
參數展開無論如何都會發生,但如果沒有空指令或類似結構,shell 會將產生的字串視為要執行的指令。例如:
% ${HELLO:=123} zsh: command not found: 123
在前面加上 : 會在執行展開的同時捨棄結果:
% : ${HELLO:=123} % echo $HELLO 123
既然可以用 VAR=${VAR:-default-value},為什麼還要用空指令?
兩者達到的效果相同,但冒號形式只需提及一次變數,降低了打錯字的機率:
: "${DATA_DIR:=/var/data}" # DATA_DIR 只出現一次 DATA_DIR="${DATA_DRI:-/var/data}" # 可能出現打錯字 (DATA_DRI)
Community reactions (selected HN comments)
@gpvos 回想起了 Larry Wall 的語言重新設計法則 (Laws of Language Redesign):「每個人都想要冒號。Larry 得到了冒號。」
@amiga386 注意到了
if some-command; then : else echo "command failed" fi這個慣用法,並指出等效的if! some-command; then echo "command failed" fi也是符合 POSIX 標準的。@rgrau 建議直接在指令參數中內聯
:?測試,例如echo "${1:? first param required}",並描述了一種在迴圈體中使用冒號的 while 迴圈模式。@fphilipe 將冒號用作 Git 的 EDITOR,以便在不打開編輯器的情況下執行自動 squash 的互動式 rebase。
@archargelod 認為用
: "${1:?...}"取代可讀性高的 if 語句會損害可讀性,並提出了替代方案[ -z "$1" ] && { echo "missing argument, aborting." 1>&2; exit 1 }。@kevincox 發現
:?形式對於檢查必要的環境變數很有用,但認為許多其他範例使用專用指令會更清晰。@kazinator 批評子 shell 測試
( : < file ) && echo YES是不必要的,指出單靠重定向就能測試可讀性/可寫性,且使用: >> file建立零長度檔案可能並非好事。@olexsmir 分享了一個個人使用案例:在 dotfiles bootstrap 腳本中使用
: "${DOTFILES_PATH:=$HOME/.dotfiles}"為配置環境變數設定預設值。@luciana1u 評論說冒號毫無作用,這使得它成為「LLM 無法過度設計的唯一 bash 指令」。
@m2f2 描述了在 getopts 風格的迴圈中使用冒號:
while : ; do case "$1" in... esac done。@jeffrallen 感謝作者,但表示他不會使用這種慣用法,因為他更喜歡能向受眾解釋清楚的程式碼。
@lucideer 覺得這些範例只是將可讀性高的多行程式碼變成了單行程式碼,他認為這在腳本中沒有地位。
@normie3000 比起
if! x; then something; fi,更喜歡if x then :; else something; fi。@PunchyHamster 說他會拒絕引入這種單行程式碼的 pull request,並主張用 Python 或 Perl 重寫較長的腳本。
@garethrowlands 將這種技巧的需求歸因於 shell 語法中古老的字串替換特性。
@ocd 使用冒號來建立「無操作」別名,例如
: alias f3probe,以便快速搜尋遺忘的工具。@dominiwe 需要冒號來讓一個 shell 腳本同時成為有效的 YAML 檔案。
@del 奇想冒號是否是早期 Unix shell 中少數經過特殊處理的程式碼路徑之一。
@greatgib 說他第一次在 Claude 生成的程式碼中看到這種結構,後來又在本文中看到。
@search_facility 從 bash 切換到純 Python 腳本以獲得跨平台穩定性,並放棄了「bash zoo」。