golang常用庫:cli命令列/應用程式生成工具-cobra使用

九卷發表於2021-10-30

golang常用庫:cli命令列/應用程式生成工具-cobra使用

一、Cobra 介紹

我前面有一篇文章介紹了配置檔案解析庫 Viper 的使用,這篇介紹 Cobra 的使用,你猜的沒錯,這 2 個庫都是同一個作者 spf13,他開發了很多與 golang 相關的庫,他目前在 google 領導著 golang 產品相關開發工作。

Cobra 是關於 golang 的一個命令列解析庫,用它能夠快速建立功能強大的 cli 應用程式和命令列工具。

它被很多知名的專案使用,比如 KubernetesGithub CLIEtcd 等。更多應用此庫的專案列表

我們平常用到命令:git commit -m "message",docker containter start 等都可以用 cobra 來實現。

Cobra 相關文件地址:

Cobra 的logo:

image-20211029123013983

(from:https://github.com/spf13/cobra)

二、功能特性介紹

  • 很多子命令的CLIS: 比如 app server、app fetch 等
  • 支援巢狀子命令(sub-command)
  • 輕鬆完成應用程式和命令的建立:cobra init appname 和 cobra add cmdname
  • 為應用程式生成 man 手冊
  • 全域性、本地和級聯 flag
  • 為 shell 程式完成自動提示(bash,zsh,fish, powershell etc.)
  • 支援命令列別名,可以幫助你更容易更改內容而不破壞他們
  • 靈活定義自己的help、usage資訊
  • 可選整合 viper 配置管理工具庫

更多功能特性請檢視: cobra文件介紹

三、Cobra cli 命令結構說明

Cobra 命令結構由3部分組成:

commands、arguments 和 flags

  • commands:

    命令列,代表行為動作,要執行的一個動作。每個命令還可以包含子命令。分為:rootCmd 和 subCmd。程式中具體物件是 cobra.Command{},這個是根命令;子命令(subCmd)用 rootCmd.AddCommand() 新增,子命令通常也會單獨存一個檔案,

    並通過一個全域性變數讓 rootCmd 可以 add 它。

  • arguments:

    命令列引數,通常是 []string 表示

  • flags:

    命令列選項。對 command 進一步的控制。通常用一短橫 - 或者兩短橫 -- 標識。程式中讀取儲存在變數中。

cobra 命令列格式:

APPNAME VERB NOUN --ADJECTIVE

APPNEM COMMAND ARG --FLAG

例子說明:

hugo server --port=1313 #server 代表 command, port 代表 flag。

git clone URL --bare   #clone 代表 command,URL 代表操作的物-argument,bare 代表 flag。

四、Cobra 基本使用方法

golang v1.15, cobra v1.2.1

安裝 cobra:

go get -u github.com/spf13/cobra

可以用 cobra -h 來檢視 cobra 命令的一些用法。

Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  completion  generate the autocompletion script for the specified shell
  help        Help about any command
  init        Initialize a Cobra Application

init 命令初始化應用程式

安裝 cobra generator:go get -u github.com/spf13/cobra/cobra

使用命令 cobra init 來建立第一個應用程式,這個命令也是初始化一個應用程式的專案框架:

cobra init firstappname
Error: required flag(s) "pkg-name" not set

報錯了,錯誤資訊截圖如下:

錯誤資訊:需要設定 --pkg-name 引數。

因為我們專案不存在。先建立名為 firstappname 資料夾,然後進入目錄 firstappname,在命令列下執行:cobra init --pkg-name firstappname

自動生成了如下目錄和程式:

image-20211029183608059

下面程式我去掉了英文註釋部分。

main.go

package main

import "firstappname/cmd"

func main() {
	cmd.Execute()
}

cmd/root.go

package cmd

import (
	"fmt"
	"os"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var cfgFile string

// 構建根 command 命令。前面我們介紹它還可以有子命令,這個command裡沒有構建子命令
var rootCmd = &cobra.Command{
	Use:   "firstappname",
	Short: "A brief description of your application",
	Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`
}

// 執行 rootCmd 命令並檢測錯誤
func Execute() {
	cobra.CheckErr(rootCmd.Execute())
}

func init() {
    // 載入執行初始化配置
	cobra.OnInitialize(initConfig)
    // rootCmd,命令列下讀取配置檔案,持久化的 flag,全域性的配置檔案
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.firstappname.yaml)")
	// local flag,本地化的配置
	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

// 初始化配置的一些設定
func initConfig() {
	if cfgFile != "" {
		viper.SetConfigFile(cfgFile) // viper 設定配置檔案
	} else {// 上面沒有指定配置檔案,下面就讀取 home 下的 .firstappname.yaml檔案
        // 配置檔案引數設定
		home, err := os.UserHomeDir()
		cobra.CheckErr(err)

		viper.AddConfigPath(home)
		viper.SetConfigType("yaml")
		viper.SetConfigName(".firstappname")
	}

	viper.AutomaticEnv() 

	if err := viper.ReadInConfig(); err == nil {// 讀取配置檔案
		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
	}
}

其實上面的錯誤在 cobra generator 文件裡有提示了,所以要多看官方文件。

這個 root.go 裡的 cobra.Command 就是設定命令列格式的地方。如果要執行相關的命令,可以在 Long:...下面加一行:

Run: func(cmd *cobra.Command, args []string) { },

執行程式:go run main.go , 報錯了:

image-20211029144042561

我用的是 go v1.15,GO111MODULE="on"。

用 go mod 來建立 module,進入firstappname目錄,命令: go mod init firstappname ,生成一個 go.mod,

module firstappname

go 1.15

require (
	github.com/spf13/cobra v1.2.1
	github.com/spf13/viper v1.9.0
)

在執行 go run main.go,第一次執行會下載檔案到 go.mod, go.sum 裡。再次執行,就會出現 rootCmd 下的 Long 資訊。

A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

可以看出,用 cobra init 命令初始化的專案, 生成了一個初始化的應用框架,但是沒有任何邏輯功能。僅僅輸出一些描述性資訊。

這個程式裡,最重要的是 cmd/root.go 裡的 rootCmd = &cobra.Command{} 這行程式,這裡定義命令動作。

程式裡的 init() 和 initConfig() 函式都是對命令列的配置。

為 rootCmd 新增功能:

var rootCmd = &cobra.Command{
	Use:   "firstappname",
	Short: "A brief description of your application",
	Long: `(root)A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,

	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("root called")
	},
}

測試執行 go run main.go ,輸出:

$ go run main.go
root called

執行:go run main.go --help

會輸出上面的 Long 資訊和完整的幫助資訊。

也可以把上面命令編譯:go build -o demo.exe,在執行

完整生成一個cobra應用框架的命令:

$ mkdir firstappname
$ cd firstappname

$ cobra init --pkg-name firstappname
$ go mod init firstappname

add 生成子命令subCmd

上面我們用 cobra init 建立了應用程式框架,在程式 cmd/root.go 裡有一個根命令 rootCmd,也就是說 init 命令建立了一個根命令。執行 command 命令是 &cobra.Command{} 裡的 Run 方法。

cobra add 來為 rootCmd 建立一個子命令。這個子命令通常在一個單獨的檔案裡。

  1. 用 add 命令生成子命令程式碼:
// cd 進入firstappname
$ cd ./firstappname
$ cobra add demo
demo created at D:\work\mygo\common_pkg\cobra\firstappname

在 cmd 目錄下生成了 demo.go 檔案:

image-20211029194029566

  1. 為子命令新增簡單功能

    add 命令已經為我們生成了一個簡單的應用程式碼,程式檔案通常存放在cmd目錄下,demo.go 程式:

    package cmd
    
    import (
    	"fmt"
    
    	"github.com/spf13/cobra"
    )
    
    // demoCmd represents the demo command
    // 子命令
    var demoCmd = &cobra.Command{
    	Use:   "demo",
    	Short: "A brief description of your command",
    	Long: `A longer description that spans multiple lines and likely contains examples
    and usage of using your command. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.`,
    	Run: func(cmd *cobra.Command, args []string) {
    		fmt.Println("demo called")
            fmt.Println("cmd demo")
    	},
    }
    
    func init() {
    	rootCmd.AddCommand(demoCmd)
    }
    

到現在為止,為 firstappdemo 新增了 2 個 Command 了,分別是根命令 rootCmd 和子命令 demoCmd。

子命令和根命令的關係一般通過程式 rootCmd.AddCommand() 方法確定。在程式 demo.go 裡可以看到它在 init() 函式裡。

Run 方法裡新增程式:fmt.Println("cmd demo")。一般這裡的程式都是其他 package 裡完成了具體邏輯,然後 Run 方法裡在呼叫這些程式。

測試執行:go run main.go demo

輸出:

demo called

cmd demo

也可以編譯專案 go build -o xxx 在執行。

Flags使用-給Command新增flags

flag 命令列選項,也叫標識,對command命令列為的進一步指示操作。

用這個標識可以給 command 新增一些可選項。

根據 flag 選項作用範圍不同,可以分為 2 類:

  • Persistent Flags,持久化的flag,全域性範圍

    如果設定全域性範圍的flag,可以用這個來設定。它既可以給根命令設定選項值,也可以給子命令設定選項值。

    下面例子裡的 rootCmd 和 demoCmd 都可以呼叫 flag。

  • Local Flags,區域性flag,只對指定的command生效。比如某個子命令的 flag。

因為 flag 標識是在命令列後面不同位置使用,所以我們要在方法外定義一個變數,來分配儲存使用這個識別符號。下面例子會說明。

Persistent Flags 全域性flag例子

  1. 在 cmd/root.go 檔案中新增一個變數 name

    var cfgFile string
    // 新增 name
    var name string
    

    然後在 root.go:init() 函式中新增全域性 persistent flag,把 flag 值儲存到變數 name 中

    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.firstappname.yaml)")
    // 新增全域性 flag
    rootCmd.PersistentFlags().StringVar(&name, "name", "", "Set one name")
    
  2. 在檔案 cmd/demo.go 中的 demoCmd(子命令) 裡 Run 方法輸出 name 值

    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("demo called")
        fmt.Println("cmd demo")
        // 列印輸出 name
    	fmt.Println("print persistent flag name: ", name)
    },
    
  3. 測試執行程式

    $ go run main.go demo --name setname
    demo called
    cmd demo
    print persistent flag name:  setname
    

    當然也可以先編譯 go build -o cobrademo.exe(我用的win),然後在執行測試程式

    $.\cobrademo.exe demo --name setname1
    demo called
    cmd demo
    print persistent flag name:  setname1
    

Persistent flag 的讀取方法:

// arg1:儲存變數,
// arg2:設定長flag名,這裡 name 顯示 --name,
// arg3:設定短flag名,這裡 n 顯示 -n,一般與上面對應
// arg4:預設值, 這裡設定為 ""
// arg5:flag的一些說明資訊
PersistentFlags().StringVarP(&name, "name", "n", "", "Set one name")

// 與上面用法基本相同,只是沒有短 flag 設定
PersistentFlags().StringVar(&name, "name", "", "Set one name")

// 直接設定flag名,arg1:flag 名,arg2:預設值,arg3:說明
PersistentFlags().String("foo", "", "A help for foo")

Local Flags例子

一個 flag 賦值給本地變數,只能對指定的command生效。

我們在 demo.go 中測試 local flag。

  1. 在 cmd/demo.go 檔案中定義變數 dsn,儲存這個 flag 值
// 定義 local flag
var dsn string
  1. 在 demo.go 中的 init() 中新增下面程式碼,把值儲存到 dsn上

    demoCmd.Flags().StringVarP(&dsn, "dsn", "d", "", "dsn file")
    
  2. 在 demoCmd.Command{} 獲取該值

    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("demo called")
        fmt.Println("cmd demo")
        // 列印輸出 name
        fmt.Println("print persistent flag name: ", name)
        // 列印輸出local flag: dsn
        fmt.Println("(local flag)print dsn: ", dsn)
    },
    
  3. 測試執行

    $ go run .\main.go demo --dsn setdsn1
    demo called
    cmd demo
    print persistent flag name:
    (local flag)print dsn:  setdsn1
    

    輸出了 setdsn1。

測試下其它子命令可以不可以獲取這個 dsn,新增一個新的子命令 demo2,

$ cobra add demo2

在目錄 cmd 下新增了檔案 demo2.go, 在 Run 下新增:

Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("demo2 called")
    // 新增輸出 dsn
    fmt.Println("test get local flag(dsn): ", dsn)
},

測試:

$ go run .\main.go demo2 --dsn testdsn
Error: unknown flag: --dsn

報錯了,程式終止執行了。

說明:local flag 區域性選項,只能作用於指定的 command。本例子中作用於 demoCmd,而不能作用於 demo2Cmd。

local flag 的讀取方法:

// arg1:儲存變數,
// arg2:設定長flag名,這裡 name 顯示 --name,
// arg3:設定短flag名,這裡 n 顯示 -n,一般與上面對應
// arg4:預設值, 這裡設定為 ""
// arg5:flag的一些說明資訊
// 方法(1)
Flags().StringVarP(&name, "name", "n", "", "Set one name")

// 與上面方法(1)用法基本相同,只是沒有短 flag 設定
Flags().StringVar(&name, "name", "", "Set one name")

// 直接設定flag名,arg1:flag 名,arg2:預設值,arg3:說明
Flags().String("foo", "", "A help for foo")

// 與上面方法(1)用法基本相同,除了第一個沒有變數讀取引數
Flags().StringP("toggle", "t", false, "Help message for toggle")

完整例子在 github 上,golang-library-learning/cobra

設定flag必填項

比如給 demo.go 的 dsn 這個 flag 設定必選項

demoCmd.Flags().StringVarP(&dsn, "dsn", "d", "", "dsn file")
// 把 dsn 設定為必選項
demoCmd.MarkFlagRequired("dsn")

flag 不設定dsn,執行程式:go run main.go demo, 報錯:Error: required flag(s) "dsn" not set

$ go run .\main.go demo
Error: required flag(s) "dsn" not set
Usage:
  firstappname demo [flags]

Flags:
  -d, --dsn string   dsn file
  -h, --help         help for demo

Global Flags:
      --config string   config file (default is $HOME/.firstappname.yaml)
      --name string     Set one name

Error: required flag(s) "dsn" not set
exit status 1

加上 dsn 執行,go run main.go demo --dsn setdsn,正常輸出:

$ go run main.go demo --dsn setdsn
demo called
cmd demo
print persistent flag name:
(local flag)print dsn:  setdsn

繫結配置

還可以繫結配置到 flags 上,用 viper

在 cmd/root.go 裡,有一個 initConfig() 方法,這個就是初始化配置方法。載入執行是在 init() 方法裡,

func init() {
	cobra.OnInitialize(initConfig)
    
    ... ...
}  

我們可以在 init() 方法中新增繫結 flag 程式,

rootCmd.PersistentFlags().StringVar(&name, "name", "", "Set one name")
viper.BindPFlag("name", rootCmd.PersistentFlags().Lookup("name"))

這樣就將 viper 配置和 flag 繫結,如果使用者不設定 --name,將從配置中查詢。

更多方法請檢視 viper flag doc

arguments 命令列引數設定

可以用Command 的 Args 欄位指定引數效驗規則。

Cobra 也內建了一些規則:

  • NoArgs:如果有任何命令列args引數,將會報錯

  • ArbitraryArgs:該命令接受任何引數

  • OnlyValidArgs:如果該命令引數不在 Command 的 ValidArgs 中,將會報錯

  • MinimumArgs(int): 如果命令引數數目少於N個,將會報錯

  • MaximumArgs(int): 如果命令引數數目多於N個,將會報錯

  • ExactArgs(int): 如果命令引數數目不是N個,將會報錯

  • RangeArgs(min, max):如果命令引數數目範圍不在(min, max),將會報錯

內建效驗規則的例子:

var rootCmd = &cobra.Command{
   Use:   "dmeo",
   Short: "demo short",
   Long:  `let's do it, demo!`,
   Args: cobra.MinimumNArgs(5),
   Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("hello chenqionghe")
   },
}

自定義驗證規則的例子:

var cmd = &cobra.Command {
    Short: "demo",
    Args: func(cmd *cobra.Command, args[] string) error {
        if len(args) > 0 {
            return errors.New("requires a color argument")
        }
        if myapp.IsValidColor(args[0]) {
          return nil
        }
        return fmt.Errorf("invalid color specified: %s", args[0])
    },
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello, demo!")
    },
    
}

鉤子函式 PreRun and PostRun Hooks

可以在執行命令之前或之後執行鉤子函式。如果子命令未宣告自己的 Persistent * Run 函式,則子命令將繼承父命令的鉤子函式。

函式的執行順序為:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun
package main

import (
	"fmt"

	"github.com/spf13/cobra"
)

func main() {
	var rootCmd = &cobra.Command{
		Use:   "root [sub]",
		Short: "My root command",
		PersistentPreRun: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
		},
		PreRun: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
		},
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside rootCmd Run with args: %v\n", args)
		},
		PostRun: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
		},
		PersistentPostRun: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
		},
	}

	subCmd := &cobra.Command{
		Use:   "sub [no options!]",
		Short: "My subcommand",
		PreRun: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside subCmd PreRun with args: %v\n", args)
		},
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside subCmd Run with args: %v\n", args)
		},
		PostRun: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside subCmd PostRun with args: %v\n", args)
		},
		PersistentPostRun: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args)
		},
	}

	rootCmd.AddCommand(subCmd)

	rootCmd.SetArgs([]string{""})
	rootCmd.Execute()
	fmt.Println()
	rootCmd.SetArgs([]string{"sub", "arg1", "arg2"})
	rootCmd.Execute()
}

執行程式:

$ go run .\hookdemo.go
Inside rootCmd PersistentPreRun with args: []
Inside rootCmd PreRun with args: []
Inside rootCmd Run with args: []
Inside rootCmd PostRun with args: []
Inside rootCmd PersistentPostRun with args: []

Inside rootCmd PersistentPreRun with args: [arg1 arg2]  // 子命令繼承了父命令的函式
Inside subCmd PreRun with args: [arg1 arg2]
Inside subCmd Run with args: [arg1 arg2]
Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun with args: [arg1 arg2]

為你的命令生成文件

Cobra 可以基於子命令、標誌等生成文件。具體的使用方法和生產格式文件請點選下面連結:

你可以設定 cmd.DisableAutoGenTag = true 從而把文件中 "Auto generated by spf13/cobra..." 等字樣刪掉。

help 命令

命令: cobra help,可以清楚顯示出對使用 cobra 有用的資訊,比如命令提示

你還可以定義自己的 help 命令或模板

cmd.SetHelpCommand(cmd *Command)
cmd.setHelpCommand(f func(*Command, []string))
cmd.setHelpTemplate(s string)

五、參考

cobra github

cobra user guide

Cobra Generator

相關文章