上一篇把 Flag 定位成「對動作的修飾」,這篇接著 Cobra 裡實作縮寫及型別。延續 建構 CLI 子命令與輸入參數 裡的 mytool user create <name>。這裡,我們讓範例再多一個可以設定角色的功能:加入 --role 參數來指定新使用者的角色:
mytool user create alice --role admin
name 是操作對象,--role 是可省略的執行選項;省略時使用預設角色。
在 Cobra 裡幫 createCmd 加這個 Flag:
// cmd/user/create.go
var createCmd = &cobra.Command{
Use: "create <name>",
Short: "建立新使用者",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
role, _ := cmd.Flags().GetString("role")
fmt.Printf("建立使用者:%s(角色:%s)\n", name, role)
return nil
},
}
func init() {
createCmd.Flags().StringP("role", "r", "user", "使用者角色")
}
這裡 createCmd.Flags() 掛的是 local Flag,也就是只有 mytool user create 能用。
除了完整名稱 --role,也可以搭配縮寫 -r 使用,兩種寫法效果相同:
mytool user create alice --role admin
mytool user create alice -r admin
Cobra 不會自動擷取長名稱的第一個字母作為縮寫。若要支援縮寫,必須呼叫帶 P(Shorthand)的函式並明確傳入縮寫字元:
// 不帶縮寫:只支援 --role
createCmd.Flags().String("role", "user", "使用者角色")
// 帶縮寫:第二個參數傳入 "r",同時支援 --role 與 -r
createCmd.Flags().StringP("role", "r", "user", "使用者角色")
StringP 的第二個參數 "r" 就是縮寫,只能使用單一字元,慣例採用小寫。
所有型別都有對應帶 P 的版本,例如 BoolP、IntP、StringArrayP 等。
命名縮寫時盡量與業界常見慣例一致,降低使用者的記憶成本:
| 縮寫 | 常見對應 |
|---|---|
-v |
--verbose |
-o |
--output |
-f |
--file |
-n |
--name 或 --dry-run |
-p |
--port |
-c |
--config |
-h |
--help(Cobra 自動保留) |
另外要注意的是縮寫在同一個 Command 下必須唯一,重複定義會 panic:
// 這樣會 panic:-o 被定義了兩次
flags.StringP("output", "o", "", "output file")
flags.StringP("origin", "o", "", "origin url") // ❌
如果不需要縮寫的 Flag 直接用不帶 P 的版本就好,不一定每個 Flag 都要有縮寫。
Cobra 會依照定義 Flag 時選用的型別解析命令列文字。例如 StringP 保留字串,IntP 只接受整數,BoolP 則把 Flag 是否出現解析成布林值。讀取時使用對應的 GetString、GetInt 或 GetBool。
flags.StringP("config", "c", "", "config file")
flags.IntP("port", "p", 8080, "port number")
flags.BoolP("verbose", "v", false, "verbose mode")
flags.Float64P("ratio", "r", 1.0, "ratio value")
flags.StringArrayP("tag", "t", nil, "標籤(可重複)")
Bool Flag 通常用來控制功能是否開啟。預設值是 false 時,命令列出現 --verbose 就會變成 true,不用在後面再寫 true:
./mytool # verbose = false
./mytool --verbose # verbose = true
StringArrayP 定義的 Flag 可以在同一條命令中出現多次。每次出現都加入一個值,適合接收標籤、檔案名稱等數量不固定的資料:
./mytool --tag=a --tag=b --tag=c
呼叫 cmd.Flags().GetStringArray("tag") 會取得 []string{"a", "b", "c"}。
Cobra 的 Flag 依據作用範圍分為兩種:
cmd.Flags()):只對定義它的該層命令生效。cmd.PersistentFlags()):對該層命令以及底下所有的子命令都生效。func init() {
// 所有子命令都能用 --verbose
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "enable verbose output")
// 只有 rootCmd 本身能用 --output
rootCmd.Flags().StringP("output", "o", "", "output file")
}
mytool --verbose # ✅
mytool serve --verbose # ✅ 子命令也能用
mytool serve --output # ❌ 只有 root 能用
常見放在 rootCmd.PersistentFlags() 的 Flag:--config、--log-level、--verbose、--dry-run。
Count Flag 是重複使用同一個 Flag 來累加次數,幾乎只用在 -v / -vvv:
./mytool -v # verbose level 1
./mytool -vv # verbose level 2
./mytool -vvv # verbose level 3
var verbosity int
func init() {
rootCmd.Flags().CountVarP(&verbosity, "verbose", "v", "增加 verbose 等級")
}
user create 如果要接收的不是單一名稱,而是一整個 JSON({"name":"alice","role":"admin"}),直接把 JSON 字串塞進 arg 或 Flag 的值都不好用——兩種都要處理 Shell 的引號跳脫,JSON 一長就難維護、容易打錯:
mytool user create '{"name":"alice","role":"admin"}'
mytool user create --data '{"name":"alice","role":"admin"}'
慣例做法是用 Flag 帶檔案路徑,命令自己讀檔案內容再解析:
var createCmd = &cobra.Command{
Use: "create",
Short: "建立新使用者",
RunE: func(cmd *cobra.Command, args []string) error {
path, _ := cmd.Flags().GetString("file")
data, err := os.ReadFile(path)
if err != nil {
return err
}
var u User
if err := json.Unmarshal(data, &u); err != nil {
return fmt.Errorf("解析 %s 失敗:%w", path, err)
}
fmt.Printf("建立使用者:%+v\n", u)
return nil
},
}
func init() {
createCmd.Flags().StringP("file", "f", "", "使用者資料的 JSON 檔案路徑")
createCmd.MarkFlagRequired("file")
}
-f 的值是 "-" 時,程式改讀 os.Stdin;其他值則當成檔案路徑:
var r io.Reader
if path == "-" {
// -f -:從 stdin 讀取
r = os.Stdin
} else {
// -f user.json:開啟指定檔案
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
r = f
}
data, err := io.ReadAll(r)
r 統一表示資料來源,因此 io.ReadAll 不需要區分資料來自 stdin 或檔案。
使用 -f - 時,管道左側命令的輸出會成為 mytool 的 stdin:
# 把本機檔案內容送進 stdin
cat user.json | mytool user create -f -
# 把 HTTP 回應內容送進 stdin
curl https://api.example.com/users/1 | mytool user create -f -
如果對本篇範例中出現的 Go 語法不熟悉,以下為相關特性的補充說明:
Go 規定所有宣告的變數都必須被使用,否則編譯不會通過。若呼叫的函式回傳多個值(例如 (string, error)),但特定情境下確定不需要處理該錯誤時,可以使用 _ 忽略:
role, _ := cmd.Flags().GetString("role")
這裡的 GetString 會回傳解析到的字串與 error。因為 role Flag 已經在 init() 中註冊過,呼叫時必定能找到該 Flag,因此使用 _ 忽略 error 變數。
init() 是 Go 語言保留的特殊函式。每個 package 內可以有多個 init() 函式,它們會在程式啟動、main() 執行之前自動被呼叫:
func init() {
createCmd.Flags().StringP("role", "r", "user", "使用者角色")
}
Cobra 利用這個特性,讓各個命令檔(如 create.go)在載入時自動將 Flag 掛載至對應的 Command 上,不需要在 main() 裡手動撰寫長長的初始化流程。
像 CountVarP 這種帶有 Var 字尾的 Cobra 函式,需要傳入變數的記憶體位址(指標 *int),而不是傳入數值本身:
var verbosity int
func init() {
rootCmd.Flags().CountVarP(&verbosity, "verbose", "v", "增加 verbose 等級")
}
在 verbosity 前加上取址符號 &,代表傳入該變數的記憶體位址。這樣當 Cobra 解析命令列參數時,可以直接寫入修改外部宣告的 verbosity 變數。
Go 的 io.Reader 是一個介面(interface),定義了標準的讀取行為 Read(p []byte) (n int, err error)。只要實作了這個方法的型別,都可以指派給 io.Reader 變數:
var r io.Reader
if path == "-" {
r = os.Stdin
} else {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
r = f
}
data, err := io.ReadAll(r)
os.Stdin 與 os.Open 回傳的 *os.File 都實作了 io.Reader 介面。因此 r 可以統一代表資料來源,讓後續的 io.ReadAll(r) 不必關心資料是來自標準輸入(stdin)還是本機檔案。
下一篇:mytool 現在能吃 Flag(輸入)、用 fmt.Println 印結果(輸出)。這組輸入輸出不是只有你的程式懂——Shell 也看得懂,而且能拿去跟別的命令串接。下一篇來看這件事的運作方式:管道與重導向 (Pipeline & Redirection)。