Golang:命令行框架cobra簡介


項目地址:spf13/cobra: A Commander for modern Go CLI interactions (github.com)

文檔地址:cobra/user_guide.md at master · spf13/cobra (github.com)

Overview

cobra是一個用於創建命令行工具的庫(框架),可以創建出類似git或者go一樣的工具,進行我們平時熟悉的git clone/pull、go get/install等操作。kubernetes工具中就使用了cobra。

上述這些命令行工具的--help/-h既好看又好理解,開始時我也會好奇這些工具是如何實現多級子命令支持的?cobra的目標即是提供快速構建出此類工具的能力(cobra是框架,具體的業務邏輯當然還是要自己寫)。例如可以為我們app或系統工具的bin文件添加--config來啟動服務、添加--version來查看版本號等等。相比起標准庫里的flag包,cobra強大了很多。

官網列出了cobra的一大堆優點:

  1. 易用的subcommand模式(即嵌套命令或子命令)

  2. 強大的flags支持(參考標准庫flagSet)

  3. 支持global、local、cascading級別的flag設置

  4. 錯誤時的智能提示

  5. 自動生成的、美觀的help信息,並默認支持--help和-h打印help信息

  6. 提供命令自動補全功能(bash, zsh, fish, powershell環境)

  7. 提供命令man page自動生成功能

  8. 有兄弟項目viper可以快捷的實現配置文件解析和flag綁定(暫不建議直接使用,viper包裝了一些配置文件解析的細節,應當在熟悉cobra之后再去單獨了解)

本文的核心目的為:通過手寫一個示例來迅速入門cobra。

先來簡單看下cobra框架中主要概念:

kubectl get pod|service [podName|serviceName] -n <namespace>

以上述kubectl get為例,cobra將kubectl稱作做rootcmd(即根命令),get稱做rootcmd的subcmd,pod|service則是get的subcmd,podName、serviceName是pod/service的args,-n/--namespace稱作flag。同時我們還觀察到-n這個flag其實寫在任意一個cmd之后都會正常生效,這說明這是一個global flag,global flag對於rootcmd及所有子命令生效。

此外,當為cmd指定了一個與subcmd同名的args時,subcmd優先生效。

一、使用cobra的建議的項目目錄結構

cobra文檔告訴我們,使用cobra時最好遵循如下的項目目錄設置(即:把命令行定義相關的部分單獨寫在一個名為cmd的包內):

  ▾ appName/
    ▾ cmd/
        add.go
        your.go
        commands.go
        here.go
      main.go

使用cobra的項目,其程序入口main.go一般極為簡潔(因為相應的業務邏輯都在cmd里調用或實現):

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute() 
}

二、寫一個簡單地mytool工具

本文示例不會去寫一個常駐的后台服務,只寫一個命令行工具mytool進行演示。

我們為mytool工具預設如下功能,朝着這個目標前進:

// 打印mytool的版本號,預設為1.0-beta
mytool version    		

// names目錄中創建name同名文件,將desc內容寫入文件中,成功后打印"${name} is added"
mytool add "Donald.Trump" --desc "He knows everything"  
// names目錄中刪除name同名文件,成功后打印"${name} is deleted"
mytool del "Donald.Trump" 		
// names目錄中讀取name同名文件中的信息,如未找到對應name那么顯示"${name} not found"
mytool get "Donald.Trump" 
// names目錄中讀取所有文件並打印相關信息
mytool get all

1. 首先,創建如下的項目結構:

然后使用go mod初始化包管理文件go.mod:go mod init mytool

2. 編寫cmd/root.go文件,正如其名這是所有命令行的root:

package cmd

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

var rootCmd = &cobra.Command{
	Use:   "mytool",
	Short: "mytool is a tool to record names", // 子命令的簡單說明,務必簡短,因為會出現在其上級命令的help結果中,過長會導致換行、不美觀,此外還會出現在自動補全時,過長容易影響視線
	Long: `The best tool to record names in the world! // 子命令的功用詳細說明,可以寫的比較詳細些
Just try it!!!
Complete documentation is available at http://mytool.com`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Use mytool -h or --help for help.")
	},
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

3. 然后創建main/main.go文件,並編譯后試試效果:

可以看到基礎的-h功能展示了出來。接下來我們為框架補充實體邏輯。

4. 先增加一個version功能試試

// cmd目錄下新增version.go文件,內容如下:
package cmd

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

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "show version of mytool",
	Long: `All tools have a version, this is mytool's"`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("mytool-1.0-beta")
	},
}

func initVersion()  {
	rootCmd.AddCommand(versionCmd)
}

我們定義了一個全新的cobra.Command:versionCmd,我們希望他成為mytool的一個subcmd,只要使用rootCmd.AddCommand(versionCmd)就可以實現了。這個操作可以在cmd包中任意地方寫,但統一起見我們在每個cmd文件中寫一個init函數來執行對應cmd的add操作和未來可能存在的flag綁定等操作,然后在root.go中增加initAll()函數來調用組織子命令的init函數(需要注意的是init()已經被cobra占用,所以我們自己的init函數加上各種后綴即可)。

package cmd

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

var rootCmd = &cobra.Command{
	Use:   "mytool",
	Short: "mytool is a tool to record names",
	Long: `The best tool to record names in the world!
Just try it!!!
Complete documentation is available at http://mytool.com`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Use mytool -h or --help for help.")
	},
}

func initAll()  {
	initVersion()
}

func Execute() {
	initAll()
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

可以看到關於version的help已經呈現了出來,也能正常查看mytool的version了,但實測還沒有子命令自動預測補全的功能,這個在下邊嘗試。

5. 增加add子命令

cmd/add.go,這個子命令使用指定的name args作為參數,使用--desc作為flag

package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
	"io/ioutil"
)

var (
	name string
	nameDesc string
)

var addNameCmd = &cobra.Command{
	Use:   "add",
	Short: "mytool name add operations",
	Long: `mytool add: add name info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 1 {
			fmt.Println(cmd.Help())
			return
		}
		name = args[0]
		err := ioutil.WriteFile(fmt.Sprintf("./names/%s",name), []byte(nameDesc), 0644)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("%s is added\n", name)
	},
}

func initAdd() {
	addNameCmd.Flags().StringVarP(&nameDesc, "desc", "D", "", "description of this person")
	rootCmd.AddCommand(addNameCmd)
}

然后在root.go initAll()中增加initAdd()的調用:

func initAll()  {
	initVersion()
	initAdd()
}

可以看到add子命令已經出現在help中了,且多了一個completion子命令,嘗試一下可以發現,這個子命令可以用於生成各個shell環境下的自動補全腳本,我這里就略過不做嘗試了。

6. 接下來照貓畫虎添加delete.go和get.go文件

package cmd

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

var delNameCmd = &cobra.Command{
	Use:   "del",
	Short: "mytool name del operations",
	Long: `mytool del: del name info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 1 {
			fmt.Println("Only one name can be deleted in one time!")
			return
		}
		name = args[0]
		err := os.Remove(fmt.Sprintf("./names/%s",name))
		if err != nil {
			fmt.Printf("%s not found\n", name)
			return
		}
		fmt.Printf("%s is deleted\n", name)
	},
}

func initDelete()  {
	rootCmd.AddCommand(delNameCmd)
}
package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
	"io/ioutil"
)

var getNameCmd = &cobra.Command{
	Use:   "get",
	Short: "mytool name get operations",
	Long: `mytool get: get name info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 1 {
			fmt.Println("Only one name can be get in one time! Or use get all to get all names!")
			return
		}
		name = args[0]
		nameInfo,err := ioutil.ReadFile(fmt.Sprintf("./names/%s",name))
		if err != nil {
			fmt.Printf("%s not found\n", name)
			return
		}
		fmt.Println(name,":",string(nameInfo))
	},
}

var getAllNamesCmd = &cobra.Command{
	Use:   "all",
	Short: "mytool name get all operations",
	Long: `mytool get all: get all names info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 0 {
			fmt.Println(getNameCmd.Help())
			return
		}
		files, err := ioutil.ReadDir("./names")
		if err != nil {
			fmt.Println("Dir names not found!")
			return
		}
		for _, file := range files {
			nameInfo,_ := ioutil.ReadFile(fmt.Sprintf("./names/%s", file.Name()))
			fmt.Println(file.Name(),":",string(nameInfo))
		}
	},
}

func initGet() {
	getNameCmd.AddCommand(getAllNamesCmd)
	rootCmd.AddCommand(getNameCmd)
}

至此所有cmd添加完畢,我們只需要在root.go中集成相關init函數即可。

func initAll()  {
	initVersion()
	initAdd()
	initDelete()
	initGet()
}

經測試各項功能正常。

三、思考

1. 我做了什么?

在本次筆記中,根據官方文檔編寫了一個mytool的指令,其實現了基本的subcommand添加、flag添加、args判斷,基於此初步了解了cobra的命令添加體系和組織邏輯。

2. 有哪些地方需要注意的 以及 我還能做些什么?

實際項目中的情況會比示例復雜的多,但只要清楚其脈絡就可以融會貫通。

  • 如何設置無需指定參數的flag:通過指定包含默認值的bool類型的flag來實現無參flag設置,例如如何將本例中的get all改為無參flag get --all
  • args的自動校驗:本例通過簡單的len(args)進行參數校驗,cobra提供了便捷的Command內置字段Args來支持args校驗,其值可以是cobra內置args校驗函數,如NoArgs,MinimumNArgs(int),MaximumNArgs(int),ExactArgs(int)等等,也可以自定義匿名函數進行args的提前校驗
  • 如何強制指定flag:rootCmd.MarkFlagRequired(<flagName>)、rootCmd.MarkPersistentFlagRequired(<flagName>)
  • cobra.Command的Run函數返回error:默認的Run函數不會返回error,所以實際上本例中的Execute()沒有遇到異常panic時都會返回nil。如果想要捕捉各個cmd的運行時的業務error,可以使用RunE參數來代替Run參數,RunE支持error返回
  • help和usage的自定義:一般不需要
  • preRun和PostRun鈎子(hooks):cobra提供了preRun和PostRun的hooks,參考webhooks理解即可,他們提供了在運行Run函數之前/之后執行其他更多操作的能力,這部分的使用很簡單,只需要為Command對象的幾個字段賦值相應的匿名函數即可。以本文為例,我們需要在執行任意命令組合之前都檢查names文件夾是否存在,如果不存在則創建之,這個邏輯就可以寫在rootcmd的preRun函數中,這樣我們就不必在每次用到names文件夾時都檢查其是否存在了。
  • 自動補全和man page生成功能測試:completion功能很簡單,而關於man page生成則可參考:cobra/README.md at master · spf13/cobra (github.com)

  • 多級子命令:通常我們最多寫3層子命令(包含rootCmd在內)就可以了,建議按命令繼承的層次來組織cmd內的目錄層次,這樣可以快速找到對應子命令的實現邏輯。

cobra作者spf13還有一個viper項目,此項目提供便捷的針對json,yaml,toml,ini,hcl等類型配置文件的解析,配合cobra使用更佳,當然僅使用cobra也很好,自己解析配置文件也很便捷。

自動補全相關:

app completion bash > /etc/bash_completion.d/app

上述指令可以快捷的生成針對app指令的bash補全配置,並直接放入/etc/bash_completion.d文件中,新會話登錄后即可實現自動補全。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM