Go 每日一庫之 cli

簡介

cli是一個用於構建命令行程序的庫。咱們以前也介紹過一個用於構建命令行程序的庫cobra。在功能上來講二者差很少,cobra的優點是提供了一個腳手架,方便開發。cli很是簡潔,全部的初始化操做就是建立一個cli.App結構的對象。經過爲對象的字段賦值來添加相應的功能。linux

cli與咱們上一篇文章介紹的negroni是同一個做者urfavegit

快速使用

cli須要搭配 Go Modules 使用。建立目錄並初始化:github

$ mkdir cli && cd cli
$ go mod init github.com/darjun/go-daily-lib/cli
複製代碼

安裝cli庫,有v1v2兩個版本。若是沒有特殊需求,通常安裝v2版本:golang

$ go get -u github.com/urfave/cli/v2
複製代碼

使用:微信

package main

import (
  "fmt"
  "log"
  "os"

  "github.com/urfave/cli/v2"
)

func main() {
  app := &cli.App{
    Name:  "hello",
    Usage: "hello world example",
    Action: func(c *cli.Context) error {
      fmt.Println("hello world")
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

使用很是簡單,理論上建立一個cli.App結構的對象,而後調用其Run()方法,傳入命令行的參數便可。一個空白的cli應用程序以下:app

func main() {
  (&cli.App{}).Run(os.Args)
}
複製代碼

可是這個空白程序沒有什麼用處。咱們的hello world程序,設置了Name/Usage/ActionNameUsage都顯示在幫助中,Action是調用該命令行程序時實際執行的函數,須要的信息能夠從參數cli.Context獲取。dom

編譯、運行(環境:Win10 + Git Bash):ide

$ go build -o hello
$ ./hello
hello world
複製代碼

除了這些,cli爲咱們額外生成了幫助信息:函數

$ ./hello --help
NAME: hello - hello world example USAGE: hello [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) 複製代碼

參數

經過cli.Context的相關方法咱們能夠獲取傳給命令行的參數信息:oop

  • NArg():返回參數個數;
  • Args():返回cli.Args對象,調用其Get(i)獲取位置i上的參數。

示例:

func main() {
  app := &cli.App{
    Name:  "arguments",
    Usage: "arguments example",
    Action: func(c *cli.Context) error {
      for i := 0; i < c.NArg(); i++ {
        fmt.Printf("%d: %s\n", i+1, c.Args().Get(i))
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

這裏只是簡單輸出:

$ go run main.go hello world
1: hello
2: world
複製代碼

選項

一個好用的命令行程序怎麼會少了選項呢?cli設置和獲取選項很是簡單。在cli.App{}結構初始化時,設置字段Flags便可添加選項。Flags字段是[]cli.Flag類型,cli.Flag其實是接口類型。cli爲常見類型都實現了對應的XxxFlag,如BoolFlag/DurationFlag/StringFlag等。它們有一些共用的字段,Name/Value/Usage(名稱/默認值/釋義)。看示例:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:  "lang",
        Value: "english",
        Usage: "language for the greeting",
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if c.String("lang") == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("你好", name)
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

上面是一個打招呼的命令行程序,可經過選項lang指定語言,默認爲英語。設置選項爲非english的值,使用漢語。若是有參數,使用第一個參數做爲人名,不然使用world。注意選項是經過c.Type(name)來獲取的,Type爲選項類型,name爲選項名。編譯、運行:

$ go build -o flags

# 默認調用
$ ./flags
hello world

# 設置非英語
$ ./flags --lang chinese
你好 world

# 傳入參數做爲人名
$ ./flags --lang chinese dj
你好 dj
複製代碼

咱們能夠經過./flags --help來查看選項:

$ ./flags --help
NAME: flags - A new cli application USAGE: flags [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --lang value language for the greeting (default: "english") --help, -h show help (default: false) 複製代碼

存入變量

除了經過c.Type(name)來獲取選項的值,咱們還能夠將選項存到某個預先定義好的變量中。只須要設置Destination字段爲變量的地址便可:

func main() {
  var language string

  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:        "lang",
        Value:       "english",
        Usage:       "language for the greeting",
        Destination: &language,
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if language == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("你好", name)
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

與上面的程序效果是同樣的。

佔位值

cli能夠在Usage字段中爲選項設置佔位值,佔位值經過反引號 ` 包圍。只有第一個生效,其餘的維持不變。佔位值有助於生成易於理解的幫助信息:

func main() {
  app := & cli.App{
    Flags : []cli.Flag {
      &cli.StringFlag{
        Name:"config",
        Usage: "Load configuration from `FILE`",
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

設置佔位值以後,幫助信息中,該佔位值會顯示在對應的選項後面,對短選項也是有效的:

$ go build -o placeholder
$ ./placeholder --help
NAME: placeholder - A new cli application USAGE: placeholder [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --config FILE Load configuration from FILE --help, -h show help (default: false) 複製代碼

別名

選項能夠設置多個別名,設置對應選項的Aliases字段便可:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "lang",
        Aliases: []string{"language", "l"},
        Value:   "english",
        Usage:   "language for the greeting",
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if c.String("lang") == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("你好", name)
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

使用--lang chinese--language chinese-l chinese效果是同樣的。若是經過不一樣的名稱指定同一個選項,會報錯:

$ go build -o aliase
$ ./aliase --lang chinese
你好 world
$ ./aliase --language chinese
你好 world
$ ./aliase -l chinese
你好 world
$ ./aliase -l chinese --lang chinese
Cannot use two forms of the same flag: l lang
複製代碼

環境變量

除了經過執行程序時手動指定命令行選項,咱們還能夠讀取指定的環境變量做爲選項的值。只須要將環境變量的名字設置到選項對象的EnvVars字段便可。能夠指定多個環境變量名字,cli會依次查找,第一個有值的環境變量會被使用。

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "lang",
        Value:   "english",
        Usage:   "language for the greeting",
        EnvVars: []string{"APP_LANG", "SYSTEM_LANG"},
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("你好")
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

編譯、運行:

$ go build -o env
$ APP_LANG=chinese ./env
你好
複製代碼

文件

cli還支持從文件中讀取選項的值,設置選項對象的FilePath字段爲文件路徑:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:     "lang",
        Value:    "english",
        Usage:    "language for the greeting",
        FilePath: "./lang.txt",
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("你好")
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

main.go同級目錄建立一個lang.txt,輸入內容chinese。而後編譯運行程序:

$ go build -o file
$ ./file
你好
複製代碼

cli還支持從YAML/JSON/TOML等配置文件中讀取選項值,這裏就不一一介紹了。

選項優先級

上面咱們介紹了幾種設置選項值的方式,若是同時有多個方式生效,按照下面的優先級從高到低設置:

  • 用戶指定的命令行選項值;
  • 環境變量;
  • 配置文件;
  • 選項的默認值。

組合短選項

咱們時常會遇到有多個短選項的狀況。例如 linux 命令ls -a -l,能夠簡寫爲ls -alcli也支持短選項合寫,只須要設置cli.AppUseShortOptionHandling字段爲true便可:

func main() {
  app := &cli.App{
    UseShortOptionHandling: true,
    Commands: []*cli.Command{
      {
        Name:  "short",
        Usage: "complete a task on the list",
        Flags: []cli.Flag{
          &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}},
          &cli.BoolFlag{Name: "option", Aliases: []string{"o"}},
          &cli.BoolFlag{Name: "message", Aliases: []string{"m"}},
        },
        Action: func(c *cli.Context) error {
          fmt.Println("serve:", c.Bool("serve"))
          fmt.Println("option:", c.Bool("option"))
          fmt.Println("message:", c.Bool("message"))
          return nil
        },
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

編譯運行:

$ go build -o short
$ ./short short -som "some message"
serve: true option: true message: true 複製代碼

須要特別注意一點,設置UseShortOptionHandlingtrue以後,咱們不能再經過-指定選項了,這樣會產生歧義。例如-langcli不知道應該解釋爲l/a/n/g 4 個選項仍是lang 1 個。--仍是有效的。

必要選項

若是將選項的Required字段設置爲true,那麼該選項就是必要選項。必要選項必須指定,不然會報錯:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:     "lang",
        Value:    "english",
        Usage:    "language for the greeting",
        Required: true,
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("你好")
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

不指定選項lang運行:

$ ./required
2020/06/23 22:11:32 Required flag "lang" not set
複製代碼

幫助文本中的默認值

默認狀況下,幫助文本中選項的默認值顯示爲Value字段值。有些時候,Value並非實際的默認值。這時,咱們能夠經過DefaultText設置:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.IntFlag{
        Name:     "port",
        Value:    0,
        Usage:    "Use a randomized port",
        DefaultText :"random",
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

上面代碼邏輯中,若是Value設置爲 0 就隨機一個端口,這時幫助信息中default: 0就容易產生誤解了。經過DefaultText能夠避免這種狀況:

$ go build -o default-text
$ ./default-text --help
NAME: default-text - A new cli application USAGE: default-text [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --port value Use a randomized port (default: random) --help, -h show help (default: false) 複製代碼

子命令

子命令使命令行程序有更好的組織性。git有大量的命令,不少以某個命令下的子命令存在。例如git remote命令下有add/rename/remove等子命令,git submodule下有add/status/init/update等子命令。

cli經過設置cli.AppCommands字段添加命令,設置各個命令的SubCommands字段,便可添加子命令。很是方便!

func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name:    "add",
        Aliases: []string{"a"},
        Usage:   "add a task to the list",
        Action: func(c *cli.Context) error {
          fmt.Println("added task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:    "complete",
        Aliases: []string{"c"},
        Usage:   "complete a task on the list",
        Action: func(c *cli.Context) error {
          fmt.Println("completed task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:    "template",
        Aliases: []string{"t"},
        Usage:   "options for task templates",
        Subcommands: []*cli.Command{
          {
            Name:  "add",
            Usage: "add a new template",
            Action: func(c *cli.Context) error {
              fmt.Println("new task template: ", c.Args().First())
              return nil
            },
          },
          {
            Name:  "remove",
            Usage: "remove an existing template",
            Action: func(c *cli.Context) error {
              fmt.Println("removed task template: ", c.Args().First())
              return nil
            },
          },
        },
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

上面定義了 3 個命令add/complete/templatetemplate命令定義了 2 個子命令add/remove。編譯、運行:

$ go build -o subcommand
$ ./subcommand add dating
added task:  dating
$ ./subcommand complete dating
completed task:  dating
$ ./subcommand template add alarm
new task template:  alarm
$ ./subcommand template remove alarm
removed task template:  alarm
複製代碼

注意一點,子命令默認不顯示在幫助信息中,須要顯式調用子命令所屬命令的幫助(./subcommand template --help):

$ ./subcommand --help
NAME: subcommand - A new cli application USAGE: subcommand [global options] command [command options] [arguments...] COMMANDS: add, a add a task to the list complete, c complete a task on the list template, t options for task templates help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) $ ./subcommand template --help NAME: subcommand template - options for task templates USAGE: subcommand template command [command options] [arguments...] COMMANDS: add add a new template remove remove an existing template help, h Shows a list of commands or help for one command OPTIONS: --help, -h show help (default: false) 複製代碼

分類

在子命令數量不少的時候,能夠設置Category字段爲它們分類,在幫助信息中會將相同分類的命令放在一塊兒展現:

func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name:  "noop",
        Usage: "Usage for noop",
      },
      {
        Name:     "add",
        Category: "template",
        Usage:    "Usage for add",
      },
      {
        Name:     "remove",
        Category: "template",
        Usage:    "Usage for remove",
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
複製代碼

編譯、運行:

$ go build -o categories
$ ./categories --help
NAME: categories - A new cli application USAGE: categories [global options] command [command options] [arguments...] COMMANDS: noop Usage for noop help, h Shows a list of commands or help for one command template: add Usage for add remove Usage for remove GLOBAL OPTIONS: --help, -h show help (default: false) 複製代碼

看上面的COMMANDS部分。

自定義幫助信息

cli中全部的幫助信息文本均可以自定義,整個應用的幫助信息模板經過AppHelpTemplate指定。命令的幫助信息模板經過CommandHelpTemplate設置,子命令的幫助信息模板經過SubcommandHelpTemplate設置。甚至能夠經過覆蓋cli.HelpPrinter這個函數本身實現幫助信息輸出。下面程序在默認的幫助信息後添加我的網站和微信信息:

func main() {
  cli.AppHelpTemplate = fmt.Sprintf(`%s WEBSITE: http://darjun.github.io WECHAT: GoUpUp`, cli.AppHelpTemplate)

  (&cli.App{}).Run(os.Args)
}
複製代碼

編譯運行:

$ go build -o help
$ ./help --help
NAME: help - A new cli application USAGE: help [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) WEBSITE: http://darjun.github.io WECHAT: GoUpUp 複製代碼

咱們還能夠改寫整個模板:

func main() {
  cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: {{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} COPYRIGHT: {{.Copyright}} {{end}}{{if .Version}} VERSION: {{.Version}} {{end}} `

  app := &cli.App{
    Authors: []*cli.Author{
      {
        Name:  "dj",
        Email: "darjun@126.com",
      },
    },
  }
  app.Run(os.Args)
}
複製代碼

{{.XXX}}其中XXX對應cli.App{}結構中設置的字段,例如上面Authors

$ ./help --help
NAME: help - A new cli application USAGE: help [global options] command [command options] [arguments...] AUTHOR: dj <darjun@126.com> COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) 複製代碼

注意觀察AUTHOR部分。

經過覆蓋HelpPrinter,咱們能本身輸出幫助信息:

func main() {
  cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
    fmt.Println("Simple help!")
  }

  (&cli.App{}).Run(os.Args)
}
複製代碼

編譯、運行:

$ ./help --help
Simple help!
複製代碼

內置選項

幫助選項

默認狀況下,幫助選項爲--help/-h。咱們能夠經過cli.HelpFlag字段設置:

func main() {
  cli.HelpFlag = &cli.BoolFlag{
    Name:    "haaaaalp",
    Aliases: []string{"halp"},
    Usage:   "HALP",
  }

  (&cli.App{}).Run(os.Args)
}
複製代碼

查看幫助:

$ go run main.go --halp
NAME: main.exe - A new cli application USAGE: main.exe [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --haaaaalp, --halp HALP (default: false) 複製代碼

版本選項

默認版本選項-v/--version輸出應用的版本信息。咱們能夠經過cli.VersionFlag設置版本選項 :

func main() {
  cli.VersionFlag = &cli.BoolFlag{
    Name:    "print-version",
    Aliases: []string{"V"},
    Usage:   "print only the version",
  }

  app := &cli.App{
    Name:    "version",
    Version: "v1.0.0",
  }
  app.Run(os.Args)
}
複製代碼

這樣就能夠經過指定--print-version/-V輸出版本信息了。運行:

$ go run main.go --print-version
version version v1.0.0

$ go run main.go -V
version version v1.0.0
複製代碼

咱們還能夠經過設置cli.VersionPrinter字段控制版本信息的輸出內容:

const (
  Revision = "0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b"
)

func main() {
  cli.VersionPrinter = func(c *cli.Context) {
    fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision)
  }

  app := &cli.App{
    Name:    "version",
    Version: "v1.0.0",
  }
  app.Run(os.Args)
}
複製代碼

上面程序同時輸出版本號和git提交的 SHA 值:

$ go run main.go -v
version=v1.0.0 revision=0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b
複製代碼

總結

cli很是靈活,只須要設置cli.App的字段值便可實現相應的功能,不須要額外記憶函數、方法。另外cli還支持 Bash 自動補全的功能,對 zsh 的支持也比較好,感興趣可自行探索。

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. cli GitHub:github.com/urfave/cli
  2. Go 每日一庫之 cobra:darjun.github.io/2020/01/17/…
  3. Go 每日一庫 GitHub:github.com/darjun/go-d…

個人博客:darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索