上一篇建好的 mytool 目前只有根命令,執行後只會印出說明文字。這篇我們要讓它開始處理具體工作:先建立第一個子命令、讓它接收並驗證位置參數,再進一步組織成多層子命令結構。
範例程式碼位於 cli-sample/mytool/。在 cmd/ 底下新增 greet.go,寫第一個子命令。這個命令負責印出問候語,執行 mytool greet 時在終端機輸出一行問候訊息:
// cmd/greet.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var greetCmd = &cobra.Command{
Use: "greet",
Short: "印出問候語",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello! 這是一個最簡單的子命令。")
},
}
func init() {
rootCmd.AddCommand(greetCmd)
}
Run 裡面放的是子命令執行時會執行的程式碼。當使用者在命令列輸入 mytool greet 時,Cobra 就會執行這個函式。
函式接收的 cmd 是當前命令物件,args 則是接在命令後面的位置參數。如果邏輯可能失敗、需要回傳錯誤,實務上會把 Run 改成 RunE,直接回傳 error 讓 Cobra 處理。
init() 是 Go 的特殊函式,同一個 package 裡的 init() 會在 main() 執行前自動執行。main.go import cmd package 時,這裡的 init() 會把 greetCmd 掛到 rootCmd 底下,因此不需要在 main.go 手動註冊。
重新 build 再執行一次:
go build -o mytool
./mytool
執行後會發現 Available Commands 這時多了一行:
Available Commands:
completion Generate the autocompletion script for the specified shell
greet 印出問候語
help Help about any command
除了剛註冊的 greet,清單中另外兩個是 Cobra 自動內建的命令:help 用來顯示說明,completion 則用來產生 Shell 的自動完成腳本。
執行 greet,確認子命令已經註冊:
./mytool greet
Hello! 這是一個最簡單的子命令。
接著讓 greet 接收使用者輸入的名字。執行 mytool greet Evan 時,印出 Hello, Evan!。將 cmd/greet.go 修改如下:
// cmd/greet.go
var greetCmd = &cobra.Command{
Use: "greet <name>",
Short: "印出問候語",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
fmt.Printf("Hello, %s!\n", name)
return nil
},
}
重新編譯並傳入名字測試:
$ go build -o mytool
$ mytool greet Evan
Hello, Evan!
在 RunE 函式中,傳進來的第二個參數 args 是型別為 []string 的字串切片。使用者接在命令後面的位置參數會依序存入切片中,透過 args[0] 就能拿到第一個位置參數 "Evan"。
Args 驗證參數數量如果使用者直接執行 mytool greet 忘記傳名字,或是傳了多個名字,Cobra 會因為設定了 Args: cobra.ExactArgs(1) 而自動回報錯誤:
$ mytool greet
Error: accepts 1 arg(s), received 0
$ mytool greet Evan Bob
Error: accepts 1 arg(s), received 2
Args 欄位負責檢查參數數量。在沒有設定 Args 時,預設接受任意數量的參數;如果命令內部直接存取 args[0],應明確設定驗證器,避免使用者未傳參數時發生 index out of range 錯誤。
Cobra 內建了幾種常用的參數數量驗證器:
| 驗證器 | 行為 |
|---|---|
cobra.ExactArgs(n) |
必須剛好有 n 個參數 |
cobra.MinimumNArgs(n) |
至少要有 n 個參數 |
cobra.MaximumNArgs(n) |
最多只能有 n 個參數 |
cobra.RangeArgs(min, max) |
參數數量必須介於指定範圍 |
cobra.NoArgs |
不允許傳入任何參數 |
cobra.ArbitraryArgs |
接受任意數量的參數 |
Use: "greet <name>" 是寫給使用者看的語法提示。字串裡的 greet 是命令名稱,後面的 <name> 則是提示使用者輸入內容的參數標籤,會在 --help 中顯示:
$ mytool greet --help
Usage:
mytool greet <name> [flags]
要注意的是,<name> 只是 Help 的說明文字,Cobra 本身並不會讀取 <name> 來驗證參數數量;真正的數量驗證仍然是由 Args 欄位處理。
RunE 檢查參數內容接著檢查參數內容(例如確認輸入的是否為數字),寫在 RunE 函式內自行檢查。驗證失敗時直接回傳 error,Cobra 就會自動把錯誤訊息印出並終止執行:
RunE: func(cmd *cobra.Command, args []string) error {
count, err := strconv.Atoi(args[1])
if err != nil {
return fmt.Errorf("第二個參數必須是數字,你輸入的是:%s", args[1])
}
// ...
},
此外,Cobra 傳給命令的參數型別固定是 []string。即使使用者輸入的是數字,程式收到的仍是字串,因此範例使用 strconv.Atoi 完成轉換。
重新編譯並執行:
go build -o mytool
./mytool greet Evan
Hello, Evan!
接著試一下錯誤的情況,當沒有傳入名字時,ExactArgs(1) 會在執行 RunE 前回報錯誤:
./mytool greet
Error: accepts 1 arg(s), received 0
當工具包含一組相關的操作時,可以將它們組織成兩層的巢狀結構。例如替 mytool 加入使用者功能時,以 user 作為父命令,底下包含 create(建立)與 list(列表):
mytool user create
mytool user list
在 cmd/user/ 新增 user.go,建立 user 父命令:
// cmd/user/user.go
package user
import "github.com/spf13/cobra"
var UserCmd = &cobra.Command{
Use: "user",
Short: "管理使用者",
// 不寫 Run:mytool user 直接印出 help,列出底下有哪些命令
}
UserCmd 大寫開頭——這個變數需要從 cmd/root.go 拿到,所以要 export。接著在 cmd/user/ 新增 create.go,建立接收使用者名稱的 create 子命令:
// cmd/user/create.go
package user
import (
"fmt"
"github.com/spf13/cobra"
)
var createCmd = &cobra.Command{
Use: "create <name>",
Short: "建立新使用者",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
fmt.Printf("建立使用者:%s\n", name)
return nil
},
}
create 現在只有一個 arg,一次只能建一個使用者。要一次建多個——mytool user create alice bob charlie——把 Args 換成 cobra.MinimumNArgs(1),RunE 裡改成迴圈跑過 args:
var createCmd = &cobra.Command{
Use: "create <name>...",
Short: "建立新使用者",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
for _, name := range args {
fmt.Printf("建立使用者:%s\n", name)
}
return nil
},
}
這種寫法只適用於同質性的批次輸入——每個 arg 都是同一種東西(使用者名稱),順序不重要,就像 rm a.txt b.txt c.txt 一次刪多個檔案,如果要傳的是「名字、角色、年齡」這種彼此不同質的值,不該塞進多個 arg,要改用具名的 Flag。
// cmd/user/list.go
package user
import (
"fmt"
"github.com/spf13/cobra"
)
var listCmd = &cobra.Command{
Use: "list",
Short: "列出所有使用者",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("alice\nbob\ncharlie")
return nil
},
}
有了 UserCmd、createCmd、listCmd,需要把它們串起來。每個資源的子命令,統一掛在那個資源的 user.go 的 init() 裡:
// cmd/user/user.go
package user
import "github.com/spf13/cobra"
var UserCmd = &cobra.Command{
Use: "user",
Short: "管理使用者",
}
func init() {
UserCmd.AddCommand(createCmd) // user 底下有什麼,看這裡
UserCmd.AddCommand(listCmd)
}
根命令也一樣——所有一層的子命令,集中在 root.go 的 init() 裡:
// cmd/root.go
func init() {
rootCmd.AddCommand(user.UserCmd)
}
完成後的目錄結構:
cmd/
├── root.go ← rootCmd + 掛 user
└── user/
├── user.go ← UserCmd + 掛 create, list
├── create.go ← createCmd(小寫,不 export)
└── list.go ← listCmd(小寫,不 export)
執行效果:
$ mytool user --help
管理使用者
Usage:
mytool user [command]
Available Commands:
create 建立新使用者
list 列出所有使用者
$ mytool user create alice
建立使用者:alice
$ mytool user create
Error: accepts 1 arg(s), received 0
如果在閱讀前面程式碼時對某些 Go 語法感到陌生,以下為本篇出現的核心機制說明:
{}Go 語言沒有傳統物件導向語言的 class 關鍵字與繼承機制,而是使用 struct(結構體)來組合與封裝資料欄位。
在 Go 中建立結構體實例時,Type{Field: Value} 稱為 Struct 字面值:
var greetCmd = &cobra.Command{
Use: "greet",
Short: "印出問候語",
}
class:cobra.Command 本身就是一個 struct 型別,不需要像其他語言一樣使用 new 類別實例化。"" 或 nil)。&:代表取址符號。這行程式碼會在記憶體建立 cobra.Command 結構體,並回傳其記憶體位址(指標 *cobra.Command),確保整棵命令樹共享同一份命令實例。Go 將函式視為First-class citizen,代表函式可以像字串或數字一樣存入變數,或是作為 struct 的欄位型別。
RunE 欄位的型別為 func(cmd *cobra.Command, args []string) error。宣告時直接賦予一個沒有名字的函式:
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("執行命令")
return nil
},
func(...) error:宣告函式的參數與回傳型別。{}:包住該匿名函式的實際執行邏輯(Function Body)。這個函式在建立 Command 時不會立即執行,而是交由 Cobra 在比對到命令時呼叫。range 迴圈RunE 接收到的 args 型別是 []string(字串切片)。Slice 是 Go 用來處理同型別動態長度序列的資料結構:
args[0] 讀取第一個位置參數。range 遍歷:需要處理多個參數時,搭配 range 迴圈依序取出:for _, name := range args {
fmt.Printf("建立使用者:%s\n", name)
}
range 每次迴圈會同時回傳 (索引, 數值)。因為 Go 規定宣告的變數必須使用,若不需要索引值,可以用 _(底線 / Blank Identifier)忽略。
Go 支援函式回傳多個數值,標準庫普遍採用「回傳結果 + error」的設計:
count, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("數量必須是數字:%s", args[0])
}
strconv.Atoi 會同時回傳轉換後的整數與 error:
err != nil:代表轉換失敗(例如傳入非數字字串),此時可封裝錯誤訊息並由 RunE 向上回傳。err == nil:代表轉換成功,可以安心使用 count 變數。下一篇:命令和 arg 都寫好了,接著把 Flag 的實作補完——縮寫、型別、local vs persistent → 用 Cobra 定義 Flag:縮寫、型別與作用範圍。