手把手教你開發docker一樣的命令行

前言

Cobra是一個強大的用來構建命令行程序的庫,許多流行的Go項目都是用它來構建的,比如Kubernetes、Docker、etcd、Istio、Github CLI等等。

接下來,演示開發一個我們自己的命令行程序chenqionghe,模仿一下docker命令行,預期功能如下

# 查看幫助  chenqiong -h  # 查看版本,類似docker version  chenqionghe version  # 查看hello命令幫助,類似docker ps -h  chenqionghe hello -h  # 使用hello命令,類似docker run --name app --volume /app/data  chenqionghe hello --name light-weight-baby --author gym  

Cobra基於三個基本概念

  • commands(行為)
  • arguments(位置參數)
  • flags(命令行選項)

使用基本模式是APPNAME VERB NOUN –ADJECTIVE或APPNAME COMMAND ARG –FLAG,例如

# server是一個command,--port=1313是一個命令行選項  hugo server --port=1313  #  clone 是 commands,URL 是 arguments,brae是命令行選項  git clone URL --bare  

一、安裝

go get -u github.com/spf13/cobra/cobra  go install github.com/spf13/cobra/cobra  

二、初始化應用

初始化項目

這裡我的應用名叫chenqionghe

go mod init chenqionghe  

創建入口文件cmd/root.go

創建文件夾cmd,並創建文件cmd/root.go,這是用來放所有的命令的基本文件

package cmd  import (  	"fmt"  	"github.com/spf13/cobra"  	"os"  )  var rootCmd = &cobra.Command{  	Use:   "chenqionghe",  	Short: "getting muscle is not easy",  	Long: `let's do it, yeah buddy light weight baby!`,  	Run: func(cmd *cobra.Command, args []string) {  		fmt.Println("hello chenqionghe")  	},  }  func Execute() {  	if err := rootCmd.Execute(); err != nil {  		fmt.Println(err)  		os.Exit(1)  	}  }  

創建主程序main.go

package main    import "chenqionghe/cmd"    func main() {  	cmd.Execute()  }  

運行一下main.go可以看到生效了

三、如何自定義命令

創建hello子命令

cobra add hello  

會在cmd下生成一個hello.cmd的命令,生成的命令是長下面這樣的,核心是調用了AddCommand方法

我們把沒用的信息幹掉,精簡後如下

package cmd    import (  	"fmt"  	"github.com/spf13/cobra"  )  var helloCmd = &cobra.Command{  	Use:   "hello",  	Short: "hello命令簡介",  	Long:  `hello命令詳細介紹`,  	Run: func(cmd *cobra.Command, args []string) {  		fmt.Println("hello called")  	},  	TraverseChildren: true,  }    func init() {  	rootCmd.AddCommand(helloCmd)  }  

直接運行看看

go run main.go hello  

創建version子命令

同理,我們再創建一個version命令

cobra add version  

修改一下Run方法

Run: func(cmd *cobra.Command, args []string) {     fmt.Println("chenqionghe version v0.0.1")  },  

運行如下

四、如何設置flag選項

flag選項按作用範圍分為persistent和local兩類

全局選項

persistent是全局選項,對應的方法為PersistentFlags,可以分配給命令和命令下的所有子命令,上面的rootCmd和helloCmd都是可以調用flag
例如,添加一個-v選項

func init() {  	var Verbose bool  	rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "全局版本")  }  

運行,可以看到生效了

本地選項

local為本地選項,對應方法為Flags,只對指定的Command生效,我們往hello命令的init裡邊添加一個本地flag

func init() {  	rootCmd.AddCommand(helloCmd)  	//本地flag  	var Source string  	helloCmd.Flags().StringVarP(&Source, "source", "s", "", "讀取文件路徑")  }  

運行如下

設置必填

我們在init函數添加以下代碼

rootCmd.Flags().StringVarP(&Name, "name", "n", "", "你的名字")  rootCmd.MarkFlagRequired("name")  

運行如下,必須填寫name參數才可以運行

綁定配置

添加一個initConfig方法

func initConfig() {  	viper.AddConfigPath("./")  	viper.AddConfigPath("./conf")  	viper.SetConfigName("config")  	viper.SetConfigType("yaml")  	viper.AutomaticEnv()  	if err := viper.ReadInConfig(); err != nil {  		fmt.Println("Error:", err)  		os.Exit(1)  	}  }  

在init中調用

cobra.OnInitialize(initConfig) //這會運行每個子命令之前運行  rootCmd.PersistentFlags().StringVar(&Author, "author", "defaultAuthor", "作者名")  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))  

這將把viper配置和flag綁定,如果用戶不設置-author選項,將從配置中查找

五、如何設置arguments

cobra默認提供了一些驗證方法

  • NoArgs: 如果包含任何位置參數,命令報錯
  • ArbitraryArgs: 命令接受任何參數
  • OnlyValidArgs: 如果有位置參數不在ValidArgs中,命令報錯
  • MinimumArgs(init): 如果參數數目少於N個後,命令行報錯
  • MaximumArgs(init): 如果參數數目多餘N個後,命令行報錯
  • ExactArgs(init): 如果參數數目不是N個話,命令行報錯
  • RangeArgs(min, max): 如果參數數目不在範圍(min, max)中,命令行報錯

使用示例

往Command中添加參數Args,我們規定參數不能少於5個,如下

var rootCmd = &cobra.Command{     Use:   "chenqionghe",     Short: "getting muscle is not easy",     Long:  `let's do it, yeah buddy light weight baby!`,     Run: func(cmd *cobra.Command, args []string) {        fmt.Println("hello chenqionghe")     },     Args: cobra.MinimumNArgs(5),  }  

運行輸出

六、如何使用參數

我們可以看到核心的方法,其實就是cobra.Command中的Run參數,指定了func(cmd *cobra.Command, args []string)類型的回調
代表我們可以直接使用cmd和args來編寫我們的程序

獲取flag參數

我們可以直接使用cmd的flag方法獲取傳遞的flag

var helloCmd = &cobra.Command{  	Use:   "hello",  	Short: "hello命令簡介",  	Long:  `hello命令詳細介紹`,  	Run: func(cmd *cobra.Command, args []string) {  		fmt.Println(cmd.Flag("author").Value)  		fmt.Println(cmd.Flag("name").Value)  	},  }  

運行如下

獲取args參數

var helloCmd = &cobra.Command{  	Use:   "hello",  	Short: "hello命令簡介",  	Long:  `hello命令詳細介紹`,  	Run: func(cmd *cobra.Command, args []string) {  		fmt.Println(args)  	},  	TraverseChildren: true,  }  

調用如下,可以看到已經取出了所有的args參數

七、如何設置鉤子

cobra提供了很多鉤子方法,可按運行順序排列如下

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun
    使用示例
var helloCmd = &cobra.Command{  	Use:   "hello",  	Short: "hello命令簡介",  	Long:  `hello命令詳細介紹`,  	//Args: cobra.MinimumNArgs(1),  	PersistentPreRun: func(cmd *cobra.Command, args []string) {  		fmt.Printf("Inside rootCmd PersistentPreRun with args: %vn", args)  	},  	PreRun: func(cmd *cobra.Command, args []string) {  		fmt.Printf("Inside rootCmd PreRun with args: %vn", args)  	},  	Run: func(cmd *cobra.Command, args []string) {  		fmt.Printf("Run with args: %vn", args)  	},  	PostRun: func(cmd *cobra.Command, args []string) {  		fmt.Printf("Inside rootCmd PostRun with args: %vn", args)  	},  	PersistentPostRun: func(cmd *cobra.Command, args []string) {  		fmt.Printf("Inside rootCmd PersistentPostRun with args: %vn", args)  	},  }  

運行如下

總結

到這裡,我們就已經學會了如果設置子命令、flag參數、arguments參數以及編寫方法使用這些參數,
還有最後一步,就是編譯出我們的二進制程序,驗證一下我們之前的需求

  • 編譯
go build -o chenqionghe  

如下,已經生成二進制文件chenqionghe

  • 運行命令
./chenqionghe -h # 查看幫助  ./chenqionghe version # 查看版本,類似docker version  ./chenqionghe hello -h # 查看hello命令幫助,類似docker ps -h  ./chenqionghe hello --name light-weight-baby --author gym # 使用hello命令,類似docker run --name app --volume /app/data  

可以看到,完美的實現了預期需求,就是這麼簡單,light weight baby!