像 Git,Go,Docker 等等的命令行腳本都有很多子命令,那麼我們用 Go 語言如何實現多個子命令的腳本應用呢?
例子和源碼
先來看一個不含子命令的 CLI 應用:
1package main
2
3import "flag"
4import "net/http"
5
6var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
7 w.Write([]byte("hello world"))
8})
9
10func main() {
11 // 綁定監聽地址參數
12 addr := flag.String("addr", ":8080", "addr")
13 // 解析參數
14 flag.Parse()
15
16 http.ListenAndServe(*addr, handler)
17}
1$ go run main.go --addr :23333
2$ curl http://localhost:23333
3hello world
現在我們來查看下 flag 包的源碼,可以找到以下代碼片段:
1var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
2
3func Parse() {
4 CommandLine.Parse(os.Args[1:])
5}
可以清晰地看到 flag 包中的 CommandLine 是一個頂層的 Flag Set,參數解析是從第二個參數開始。
分析
先從命令格式開始,多個子命令的格式一般為:
script <command> [arguments]
- 第二個參數是子命令名稱
- 第三個參數開始都是該子命令的參數,可選
在先前的例子,我們其實只需要為每個子命令創建一個新的 Flag Set,然後綁定參數並從第三個參數開始解析即可。
實現
將先前例子的 web 服務改成 serve 子命令,再新增一個 migrate 子命令用於數據遷移:
1package main
2
3import "flag"
4import "fmt"
5import "net/http"
6import "os"
7
8var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
9 w.Write([]byte("hello world"))
10})
11
12func main() {
13 // serve command
14 serveCmd := flag.NewFlagSet("serve", flag.ExitOnError)
15 serveAddr := serveCmd.String("addr", ":8080", "addr")
16
17 // migrate command
18 migrateCmd := flag.NewFlagSet("migrate", flag.ExitOnError)
19 migrateConfig := migrateCmd.String("config", "", "config")
20
21 // 第二個參數為子命令名稱
22 var cmd string
23 if len(os.Args) > 1 {
24 cmd = os.Args[1]
25 }
26
27 switch cmd {
28 case "serve":
29 // 從第三個參數開始解析
30 serveCmd.Parse(os.Args[2:])
31 http.ListenAndServe(*serveAddr, handler)
32 case "migrate":
33 migrateCmd.Parse(os.Args[2:])
34 fmt.Println("migrate config:" + *migrateConfig)
35 default:
36 fmt.Println("Usage: \n\tscript <command> [arguments]")
37 os.Exit(1)
38 }
39}
1$ go run main.go
2Usage:
3 script <command> [arguments]
4exit status 1
5
6$ go run main.go serve --addr :23333
7$ curl http://localhost:23333
8hello world
9
10$ go run main.go migrate --config config.yaml
11migrate config:config.yaml