在終端機操作 CLI 工具時,如果每次都需要手動打對完整的參數名稱與選項,不僅操作效率低,也容易因為打錯字而導致命令執行失敗。
為了降低記憶負擔並避免輸入錯誤,我們可以在兩個不同的執行階段提供互動輔助:
Tab 鍵時,由 Shell 觸發完成機制。例如在查詢服務狀態時指定 --env 參數,按 Tab 就能在 Enter 送出前直接列出 development、staging 或 production 等環境候選值,不必憑記憶手打。這兩種機制運作在不同的時間點:自動完成發生在 Enter 送出前,互動選單則發生在 Enter 送出後。本篇會從命令執行後的 Huh 表單開始實作,接著介紹命令執行前的 Shell 自動完成。
我們用一個像 Claude Code /model 的命令當例子。使用者執行命令後,CLI 會詢問三件事——切換到哪個模型、要給多少 extended thinking 的 token 預算、真的要不要套用這次切換。
畫面長這樣:
? Select model
▸ Sonnet 5
Opus 4.8
Haiku 4.5
? Switch to Opus 4.8. Are you sure? (y/N)
模型清單用方向鍵在選項裡挑,不用自己打對完整名稱(例如把 "Opus 4.8" 打成 "opus-4.8" 或漏掉版號);confirm 用一個按鍵決定要不要真的切換。跟自己用 fmt.Println 印提示、fmt.Scan(&model) 等使用者打字比起來,huh 把「輸入」換成「選擇」——選項本身就是合法值的清單,使用者選不出清單以外的字串,不用另外寫程式檢查打錯字。
這部分的範例程式碼放在 cli-sample/interactive-ui/cmd/huh_demo.go。
huh 把畫面拆成三層:Field 是一個問題(Select、Confirm、Input……);Group 是畫面上同時顯示、可以互動的一組 Field;Form 是一個或多個 Group 依序組成的整份流程,form.Run() 擋住程式直到填完或取消。
下面實際看「放進同一個 Group」跟「拆成兩個 Group」畫面上的差別。先看放進同一個 Group:「選模型」跟「Are you sure?」這兩個 Field 放進同一個 Group,程式跑起來,終端機畫面會同時顯示兩題:
? Select model
▸ Sonnet 5
Opus 4.8
Haiku 4.5
? Are you sure?
這兩題會同時停留在畫面上,使用者選完模型後按 Tab 或方向鍵,就能直接移到下面的 Confirm 作答。同一個 Group 裡的欄位不會分頁,寫成樹狀圖是:
Form(form.Run() 一路執行到填完或取消)
└── Group(兩個 Field 同時在畫面上)
├── Select(模型)
└── Confirm(是否切換)
如果改成兩個 Group——「選模型」自己一個 Group,「Are you sure?」放進另一個 Group——畫面一開始只會顯示:
? Select model
▸ Sonnet 5
Opus 4.8
Haiku 4.5
使用者選完按 Enter 後,終端機會清除這段選單畫面,接著在原地輸出下一個 Group:
? Are you sure?
在 Huh 的設計裡,一個 Group 就代表表單的一個分頁(Page)。使用者填完一個 Group 按 Enter 後清除舊題目、繪製新題目的過程,就是 CLI 互動裡的「換頁」。
同樣的道理,如果還要多問「要給多少 extended thinking 的 token 預算」,並且想讓「選模型」單獨一頁、換頁後才問預算與 Confirm,就是把三個 Field 拆進兩個 Group:
Form
├── Group 1(第一頁)
│ └── Select(選模型)
└── Group 2(Enter 換頁後顯示的第二頁)
├── Input(輸入 token 預算)
└── Confirm(是否切換)
程式碼對應第一種寫法(單一 Group,一頁問完):
import "github.com/charmbracelet/huh"
var model string
var confirmed bool
form := huh.NewForm(
// 只給一個 Group,底下兩個 Field 會同時顯示在同一頁
huh.NewGroup(
// Field 1:Select,選項清單是 Sonnet 5/Opus 4.8/Haiku 4.5
huh.NewSelect[string]().
Title("Select model").
Options(huh.NewOptions("Sonnet 5", "Opus 4.8", "Haiku 4.5")...).
Value(&model), // 使用者選的結果,Run() 結束後會寫進 model
// Field 2:Confirm,是非題,只有 y/N 兩種結果
huh.NewConfirm().
Title("Are you sure?").
Value(&confirmed), // 結果寫進 confirmed
),
)
// Run() 會一直佔住終端機,直到使用者按 Enter 填完,或 Ctrl+C 取消
if err := form.Run(); err != nil {
log.Fatal(err)
}
if confirmed {
fmt.Printf("Switched to %s\n", model)
}
進入 cli-sample/interactive-ui/ 執行 go run . huh,終端機會以 Huh 預設的 Charm 主題渲染出完整的表單介面:

同一個 Group 裡的欄位會同時排列在終端機畫面上。使用者可以用方向鍵在「Select model」選項間移動切換,按 Tab 移動到下方的「Extended thinking token 預算」輸入框,最後再切換至 Confirm 選擇 Yes 或 No,不需要在題與題之間換頁。
接續前面「Group 2:輸入 token 預算」那個 Input,使用者打字輸入的內容不一定合法——留空、打成非數字、數字小到沒意義,都要擋下來。huh 的作法是把檢查邏輯直接寫進 Validate,不用等 form.Run() 結束後自己再寫 if 檢查一次:
huh.NewInput().
Title("Extended thinking token 預算").
Placeholder("e.g. 4096").
Validate(func(s string) error {
if s == "" {
return errors.New("token 預算不能留空")
}
n, err := strconv.Atoi(s)
if err != nil {
return errors.New("必須是數字")
}
if n < 1024 {
return errors.New("token 預算至少要 1024")
}
return nil
}).
Value(&budget)
Validate 回傳的 error 不是 nil 時,huh 會直接在該欄位下方以紅色文字顯示錯誤原因,並將畫面卡在同一個 Field,使用者必須修改到合法才能按 Enter 繼續前進:

如上圖所示,當輸入值為 500(小於下限 1024)並按 Enter 時,下方隨即跳出 * token 預算至少要 1024,終端機不會進入下一個欄位,確保不合法的數值在送出前就被即時攔截。
密碼、API key、token 這類敏感資料輸入時不能顯示在螢幕上,避免被旁邊的人看到或留在 terminal scroll history。huh.NewInput() 內建 EchoMode(huh.EchoModePassword),打字時畫面上不顯示字元:
huh.NewInput().
Title("請輸入 API key").
EchoMode(huh.EchoModePassword).
Value(&apiKey)
對應範例:cli-sample/interactive-ui/cmd/complete_demo.go,root Command 是 interactive-ui。
Shell 提示字元下的自動完成,是由 Shell 與 CLI 程式 協同運作完成的:
Tab 鍵,並顯示候選清單。只要用 Cobra 建立 CLI,Cobra 預設就會自動產生 completion 子命令。這個子命令不執行業務邏輯,而是輸出給特定 Shell(如 zsh、bash)執行的補完腳本。
在本地開發與測試時,我們需要先編譯執行檔,並用 source 將腳本直接載入目前 Shell 的環境中:
cd cli-sample/interactive-ui
go build -o interactive-ui
export PATH="$PWD:$PATH"
# 將 interactive-ui 產生的補完腳本直接載入當前 Shell
source <(interactive-ui completion zsh)
語法中的 <(...)(Process Substitution)會把命令的輸出轉為暫時檔案交給 source 讀取,省去手動寫入 .sh 檔案的步驟。
腳本載入完成後,Cobra 預設就會自動補全子命令與 Flag 的名稱,因為這些名稱原本就註冊在 cobra.Command 與 Flag 結構裡,不需要寫任何額外程式碼:
$ interactive-ui des<TAB>
describe
$ interactive-ui describe --e<TAB>
--env
但若要補全參數的內容(例如 describe 後面有哪些資源、--env 後面有哪些環境),Cobra 無法預先知道,這部分就必須由我們在 Go 程式裡定義規則。
在 interactive-ui describe <TAB> 中,命令後方的資源名稱屬於「位置參數(Positional Argument)」。若候選值是固定的列舉,直接在 Command 設定 ValidArgs 陣列:
var describeCmd = &cobra.Command{
Use: "describe [resource]",
ValidArgs: []string{"web", "worker", "database", "cache"},
// 限制只能傳入 1 個參數,且必須存在於 ValidArgs 清單中
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
}
$ interactive-ui describe <TAB>
web worker database cache
ValidArgs 負責在按 Tab 時提供候選值;搭配 cobra.OnlyValidArgs 則能在執行時驗證輸入,阻擋清單以外的非法參數:
$ interactive-ui describe nope
Error: invalid argument "nope" for "interactive-ui describe"
宣告 Flag(例如 --env)時,Cobra 預設只知道它是字串型別,按 Tab 只會補出 --env ,不知道該提供哪些環境。
如果 Flag 的選項是固定的,使用 RegisterFlagCompletionFunc 搭配 cobra.FixedCompletions 綁定候選清單:
describeCmd.Flags().String("env", "development", "target environment")
describeCmd.RegisterFlagCompletionFunc(
"env", // 綁定的 Flag 名稱,要與註冊時一致
cobra.FixedCompletions(
[]string{"development", "staging", "production"},
// 補完選項後停止,避免 Shell 接著把當前目錄的檔案混進選單
cobra.ShellCompDirectiveNoFileComp,
),
)
$ interactive-ui describe web --env <TAB>
development staging production
ShellCompDirectiveNoFileComp 告知 Shell:這組選項就是全部,不要把當前目錄下的檔案名稱也混入候選清單。
如果候選清單無法在寫程式時定死(例如當前 Git 分支、線上 Kubernetes Pod 名稱,或是本地快取的資源),就必須在使用者按下 Tab 的瞬間即時動態查詢。
這時改用動態補完函式 ValidArgsFunction。每次使用者按 Tab,Cobra 都會現場執行這個函式取得最新清單:
var logsCmd = &cobra.Command{
Use: "logs [resource]",
Short: "Show logs of a resource",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// 若已填入第一個參數,代表目前在嘗試補第二個參數,直接不提供選項
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
// 實務上在此處呼叫 API 或讀取本地快取
return listResourceNames(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("showing logs of %q\n", args[0])
},
}
$ interactive-ui logs <TAB>
web worker database cache
本篇介紹了命令執行前後的兩種互動輔助:
ValidArgs、RegisterFlagCompletionFunc 與 ValidArgsFunction 分別提供靜態參數、靜態 Flag 與動態查詢的選項,讓使用者按 Tab 就能補齊內容。目前介紹的互動模式都屬於「單次執行」:命令執行或表單填寫完畢後,CLI process 就會隨即退出。下一篇我們將探討如何讓 CLI 長時間維持運行,透過 REPL(Read-Eval-Print Loop)在同一個 Process 中保留記憶體變數、連線與對話上下文,打造持續互動的操作體驗。