Go通過cobra快速構建命令列應用

iqsing發表於2022-04-19

來自jetbrains Go 語言現狀調查報告 顯示:在go開發者中使用go開發實用小程式的比例為31%僅次於web,go得益於跨平臺、無依賴的特性,用來編寫命令列或系統管理這類小程式非常不錯。

image-20220418152624008

本文主要介紹Steve Francia(spf13)大神寫的用於快速構建命令列程式的golang包cobra,基於cobra寫命令列的著名專案一隻手數不過來:Docker CLI、Helm、istio、etcd、Git、Github CLI ...

下面進入正題

cobra能幫我們做啥?


cobra包提供以下功能:

  • 輕鬆建立基於子命令的 CLI:如app serverapp fetch等。
  • 自動新增-h,--help等幫助性Flag
  • 自動生成命令和Flag的幫助資訊
  • 建立完全符合 POSIX 的Flag(標誌)(包括長、短版本)
  • 支援巢狀子命令
  • 支援全域性、本地和級聯Flag
  • 智慧建議(app srver... did you mean app server?)
  • 為應用程式自動生成 shell 自動完成功能(bash、zsh、fish、powershell)
  • 為應用程式自動生成man page
  • 命令別名,可以在不破壞原有名稱的情況下進行更改
  • 支援靈活自定義help、usege等。
  • 無縫整合viper構建12-factor應用

cobra遵循commands, arguments & flags結構。

舉例來說

#appname command  arguments
docker pull alpine:latest
#appname command flag
docker ps -a
#appname command flag argument
git commit -m "msg"

開發者可根據情況進行自組織。

cobra基礎使用


安裝cobra包和二進位制工具cobra-cli,cobra-cli可以幫助我們快速建立出一個cobra基礎程式碼結構。

go get -u github.com/spf13/cobra@latest
go install github.com/spf13/cobra-cli@latest

啟用GO111MODULE=on,我們初始化一個xpower

# go mod init  xpower
go: creating new go.mod: module xpower

使用cobra-cli初始化基礎程式碼結構

# cobra-cli  init
Your Cobra application is ready at /root/demo/xpower

#檢視目錄結構
# tree xpower
xpower
├── cmd
│   └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go

1 directory, 5 files

執行demo可以看到cobra包本身的一些提示資訊。

image-20220419103917739

檢視main.go,cobra-cli為我們建立了一個cmd的包並且呼叫了包裡面的Execute()函式

/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>

*/
package main

import "xpower/cmd"

func main() {
        cmd.Execute()
}

從上面的目錄結構中可以看到cmd包目前只有一個root.go,我們可以在這裡操作根命令相關的內容。

大多數時候CLI可能會包含多個子命令比如git clonegit add,cobra-cli可通過add 新增子命令。

現在我們新增wget和ping子命令,即接下來我們將通過xpower來重寫wget和ping的部分功能。

cobra-cli add wget
cobra-cli add ping 

現在的目錄結構如下:

# tree xpower
xpower
├── cmd
│   ├── ping.go
│   ├── root.go
│   └── wget.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go

pingwget已經被整合到root.go中

image-20220419112101887

wget.go

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

// wgetCmd represents the wget command
var wgetCmd = &cobra.Command{
    Use:     "wget",
    Example: "xpower wget iqsing.github.io/download.tar -o /tmp",
    Short:   "wget is a download cli.",
    Long:    `use wget to download everything you want from net.`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("wget called")  
    },
}

func init() {
    rootCmd.AddCommand(wgetCmd)

    // Here you will define your flags and configuration settings.
}

在wget.go 中定義了一個wgetCmd結構體指標,可通過檢視Command結構體原型新增或移除成員變數。這裡我們新增了一個Example用於指示示例,Short和Long為命令簡介,Run為wget命令的真正實現。

我們知道在go中包的init()函式會在import時執行,通過AddCommand(wgetCmd)將wegetCmd新增到結構體Command 成員變數commands中,包括後面我們編寫的Flag也是如此。

接下來我們在結構體中新增Args用於驗證(限制)引數數量,在init()函式中新增Flag -o用於儲存下載的檔案地址,並通過MarkFlagRequired約束flag的引數必須輸入,最後在Run中呼叫Download即可。

package cmd

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"

    "github.com/spf13/cobra"
)

var (
    output string
)

// wgetCmd represents the wget command
var wgetCmd = &cobra.Command{
    Use:     "wget",
    Example: "xpower wget iqsing.github.io/download.tar.gz -o /tmp/download.tar.gz",
    Args:    cobra.ExactArgs(1),
    Short:   "wget is a download cli.",
    Long:    `use wget to download everything you want from net.`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("---wget running---")
        Download(args[0], output)
    },
}

func init() {
    rootCmd.AddCommand(wgetCmd)
    // Here you will define your flags and configuration settings.

    wgetCmd.Flags().StringVarP(&output, "output", "o", "", "output file")
    wgetCmd.MarkFlagRequired("output")
}
func Download(url string, path string) {
    out, err := os.Create(path)
    check(err)
    defer out.Close()

    res, err := http.Get(url)
    check(err)
    defer res.Body.Close()

    _, err = io.Copy(out, res.Body)
    check(err)
    fmt.Println("save as" + path)
}
func check(err error) {
    if err != nil {
        log.Fatal(err)
    }
}
args
Args:    cobra.ExactArgs(1)

cobra內建的引數驗證也是比較多,NoArgs、OnlyValidArgs、MinimumNArgs、MaximumNArgs等等可翻閱原始碼args.go,可以滿足基本使用,如果有自己的特殊要求可以通過解析arg來實現。

flags
wgetCmd.Flags().StringVarP(&output, "output", "o", "", "output file(required)")

flag包含區域性和全域性兩種,全域性flag在父命令定義後子命令也會生效,而區域性flag則在哪定義就在哪生效。

如上面的區域性flag,我們在wgetCmd中定義的flag只有wget這個子命令能用。

全域性flag

rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

StringVarpBoolVarP 用於flag資料型別限制。

簡單的應用從命令列直接寫入引數是很常見的,但是如果比較複雜的命令列應用引數需要非常多,再這樣操作不太合理,cobra作者還寫了另一個在go中很流行的包viper用於解析配置檔案,比如kubectl 的yml,以及各種json

前面也說過可以無縫銜接,只需Bind一下即可。

var author string

func init() {
  rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}

flag還可以做依賴,比如下面username和password必須同時接收到引數。

rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)")
rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)")
rootCmd.MarkFlagsRequiredTogether("username", "password")

新增子命令可參考包go-ping/ping,這裡不再贅述。

我們來看編譯後使用如何?

通過-h檢視幫助:

image-20220419153813818

引數個數錯誤:

image-20220419154100059

需要flag-o

image-20220419154336330

正確使用:

xpower 子命令ping:

image-20220419154528095

xpower 子命令wget:

image-20220419154738554


以上我們通過go中cobra包實現xpower命令,包含重寫了簡單功能的ping和wget兩子命令,甚至我們還可以以此來實現自己的跨平臺、無依賴的工具集。本文涉及程式碼已提交至倉庫code/xpower

cobra包含很多開箱即用的功能,經過大量專案驗證和完善,已滿足大部分命令列應用構建需求。本文只介紹了一部分內容,更多內容可檢視倉庫spf13/cobra

通過部落格閱讀:iqsing.github.io

相關文章