Golang第三方命令行工具 - spf13/cobra和urfave/cli
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.
虽然Go语言标准库里面有flag包可以作为命令行解析工具,但是flag包的用法有限,举例几个场景:
-
子命令。flag虽然也可以支持子命令,如
hugo server
,但是写起来非常麻烦。对于三级甚至更多级别的子命令更不支持。 -
–开头参数。flag只支持带一个减号的参数,例如
hugo --debug
这种支持很有限。 -
同时支持1/2个开头的参数。例如hugo,支持
-h
和--help
,都是显示帮助信息。
所以在实际项目中可以引入第三方命令行工具更好的支持这些场景。目前Go语言世界中最流行的工具库包含 spf13/cobra
和 urfave/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种定义参数的方法:
- newCmd.PersistentFlags。全局参数,根命令和子命令下都包含了这个参数项」。可以说定义一次,全局可用。
- 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独立成了一个函数,里面的逻辑:
- 如果端口号的值小于1024会先当因帮助信息,再抛错
-
打印主机和端口的值,这里用了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
代码地址
完整代码可以在 这个地址 找到。
延伸阅读
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK