68

Golang第三方命令行工具 - spf13/cobra和urfave/cli

 5 years ago
source link: https://www.tuicool.com/articles/M7VbUvI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

虽然Go语言标准库里面有flag包可以作为命令行解析工具,但是flag包的用法有限,举例几个场景:

  1. 子命令。flag虽然也可以支持子命令,如 hugo server ,但是写起来非常麻烦。对于三级甚至更多级别的子命令更不支持。
  2. –开头参数。flag只支持带一个减号的参数,例如 hugo --debug 这种支持很有限。
  3. 同时支持1/2个开头的参数。例如hugo,支持 -h--help ,都是显示帮助信息。

所以在实际项目中可以引入第三方命令行工具更好的支持这些场景。目前Go语言世界中最流行的工具库包含 spf13/cobraurfave/cli

spf13/cobra

spf13/cobra 既是一个用来创建强大的现代CLI命令行的golang库,也是一个生成程序应用和命令行文件的工具。Docker、Kubernetes、Hugo、 Etcd等程序都使用了它构建命令。

首先安装它:

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

先感受下生成程序应用和命令行的用法,也就是直接生成个命令行架子:

❯ mkdir -p ~/strconv.code/cli/src/cobraDemo
❯ cd ~/strconv.code/cli/src/cobraDemo
❯ cobra init --pkg-name cobraDemo  # 创建一个叫做cobraDemo的应用,目前官网文档该没有更新,旧的`cobra init cobraDemo`已经不能用了
❯ tree .  # 在当前目录下会创建一个cmd(里面只包含root.go)和一个main.go
.
├── LICENSE
├── cmd
│   └── root.go
└── main.go

1 directory, 3 file
# 由于指定了包名cobraDemo,且其不在默认的GOROOT或者GOPATH目录下,所以需要让GOPATH加上当前项目的源代码目录
❯ export GOPATH=$GOPATH:/Users/dongwm/strconv.code/cli
❯ 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.

这样可以看到默认的输出了,不过目前还没有子命令。代码架构是这样的(为了让结构清晰,去掉了一部分代码):

// main.go
package main

import "cobraDemo/cmd"

func main() {
  cmd.Execute()
}
// cmd/root.go
package cmd

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

var rootCmd = &cobra.Command{
  Use:   "cobraDemo",
  Short: "A brief description of your application",
  Long: `A longer description...`
}

func init() {
  cobra.OnInitialize(initConfig)  // viper是cobra集成的配置文件读取的库,以后我们会专门说

  rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobraDemo.yaml)") // 添加`--config`参数。它是全局的~


  // Cobra also supports local flags, which will only run
  // when this action is called directly.
  rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // 添加一个布尔值的参数toggle, 在命令行可以用`-t`或者`--toggle`指定
}

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

接着我们添加一个子命令new:

❯ cobra add new
new created at /Users/dongwm/strconv.code/cli/src/cobraDemo
❯ tree .
.
├── LICENSE
├── cmd
│   ├── new.go
│   └── root.go
└── main.go

1 directory, 4 files

可以看到这样就多了 cmd/new.go 文件,去掉注释和空行如下:

package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)
var newCmd = &cobra.Command{
	Use:   "new",
	Short: "A brief description of your command",
	Long: `A longer description...`,
    Run: func(cmd *cobra.Command, args []string) {
           fmt.Println("new called")
    },
}
func init() {
	rootCmd.AddCommand(newCmd)
}

添加命令就是在init函数中执行 rootCmd.AddCommand ,而newCmd中比之前看的rootCmd多了一个Run成员,指定执行子命令时要做什么,这样整个命令应用就可以用了。为了让输出更简洁我把Command里面的Long部分改短一些,效果是这样的:

❯ go run main.go --help
A longer description...

Usage:
  cobraDemo [command]

Available Commands:
  help        Help about any command
  new         A brief description of your command

Flags:
      --config string   config file (default is $HOME/.cobraDemo.yaml)
  -h, --help            help for cobraDemo
  -t, --toggle          Help message for toggle

Use "cobraDemo [command] --help" for more information about a command.
❯ go run main.go new --help
A longer description...

Usage:
  cobraDemo new [flags]

Flags:
  -h, --help   help for new

Global Flags:
      --config string   config file (default is $HOME/.cobraDemo.yaml)
❯ go run main.go new
new called

可以感受到:

--config
-h

当然我们可以直接在程序里面集成cobra,就像上面做的代码解析,无非就是用 cobra.Command 创建一个rootCmd,用 rootCmd.AddCommand 添加子命令,在对应代码文件的init函数中定义flag。cobra支持2种定义参数的方法:

  1. newCmd.PersistentFlags。全局参数,根命令和子命令下都包含了这个参数项」。可以说定义一次,全局可用。
  2. newCmd.Flags。本地参数,只对当前命令下生效,其他子命令下不会继承。

在new子命令下体验定义flag的方法(都用 newCmd.Flags ):

package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var (
	n, a int
	s string
)

var newCmd = &cobra.Command{
	Use:   "new",
	Short: "A brief description of your command",
	Long: `A longer description...`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("n is %v\n", n)
		fmt.Printf("a is %v\n", a)
		fmt.Printf("s is %v\n", s)
		q, _ := cmd.Flags().GetBool("q")
		fmt.Printf("q is %v\n", q)
		bbb, _ := cmd.Flags().GetInt("bbb")
		fmt.Printf("bbb is %v\n", bbb)
	},
}

func init() {
	rootCmd.AddCommand(newCmd)

	newCmd.Flags().IntVar(&n, "intf", 0, "Set Int")
	newCmd.Flags().StringVar(&s, "stringf", "sss", "Set String")
	newCmd.Flags().Bool("q", false, "Set Bool")

	newCmd.Flags().IntVarP(&a, "aaa", "a", 1, "Set A")
	newCmd.Flags().IntP("bbb", "b", -1, "Set B")
}

和Golang语言内置的flag库用法很像,cobra也支持XXVar这样的方法(XX可以使String、Int、Bool等),第一个参数用的都是变量内存地址,这样在命令行下传递对应的参数就会改变量的值。另外也支持XXP和XXVarP这样的写法:XXP/XXVarP这样的不仅支持短参数也支持长参数,而XXVar只支持长参数;XXP和XXVarP这2种写法的区别主要是看第一个参数,XXP这样的方式中不需要预先定义变量,把内存地址传进来。

怎么获得解析后的参数项和值呢?就看newCmd的Run,用 fmt.Printf 分别打印了n、a、s(因为定义变量,可以直接找到),而q和bbb这2个参数是隐式的,需要用 cmd.Flags().GetXX ,关键看参数的值的类型,如q是一个布尔值,所以用 cmd.Flags().GetBool("q") 就能获取对应参数的值了,我们体验下:

❯ go run main.go new -h
A longer description...

Usage:
  cobraDemo new [flags]

Flags:
  -a, --aaa int          Set A (default 1)
  -b, --bbb int          Set B (default -1)
  -h, --help             help for new
      --intf int         Set Int
      --q                Set Bool
      --stringf string   Set String (default "sss")

Global Flags:
      --config string   config file (default is $HOME/.cobraDemo.yaml)
❯ go run main.go new
n is 0
a is 1
s is sss
q is false
bbb is -1
❯ go run main.go new -a 1 --bbb=2 --intf 3 --stringf="abc" --q
n is 3
a is 1
s is abc
q is true
bbb is 2

这个库的体验就先到这里了,更多的功能和用法请看官方文档。

urfave/cli

urfave/cli 是一个简单、快速的命令行程序开发框架,使用它的知名项目包含Gogs、Drone、Gitea等。

首先安装它:

❯ go get github.com/urfave/cli

先了解下 urfave/cli 中怎么定义Flag

package main

import (
	"fmt"
	"github.com/urfave/cli"
	"log"
	"os"
)

var (
	flags []cli.Flag
	host  string
	port  int
)

func init() {
	flags = []cli.Flag{
		cli.StringFlag{
			Name:        "t, host, ip-address",
			Value:       "127.0.0.1",
			Usage:       "Server host",
			Destination: &host,
		},
		cli.IntFlag{
			Name:        "p, port",
			Value:       8000,
			Usage:       "Server port",
			Destination: &port,
		},
	}
}
func main() {
	app := cli.NewApp()
	app.Name = "AppName"
	app.Usage = "Application Usage"
	app.HideVersion = true
	app.Flags = flags
	app.Action = Action

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

func Action(c *cli.Context) error {
	if c.Int("port") < 1024 {
		cli.ShowAppHelp(c)
		return cli.NewExitError("Ports below 1024 is not available", 2)
	}

    fmt.Printf("Listening at: http://%s:%d", host, c.Int("port"))
	return nil
}

flags是cli.Flag结构列表,包含主机名和端口2项Flag,每项都指定了Name(可以有多个,用逗号隔开)、Value(默认值)、Usage(帮助信息)、Destination(对应变量的内存地址,因为在命令行内会修改变量)。要注意Flag需要使用对应类型XXFlag(如cli.StringFlag、cli.IntFlag)

在main函数中首先用 cli.NewApp() 创建一个新的app,Name、Usage等项的值在帮助输出中都有体现,HideVersion是用于隐藏版本号,而Action表示「行为」,相当于Parse参数后的最终输出,在这里我把Action独立成了一个函数,里面的逻辑:

  1. 如果端口号的值小于1024会先当因帮助信息,再抛错
  2. 打印主机和端口的值,这里用了2种方法:hots直接用的是变量;而port用了 c.Int("port") 的方式获取,要注意对应参数项的类型,如果获得host的值需要用 c.String("host")

我们试一下:

❯ go run use_flags.go
Listening at: http://127.0.0.1:8000
❯ go run use_flags.go -h
NAME:
   AppName - Application Usage

USAGE:
   use_flags [global options] command [command options] [arguments...]

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   -t value, --host value, --ip-address value  Server host (default: "127.0.0.1")
   -p value, --port value                      Server port (default: 8000)
   --help, -h                                  show help

❯ go run use_flags.go -t 172.16.0.1 --port 8080
Listening at: http://172.16.0.1:8080

❯ go run use_flags.go -t 172.16.0.1 --port 21
NAME:
   AppName - Application Usage

USAGE:
   use_flags [global options] command [command options] [arguments...]

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   -t value, --host value, --ip-address value  Server host (default: "127.0.0.1")
   -p value, --port value                      Server port (default: 8000)
   --help, -h                                  show help
Ports below 1024 is not available
exit status 2

可以看到不同参数下执行效果和输出是不一样的。

命令行参数另外一个重要场景是子命令(Subcommand),基于官网文档看一下例子:

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/urfave/cli"
)

func main() {
	app := cli.NewApp()

	app.Commands = []cli.Command{
		{
			Name:     "add",
			Aliases:  []string{"a"},
			Usage:    "add a task to the list",
			Category: "Add",
			Action: func(c *cli.Context) error {
				fmt.Println("added task: ", c.Args().First())
				return nil
			},
		},
		{
			Name:     "template",
			Aliases:  []string{"t", "tmpl"},
			Usage:    "options for task templates",
			Category: "Template",
			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
					},
				},
			},
		},
	}

	app.Name = "AppName"
	app.Usage = "application usage"
	app.Description = "application description"  // 描述
	app.Version = "1.0.1" // 版本

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

urfave/cli 支持分组子命令,这个例子中包含2个子命令,分别是add(在Add组)和template(在Template组),其中template子命令下也有2个子命令add/remove。子命令支持别名,需要放在Aliases里面,分组需要放在Category里面。

另外这次设置了描述和版本号,会在帮助信息中展示出来,而前一个例子隐藏版本部分信息了。

子命令的行为由Action决定,它的值是一个函数,可以看代码了解。

那么感受一下:

❯ go run use_subcommand.go
NAME:
   AppName - application usage

USAGE:
   use_subcommand [global options] command [command options] [arguments...]

VERSION:
   1.0.1

DESCRIPTION:
   application description

COMMANDS:
     help, h  Shows a list of commands or help for one command

   Add:
     add, a  add a task to the list

   Template:
     template, t, tmpl  options for task templates

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version

❯ go run use_subcommand.go add abc
added task:  abc

❯ go run use_subcommand.go template --help
NAME:
   AppName template - options for task templates

USAGE:
   AppName template command [command options] [arguments...]

COMMANDS:
     add     add a new template
     remove  remove an existing template

OPTIONS:
   --help, -h  show help

❯ go run use_subcommand.go template add bcd
new task template:  bcd

❯ go run use_subcommand.go template remove cde
removed task template:  cde

❯ go run use_subcommand.go --version
AppName version 1.0.1

代码地址

完整代码可以在 这个地址 找到。

延伸阅读


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK