Golang | 使用 Cobra 構(gòu)建命令行工具

文章首發(fā)于個(gè)人公眾號(hào):「阿拉平平」

最近折騰了下命令行庫(kù) Cobra,和大家分享下蝴猪。本文演示環(huán)境為 CentOS 7.5若厚,Golang 1.11。

文章目錄:

  1. Cobra 介紹
    1.1 概念
    1.2 安裝
    1.3 初始化
    1.4 代碼分析

  2. Cobra 實(shí)踐
    2.1 子命令
    2.2 子命令嵌套
    2.3 參數(shù)
    2.4 標(biāo)志
    2.5 讀取配置
    2.6 編譯運(yùn)行

1. Cobra 介紹

Cobra 是一個(gè)用來(lái)創(chuàng)建命令行的 golang 庫(kù)蒿辙,同時(shí)也是一個(gè)用于生成應(yīng)用和命令行文件的程序。

1.1 概念

Cobra 結(jié)構(gòu)由三部分組成:命令 (commands)滨巴、參數(shù) (arguments)思灌、標(biāo)志 (flags)」。基本模型如下:
APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME COMMAND ARG --FLAG

如果不是太理解的話泰偿,沒(méi)關(guān)系,我們先看個(gè)例子:

hugo server --port=1313
  • hugo:根命令
  • server:子命令
  • --port:標(biāo)志

再看個(gè)帶有參數(shù)的例子:

git clone URL --bare
  • git:根命令
  • clone:子命令
  • URL:參數(shù)蜈垮,即 clone 作用的對(duì)象
  • --bare:標(biāo)志

總結(jié)下:

  • commands 代表行為耗跛,是應(yīng)用的中心點(diǎn)
  • arguments 代表行為作用的對(duì)象
  • flags 是行為的修飾符

相信看了例子后,應(yīng)該有個(gè)直觀的認(rèn)識(shí)了攒发。接下來(lái)我們安裝 Cobra调塌。

1.2 安裝

安裝很簡(jiǎn)單:

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

但是由于網(wǎng)絡(luò)原因,有些包會(huì)下載失敗惠猿,提示 i/o timeout

package golang.org/x/sys/unix: unrecognized import path "golang.org/x/sys/unix" (https fetch: Get https://golang.org/x/sys/unix?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/transform: unrecognized import path "golang.org/x/text/transform" (https fetch: Get https://golang.org/x/text/transform?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/unicode/norm: unrecognized import path "golang.org/x/text/unicode/norm" (https fetch: Get https://golang.org/x/text/unicode/norm?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

網(wǎng)上解決方法很多羔砾,這里我推薦使用 gopm 來(lái)下載:

# 下載 gopm,之后會(huì)在 $GOPATH/bin 目錄下生成 gopm
go get -u github.com/gpmgo/gopm

# 使用 gopm 來(lái)下載 cobra
gopm get -u -g github.com/spf13/cobra/cobra

下載完成后安裝 cobra 工具,在 $GOPATH/bin 會(huì)生成可執(zhí)行文件:

go install github.com/spf13/cobra/cobra

將生成的 cobra 工具放到 $PATH 目錄下姜凄,可以看到:

[root@localhost ~]# cp -a $GOPATH/bin/cobra /usr/local/bin
[root@localhost ~]# cobra
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.

Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

Use "cobra [command] --help" for more information about a command.

接下來(lái)我們初始化一個(gè)項(xiàng)目政溃。

1.3 初始化

通過(guò) cobra init 初始化 demo 項(xiàng)目:

[root@localhost ~]# cd $GOPATH/src 
[root@localhost src]# cobra init demo --pkg-name=demo
Your Cobra applicaton is ready at
/root/go/src/demo

當(dāng)前項(xiàng)目結(jié)構(gòu)為:

demo
├── cmd
│   └── root.go
├── LICENSE
└── main.go

可以看到初始化后的項(xiàng)目非常簡(jiǎn)單,主要是 main.goroot.go 文件檀葛。在編寫代碼之前玩祟,我們先分析下目前代碼的邏輯腹缩。

1.4 代碼分析

先查看下入口文件 main.go屿聋。代碼邏輯很簡(jiǎn)單,就是調(diào)用 cmd 包里 Execute()函數(shù):

package main

import "demo/cmd"

func main() {
  cmd.Execute()
}

再看下 root.go 中 rootCmd 的字段:

...

var rootCmd = &cobra.Command{
  Use:   "demo",
  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.`,
  // Uncomment the following line if your bare application
  // has an action associated with it:
  //    Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

...

簡(jiǎn)單說(shuō)明下:

  • Use:命令名
  • Short & Long:幫助信息的文字內(nèi)容
  • Run:運(yùn)行命令的邏輯

Command 結(jié)構(gòu)體中的字段當(dāng)然遠(yuǎn)不止這些藏鹊,受限于篇幅润讥,這里無(wú)法全部介紹。有興趣的童鞋可以查閱下官方文檔盘寡。

運(yùn)行測(cè)試:

[root@localhost demo]# go run main.go
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.

subcommand is required
exit status 1

如果運(yùn)行的結(jié)果和我的一致楚殿,那我們就可以進(jìn)入到實(shí)踐環(huán)節(jié)了。

2. Cobra 實(shí)踐

鋪墊了這么久竿痰,終于可以開(kāi)始實(shí)踐了脆粥。實(shí)踐環(huán)節(jié)中,我會(huì) 提一些需求影涉,然后我們一起實(shí)現(xiàn)一個(gè)簡(jiǎn)單的命令行工具变隔。

2.1 子命令

之前運(yùn)行會(huì)提示 subcommand is required,是因?yàn)楦顭o(wú)法直接運(yùn)行蟹倾。那我們就添加個(gè)子命令試試匣缘。

通過(guò) cobra add 添加子命令 create:

[root@localhost demo]# cobra add create
create created at /root/go/src/demo

當(dāng)前項(xiàng)目結(jié)構(gòu)為:

demo
├── cmd
│   ├── create.go
│   └── root.go
├── LICENSE
└── main.go

查看下 create.goinit() 說(shuō)明了命令的層級(jí)關(guān)系:

...

func init() {
       rootCmd.AddCommand(createCmd)        
}

運(yùn)行測(cè)試:

# 輸入正確
[root@localhost demo]# go run main.go create
create called

# 未知命令
[root@localhost demo]# go run main.go crea
Error: unknown command "crea" for "demo"

Did you mean this?
    create

Run 'demo --help' for usage.
unknown command "crea" for "demo"

Did you mean this?
    create

2.2 子命令嵌套

對(duì)于功能相對(duì)復(fù)雜的 CLI鲜棠,通常會(huì)通過(guò)多級(jí)子命令肌厨,即:子命令嵌套的方式進(jìn)行描述,那么該如何實(shí)現(xiàn)呢豁陆?

demo create rule

首先添加子命令 rule :

[root@localhost demo]# cobra add rule
rule created at /root/go/src/demo

當(dāng)前目錄結(jié)構(gòu)如下:

demo
├── cmd
│   ├── create.go
│   ├── root.go
│   └── rule.go
├── LICENSE
└── main.go

目前createrule 是同級(jí)的柑爸,所以需要修改 rule.goinit() 來(lái)改變子命令間的層級(jí)關(guān)系:

...

func init() {
        // 修改子命令的層級(jí)關(guān)系
        //rootCmd.AddCommand(ruleCmd)
        createCmd.AddCommand(ruleCmd)
}

雖然調(diào)整了命令的層級(jí)關(guān)系,但是目前運(yùn)行 demo create 會(huì)打印 create called盒音,我希望運(yùn)行時(shí)可以打印幫助提示表鳍。所以我們繼續(xù)完善下代碼,修改 create.go

...

var createCmd = &cobra.Command{
        Use:   "create",
        Short: "create",
        Long: "Create Command.",
        Run: func(cmd *cobra.Command, args []string) {
                // 如果 create 命令后沒(méi)有參數(shù)里逆,則提示幫助信息
                if len(args) == 0 {
                  cmd.Help()
                  return
                }
        },
}

...

運(yùn)行測(cè)試:

  • 直接運(yùn)行 create进胯,打印幫助提示:
[root@localhost demo]# go run main.go create
Create Command.

Usage:
  demo create [flags]
  demo create [command]

Available Commands:
  rule        A brief description of your command

Flags:
  -h, --help   help for create

Global Flags:
      --config string   config file (default is $HOME/.demo.yaml)

Use "demo create [command] --help" for more information about a command.
  • 運(yùn)行 create rule,輸出 rule called
[root@localhost demo]# go run main.go create rule
rule called

2.3 參數(shù)

先說(shuō)說(shuō)參數(shù)≡海現(xiàn)在有個(gè)需求:給 CLI 加個(gè)位置參數(shù)胁镐,要求參數(shù)有且僅有一個(gè)。這個(gè)需求我們要如何實(shí)現(xiàn)呢?

demo create rule foo 

實(shí)現(xiàn)前先說(shuō)下盯漂,Command 結(jié)構(gòu)體中有個(gè) Args 的字段颇玷,接受類型為 type PositionalArgs func(cmd *Command, args []string) error

內(nèi)置的驗(yàn)證方法如下:

  • NoArgs:如果有任何參數(shù),命令行將會(huì)報(bào)錯(cuò)
  • ArbitraryArgs: 命令行將會(huì)接收任何參數(shù)
  • OnlyValidArgs: 如果有如何參數(shù)不屬于 Command 的 ValidArgs 字段就缆,命令行將會(huì)報(bào)錯(cuò)
  • MinimumNArgs(int): 如果參數(shù)個(gè)數(shù)少于 N 個(gè)帖渠,命令行將會(huì)報(bào)錯(cuò)
  • MaximumNArgs(int): 如果參數(shù)個(gè)數(shù)多于 N 個(gè),命令行將會(huì)報(bào)錯(cuò)
  • ExactArgs(int): 如果參數(shù)個(gè)數(shù)不等于 N 個(gè)竭宰,命令行將會(huì)報(bào)錯(cuò)
  • RangeArgs(min, max): 如果參數(shù)個(gè)數(shù)不在 min 和 max 之間, 命令行將會(huì)報(bào)錯(cuò)

由于需求里要求參數(shù)有且僅有一個(gè)空郊,想想應(yīng)該用哪個(gè)內(nèi)置驗(yàn)證方法呢?相信你已經(jīng)找到了 ExactArgs(int)切揭。

改寫下 rule.go

...

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        
        Args: cobra.ExactArgs(1),
        Run: func(cmd *cobra.Command, args []string) {           
          fmt.Printf("Create rule %s success.\n", args[0])
        },
}

...

運(yùn)行測(cè)試:

  • 不輸入?yún)?shù):
[root@localhost demo]# go run main.go create rule
Error: accepts 1 arg(s), received 0
  • 輸入 1 個(gè)參數(shù):
[root@localhost demo]# go run main.go create rule foo
Create rule foo success.
  • 輸入 2 個(gè)參數(shù):
[root@localhost demo]# go run main.go create rule
Error: accepts 1 arg(s), received 2

從測(cè)試的情況看狞甚,運(yùn)行的結(jié)果符合我們的預(yù)期。如果需要對(duì)參數(shù)進(jìn)行復(fù)雜的驗(yàn)證廓旬,還可以自定義 Args哼审,這里就不多做贅述了。

2.4 標(biāo)志

再說(shuō)說(shuō)標(biāo)志≡斜現(xiàn)在要求 CLI 不接受參數(shù)涩盾,而是通過(guò)標(biāo)志 --name 對(duì) rule 進(jìn)行描述。這個(gè)又該如何實(shí)現(xiàn)励背?

demo create rule --name foo

Cobra 中有兩種標(biāo)志:持久標(biāo)志 ( Persistent Flags ) 和 本地標(biāo)志 ( Local Flags ) 春霍。

持久標(biāo)志:指所有的 commands 都可以使用該標(biāo)志。比如:--verbose 椅野,--namespace
本地標(biāo)志:指特定的 commands 才可以使用該標(biāo)志终畅。

這個(gè)標(biāo)志的作用是修飾和描述 rule的名字,所以選用本地標(biāo)志竟闪。修改 rule.go

package cmd

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

// 添加變量 name
var name string

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        Run: func(cmd *cobra.Command, args []string) {
          // 如果沒(méi)有輸入 name
          if len(name) == 0 {
            cmd.Help()
            return
          }     
          fmt.Printf("Create rule %s success.\n", name)
        },
}

func init() {
        createCmd.AddCommand(ruleCmd)
        // 添加本地標(biāo)志
        ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name")      
}

說(shuō)明:StringVarP 用來(lái)接收類型為字符串變量的標(biāo)志离福。相較StringVarStringVarP 支持標(biāo)志短寫炼蛤。以我們的 CLI 為例:在指定標(biāo)志時(shí)可以用 --name妖爷,也可以使用短寫 -n

運(yùn)行測(cè)試:

# 這幾種寫法都可以執(zhí)行
[root@localhost demo]# go run main.go create rule -n foo
Create rule foo success.
[root@localhost demo]# go run main.go create rule --name foo
Create rule foo success.
[root@localhost demo]# go run main.go create -n foo rule
Create rule foo success.

2.5 讀取配置

最后說(shuō)說(shuō)配置理朋。需求:要求 --name 標(biāo)志存在默認(rèn)值絮识,且該值是可配置的。

如果只需要標(biāo)志提供默認(rèn)值嗽上,我們只需要修改 StringVarPvalue 參數(shù)就可以實(shí)現(xiàn)次舌。但是這個(gè)需求關(guān)鍵在于標(biāo)志是可配置的,所以需要借助配置文件兽愤。

很多情況下彼念,CLI 是需要讀取配置信息的挪圾,比如 kubectl 的~/.kube/config。在幫助提示里可以看到默認(rèn)的配置文件為 $HOME/.demo.yaml

Global Flags:
      --config string   config file (default is $HOME/.demo.yaml)

?配置庫(kù)我們可以使用 Viper逐沙。Viper 是 Cobra 集成的配置文件讀取庫(kù)哲思,支持 YAMLJSON吩案, TOML棚赔, HCL 等格式的配置。

添加配置文件 $HOME/.demo.yaml徘郭,增加 name 字段:

[root@localhost ~]# vim $HOME/.demo.yaml 
name: foo

修改 rule.go:

package cmd

import (
        "fmt"
         // 導(dǎo)入 viper 包
        "github.com/spf13/viper"
        "github.com/spf13/cobra"
)

var name string

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        Run: func(cmd *cobra.Command, args []string) {
          // 不輸入 --name 從配置文件中讀取 name
          if len(name) == 0 {
            name = viper.GetString("name")
            // 配置文件中未讀取到 name靠益,打印幫助提示
            if len(name) == 0 {
              cmd.Help()
              return
            }
          }
          fmt.Printf("Create rule %s success.\n", name)
        },
}

func init() {
        createCmd.AddCommand(ruleCmd)
        ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name")
}

運(yùn)行測(cè)試:

[root@localhost demo]# go run main.go create rule
Using config file: /root/.demo.yaml
Create rule foo success.

如果 CLI 沒(méi)有用到配置文件,可以在初始化項(xiàng)目的時(shí)候關(guān)閉 Viper 的選項(xiàng)以減少編譯后文件的體積崎岂,如下:

cobra init demo --pkg-name=demo --viper=false

2.6 編譯運(yùn)行

?編譯生成命令行工具:

[root@localhost demo]# go build -o demo

運(yùn)行測(cè)試:

[root@localhost demo]# ./demo create rule
Using config file: /root/.demo.yaml
Create rule foo success.
參考文檔:
  1. Github - https://github.com/spf13/cobra
  2. Cobra 的一些筆記 - https://zhangguanzhang.github.io/2019/06/02/cobra/
  3. Golang之使用Cobra - https://o-my-chenjian.com/2017/09/20/Using-Cobra-With-Golang/
  4. golang命令行庫(kù)Cobra的使用 - http://www.reibang.com/p/7abe7cff5384
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末捆毫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子冲甘,更是在濱河造成了極大的恐慌,老刑警劉巖途样,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件江醇,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡何暇,警方通過(guò)查閱死者的電腦和手機(jī)陶夜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)裆站,“玉大人条辟,你說(shuō)我怎么就攤上這事『昕瑁” “怎么了羽嫡?”我有些...
    開(kāi)封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)肩袍。 經(jīng)常有香客問(wèn)我杭棵,道長(zhǎng),這世上最難降的妖魔是什么氛赐? 我笑而不...
    開(kāi)封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任魂爪,我火速辦了婚禮,結(jié)果婚禮上艰管,老公的妹妹穿的比我還像新娘滓侍。我一直安慰自己,他們只是感情好牲芋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布撩笆。 她就那樣靜靜地躺著尔破,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浇衬。 梳的紋絲不亂的頭發(fā)上懒构,一...
    開(kāi)封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音耘擂,去河邊找鬼胆剧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛醉冤,可吹牛的內(nèi)容都是我干的秩霍。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蚁阳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼铃绒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起螺捐,我...
    開(kāi)封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤颠悬,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后定血,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體赔癌,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年澜沟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了灾票。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡茫虽,死狀恐怖刊苍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情濒析,我是刑警寧澤正什,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站悼枢,受9級(jí)特大地震影響埠忘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜馒索,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一莹妒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绰上,春花似錦旨怠、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)迷扇。三九已至,卻和暖如春爽哎,著一層夾襖步出監(jiān)牢的瞬間蜓席,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工课锌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厨内,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓渺贤,卻偏偏與公主長(zhǎng)得像雏胃,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子志鞍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 寫了2次才寫完瞭亮,內(nèi)容很長(zhǎng),翻譯了很久固棚,內(nèi)容來(lái)源于Cobra github介紹统翩。翻譯完也更全面的了解了Cobra,功...
    最近不在閱讀 28,614評(píng)論 0 14
  • feisky云計(jì)算玻孟、虛擬化與Linux技術(shù)筆記posts - 1014, comments - 298, trac...
    不排版閱讀 3,815評(píng)論 0 5
  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,367評(píng)論 0 5
  • cmdr 是另一個(gè)命令行參數(shù)處理器唆缴。 Golang 自己帶有 flags 進(jìn)行命令行參數(shù)處理,算是便利的黍翎,然而和 ...
    jy_675a閱讀 1,142評(píng)論 0 0
  • Description: Given a sorted linked list, delete all dupli...
    Icytail閱讀 187評(píng)論 0 0