上一篇我們追蹤了一行命令在進到程式之前發生的事:終端機接收鍵盤輸入、Shell 解析文字並尋找執行檔、作業系統建立 Process 並將參數陣列傳給程式。當控制權正式移交給我們的 CLI 程式後,程式收到的只是一串原始字串陣列(argv)。Shell 並不知道各個字串的業務語意,程式的第一步就是將這串無標籤的文字拆解為有意義的介面元素。
站在 CLI 開發者的視角,一行命令主要由 Command(動作)、Arg(操作對象)與 Flag(選項)三個核心組成分工而成。
以下以常見的 git 命令為例,展示兩組典型命令在解析後的結構對照:在 git commit -m "fix bug" 中,-m 與 "fix bug" 屬於修飾執行行為的 Flag 與其數值;而在 git checkout main 中,main 則是直接指定操作目標的 Arg。兩組命令對照呈現了 CLI 解析器如何將傳入的參數,區分為調整行為的選項與指定目標的對象:

當控制權移交給程式後,解析器會依據語意將傳入的 argv 拆解為三種不同職責的介面元素:
Command 是整個 CLI 的「動詞」,代表使用者想要執行的核心功能,決定程式該走入哪一條執行路徑。
在 CLI 設計中,Command 依複雜度有兩種主要型態:
curl、cat、jq。這類工具功能專一,執行檔檔名本身(Root Command)就是唯一的動作,直接接收 Flag 與 Arg,不需要額外拆分子命令。git、docker、kubectl。當工具涵蓋的功能眾多時,執行檔本身只作為總入口,底下再延伸出多個特定功能的子命令(Subcommand),例如 git commit 只專注於建立提交,git push 專注於推送。無論是頂層命令還是子命令,Command 都具備以下核心特性:
--help 說明文件。git remote add 或 docker compose up,將相同領域資源的操作歸類在同一個父命令下。Arg 是直接跟在 Command 後面的值,用來指定這次動作要作用的具體目標或資源:
git checkout main 中的 main 是要切換的分支,或是 rm file.txt 中的 file.txt 是要刪除的檔案。argv 陣列中的位置來判讀意義。因此 Arg 的順序不能隨意調換,例如 cp source.txt dest.txt 若顛倒為 cp dest.txt source.txt,操作的方向與結果就會完全相反。Flag 是對 Command 動作的修飾,用來調整程式執行的選項與行為細節:
-- 開頭的全稱單字,如 --message)與短格式(以 - 開頭的單字母縮寫,如 -m)。長 Flag 可讀性高、適合寫入腳本或自動化流程;短 Flag 則方便使用者在終端機中快速輸入。- 後面。例如 ls -l -a -h 可以簡寫為 ls -lah;tar -x -v -f 可以簡寫為 tar -xvf。若組合中包含需要接值的短 Flag(如 -m "message"),該 Flag 必須放在組合的最後一位(例如 -am "message")。true),例如 git commit --amend。當某個選項的預設值本身就是 true 時,CLI 的設計慣例是提供帶有 --no- 前綴的反向開關(如 git commit --no-verify 用來關閉 Hook 檢查),或是支援 --verify=false 顯式關閉。三者的核心特性對照如下:
-- 或 - 前綴標籤,通常為選填且具備預設值。在設計 CLI 介面時,最常面臨的兩個架構抉擇是「這該做成 Arg 還是 Flag?」以及「這該做成 Subcommand 還是 Flag?」。
關鍵在於資料是否具備「單一明確的目標性質」,還是「多個無順序的屬性設定」。
git checkout main 中的分支名稱。這是因為動作與目標已經構成直覺的「動詞 + 受詞」關係,語意非常清晰。rm a.txt b.txt。這是因為每個傳入項目的性質與角色完全相同,彼此沒有順序歧義。Arg 能自然接收不定長度的參數列表;若改用 Flag 重複指定,例如 rm --file a.txt --file b.txt,會讓命令變得極度冗長且反直覺。mytool Alice 42 admin。這種情況應改用具名 Flag 明確標註語意:
mytool --name Alice --age 42 --role admin
關鍵在於新功能是「一個全新的動作」,還是「既有動作的細節變化」。
git commit 與 git push 職責完全不同,必須劃分為不同命令。mytool --mode=create 應重構為語意更清晰的子命令 mytool create 與 mytool list。git remote add。git commit --message "fix bug"
git commit --amend
當 CLI 程式解析完 Command、Arg 與 Flag 並執行完業務邏輯後,最後一步是「將結果回傳給使用者與作業系統」,完成整個執行生命週期。
CLI 的輸出並非單純「將字串印在螢幕上」,而是同時面向使用者與自動化系統,包含兩個截然不同的回饋管道:
作業系統在啟動每個程式時,預設會提供兩個獨立的輸出管道(在 Unix 底層稱為檔案描述符 FD 1 與 FD 2):
兩者的關鍵差別在於:「這是最終產出的資料,還是執行過程的訊息」。
如果沒有分開,執行過程中的提示文字就會直接污染資料流。只有將兩者切開,呼叫端(使用者、Shell 腳本或其他工具)才能依需求自由控制輸出:
>):mytool export > data.json|):mytool export | jq .jq 就會因讀到非 JSON 格式的文字而解析失敗。2>):mytool export 2> /dev/null | jq .因此,CLI 開發的核心原則很明確:命令產出的結果資料走 stdout,其餘任何過程提示、除錯日誌與錯誤訊息通通走 stderr。
程式結束退出時,必須向作業系統交還一個整數數值(範圍通常為 0 ~ 255),稱為 Exit Code(退出狀態碼):
0 代表成功(Success):一切順利完成。0(1 ~ 255)代表失敗(Failure):常見如 1 為通用錯誤、2 為參數語法錯誤(例如少傳必填 Flag)。人類判斷指令是否成功,是靠眼睛閱讀終端機印出的英文字句(如 Success! 或 Error: not found);但 Shell 腳本、CI/CD 流程與 AI Agent 無法也不該依賴字串比對,它們完全仰賴 Exit Code 來決定後續流程。
在 Shell 腳本中,常見的連鎖運算符會根據前一個指令的 Exit Code 決定下一步:
# 只有當 build 成功回傳 0 時,後續的 deploy 才會被觸發
mytool build && mytool deploy
# 若備份失敗(回傳非 0),則觸發報警
mytool backup || send-alert "Backup failed!"
在 CI/CD(如 GitHub Actions)中,流水線之所以知道某個測試步驟失敗並亮紅燈,也完全是因為該指令結束時的 Exit Code 不是 0。
在撰寫 CLI 程式時,最嚴重的 Bug 之一就是「捕捉了錯誤、印出紅字,卻以 Exit Code 0 正常結束」:
Error: failed to connect。0,外層的 CI/CD 判定該步驟為「成功綠燈」,導致整個部署流程帶著錯誤繼續上線。os.Exit(1))。一行 CLI 命令在進入程式後的運作與設計至此已完整閉環:從接收原始 argv 拆解語意,到執行完畢將結果透過 stdout/stderr 與 Exit Code 回應給系統。下一篇我們將設定 Go 開發環境,並用 Cobra 把這套 Command、Arg 與 Flag 寫成第一個可執行的 CLI 專案。