上一篇拆解了 Command、Arg 與 Flag 的角色與設計,這篇我們將開始動手建立第一個 CLI 專案。本系列示範使用 VS Code 作為 IDE、Go 為開發語言、Cobra 為 CLI 框架。
💡 範例程式碼
整個系列文章用到的範例程式碼皆收錄在 GitHub 儲存庫:evanchen76/cli-sample。本文對應的範例程式位於mytool/目錄,你可以直接從 GitHub 下載或 clone 到本機對照閱讀:
git clone https://github.com/evanchen76/cli-sample.git
這個系列選擇 Go 與 Cobra 示範 CLI。Go 能編譯成單一執行檔、支援跨平台編譯,也能用 Goroutine 處理併發工作;許多常見的開發工具也用 Go 實作 CLI,例如 Grafana CLI、Stripe CLI、Docker CLI、kubectl 與 GitHub CLI。Cobra 則負責解析 Command、arg 與 Flag,並根據命令定義產生 help 與 Shell 自動完成。你也可以使用熟悉的 Node.js、Python 或 Rust,搭配對應的 CLI 框架實作;雖然語法與工具不同,核心概念大致相同,不影響對這個系列的理解。
在終端機輸入以下命令來安裝 go
brew install go
安裝 Go 延伸套件:打開 VSCode → Extensions(Cmd+Shift+X)→ 搜尋 Go(Google 官方出的)→ Install。裝完會提示安裝 gopls、dlv 等工具,按 Install All 全裝。
建立專案目錄 mytool,接著 初始化 Go module(每個 Go 專案都要做一次,用來管理套件版本):
go mod init mytool
這會產生 go.mod
mytool/
├── go.mod
內容如下:
module mytool
go 1.22 ← go版本
接著新增檔案main.go。main.go 是整個命令列應用程式的進入點 。
mytool/
├── main.go
package main
import "mytool/cmd"
func main() {
cmd.Execute()
}
這段程式碼包含三個關鍵結構:
package main:宣告這個檔案屬於 main 套件。Go 編譯器看到 package main 時,會知道這個套件要編譯成獨立執行的執行檔,而不是提供給其他專案引用(import)的函式庫。import "mytool/cmd":匯入專案內部的 cmd 套件。這裡 mytool 是前面在 go.mod 定義的模組名稱,cmd 是存放 CLI 命令邏輯的目錄與套件名稱。func main():程式啟動時的進入點。裡面只有一行 cmd.Execute(),將所有的命令解析與執行邏輯全部交給 cmd 套件處理。這樣的分工能讓 main.go 保持簡潔,將程式進入點與實際命令邏輯解耦。接著定義根命令,新增 root.go:
mytool/
├── main.go
├── go.mod
└── cmd/
└── root.go ← 定義根命令 (mytool)
root.go 定義「根命令」—— 也就是 mytool 這個命令本身。Cobra 把所有命令組成一棵樹,根命令是樹根,之後每個子命令(mytool greet、mytool login…)都掛在它底下。當使用者只打 mytool、不帶任何子命令時,跑的就是這個根命令。
package cmd
import (
"github.com/spf13/cobra"
)
// rootCmd 是整棵命令樹的樹根,代表 mytool 這個命令本身。
var rootCmd = &cobra.Command{
Use: "mytool", // 命令名稱,也是使用者在終端機要打的字
Short: "My first CLI tool", // 一行簡短說明,會出現在 help 訊息最上面
}
// Execute 是對外的進入點,由 main.go 呼叫。
// rootCmd.Execute() 會開始解析使用者輸入的 argv,
// 判斷要跑根命令本身、還是某個子命令。
func Execute() {
rootCmd.Execute()
}
新增完我們就來執行看看。
go run main.go
結果會印出 Short 上給的內容。因為這個根命令並沒有執行任何動作。
My first CLI tool
除了用 go run來執行,也可以先打包好,再執行mytool。
go build -o mytool
./mytool
到這裡,mytool 的基礎框架與第一次建置已順利完成。如果這是你第一次閱讀 Go 程式碼,我們接著釐清剛才範例中出現的三個語法關鍵:
先講 package,就是 root.go 最上面這行:
package cmd
每個 .go 檔案最上面都要宣告自己屬於哪個 package,main.go 是 package main,這裡是 package cmd。同一個 package 裡的檔案可以直接互相呼叫,不用另外 import。
接著是大小寫,對照剛剛程式碼裡的這兩行:
var rootCmd = &cobra.Command{ ... }
func Execute() { ... }
Go 沒有 public、private 這種關鍵字,看的是名字第一個字母的大小寫:大寫開頭外面看得到(exported),小寫開頭只有自己這個 package 內部看得到(unexported)。rootCmd 小寫,只有 cmd package 自己用得到;Execute 大寫,所以 main.go 才能跨 package 呼叫 cmd.Execute()。
最後是指標(Pointer)。這是從 JavaScript、Python 或 Java 等語言轉過來的開發者容易困惑的地方,對照程式碼裡這行最前面的 &:
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "My first CLI tool",
}
在 Go 語言中,所有變數預設都是傳值(Pass-by-Value)。如果沒有加 &,當你把一個 struct 變數傳給其他函式或賦值給新變數時,Go 會在記憶體中複製出一份全新的資料副本。
這行程式碼做了兩件事:
cobra.Command{...}:在記憶體中建立一個 Command 結構體(struct)資料。& 取址符號:取得這個結構體在記憶體中的記憶體位址(Memory Address),把 rootCmd 變成一個指標變數(型別為 *cobra.Command)。在 CLI 專案中使用指標有兩個關鍵好處:
rootCmd.AddCommand(subCmd) 新增子命令,或是綁定 Flag 時,程式必須直接修改 rootCmd 本身的狀態。如果傳的是複製品,任何修改都只會作用在臨時副本上,原本的命令樹完全不受影響。cobra.Command 內部包含許多欄位與設定。傳遞指標只需要傳送一個小小的記憶體位址數字,不必每次複製整個結構體。簡而言之:在 Go 裡看到 &,代表「我拿的是這份資料在記憶體裡的實際位置,大家共用同一份」;沒加 & 則是「複製一份新副本給自己用」。
mytool 現在只有一個空殼的根命令,什麼事都還做不了。下一篇我們動手加第一個子命令 → [用 Cobra 建立子命令與 Args|建構 CLI 子命令與輸入參數]。