go-cli
本文主要是介绍go Flag包和cobra库的使用。
clis是什么?
cli全程为(Command line interfaces,命令行接口),主要用于命令行操作。因为go的特殊性(不需要安装任何的库和环境,直接会编译好目标系统的二进制文件),很适合用来做这个功能,比如Java就需要安装环境,比较麻烦,Java里面也有比较好的框架(Spring-Shell)。
flag包
它是go的标准库提供的一个包,实现了命令行标志的解析功能。
flag包中命令格式如下:
命令 标志 标志对应的值 参数
example如下:
kla(命令) -f /opt/module/words/config/dev.yaml(标志) param1 param2(参数)
比如命令行中规定-file来指定文件,flag包会帮助我们来做解析。将-f指定的参数解析出来,而不需要我们来做解析。此外还可以获取到传递的参数。
flag相关
Code:
package main
import "flag"
func main() {
s := flag.String("file", "", "config path")
flag.Parse()
println(*s)
}
下面来介绍一下flag包的一些使用。
定义步骤
在使用flag的时候,一般来说,有两个步骤
- 定义flag。
- 调用
flag.parse()解析。 - 正常使用。
声明flag
flag包提供了下面几个类型的flag声明方法,
// 需要注意方法的返回值,返回的是指针
flag.String("string", "", "string")
flag.Uint64("uint64",0,"uint64")
flag.Uint("uint",0,"uint")
flag.Float64("Float64",0,"Float64")
flag.Duration("Duration",time.Hour,"Duration")
flag.TextVar(nil,"test",nil)
此外,几个方法,他们和上述方法的差别是可以接收指针,比如:
// 返回的是指针
var nFlag = flag.Int("n", 1234, "help message for flag n")
// Var结尾的方法可以接收指针,没有返回值
var flagvar int
flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
还可以给声明的flag绑定函数
var ip string
// 指定函数,自定义解析
flag.Func("ip","ip", func(s string) error {
ip1,err := netip.ParseAddr(s)
ip = ip1.String()
return err
})
flag.Parse()
println(ip)
还可以自定义类型来做转换
需要自己定义参数类型,该类型必须实现Value接口。
可以点看上述的那几个方法比如,StringVar看,还是自定义了一个stringValue类型,并且实现了Value接口,底下调用的是Var方法
flag.Var(nil,"-c","自定义类型,需要实现Value接口")
Value接口如下:
type Value interface {
String() string
// 将传递进来的参数,做自定义的处理
Set(string) error
}
命令行传递flag
flag包允许下面的几个传递参数的方法
-flag
--flag // double dashes are also permitted
-flag=x
-flag x // non-boolean flags only
Integer flags accept 1234, 0664, 0x1234 and may be negative. Boolean flags may be:
int的flag可以接收1234, 0664, 0x1234和负数,bool类型的参数可以是下面中的一个
1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False
Duration的标志可以接收任何被time.ParseDuration可以解析的字符串
访问和查找Flag
有两个访问的方法
// 访问所有的已经被设置值的flag
flag.Visit(func(f *flag.Flag) {
println(f.Name)
})
// 访问所有的的flag,包括已被设置值和没有设置值的flag
flag.VisitAll(func(f *flag.Flag) {
println(f.Name)
})
可以通过lookup来查找Flag
// 传递标志的名字返回Flag的指针对象,通过flag可以获取到Flag的名字,Value,defaultValue
func Lookup(name string) *Flag
arg相关
arg的函数如下:
代码:
package main
import (
"flag"
"fmt"
)
func main() {
var configPath string
flag.StringVar(&configPath,"f", "/opt/module/config/dev.yaml", "config path")
flag.Parse()
fmt.Printf("config path: %s \n",configPath)
// 获取所有的参数
fmt.Printf("args: %v \n",flag.Args())
// 按照数组下标来获取
fmt.Printf("arg: %s \n",flag.Arg(1))
// 参数数量
fmt.Printf("agr number: %d \n",flag.NArg())
}
flag包不仅仅只可以从终端输入做解析
需要清楚,flag包只是来做解析,将输入的数据(-f configPath param) 这样的数组做解析,flag包还支持不同的源,从这些源来做解析。
代码:
func main() {
// 自定义flagSet
// flag.ExitOnError异常处理枚举值
flagSet := flag.NewFlagSet("demo", flag.ExitOnError)
// 替换flag包中默认创建的FlagSet
flag.CommandLine = flagSet
// 正常的声明flag
s := flag.String("f", "name", "name")
// 将参数传递过去
err := flagSet.Parse([]string{"-f", "configPath","param1","param2"})
if err != nil {
panic(err)
}
println(*s)
println(flag.NArg())
}
下面解释一下flag包
- flag只是来做解析的。
- 抽象flag,flag只是一个标志,标志是放在FlagSet中的,所有的Flag的解析都是基于FlagSet的数据源开始的。
- flag默认会创建一个
CommandLine,他是一个FlagSet对象,它关联的数据源默认是从os.args[0]中读取数据的。
如果看flag.parse()方法,其实调用的是默认的CommandLine的Parse方法
func Parse() {
// 调用的的还是FlagSet的Parse的方法(os.args第一个参数是命令的名字,第二个开始就是参数了)
CommandLine.Parse(os.Args[1:])
}
关于Flag包就介绍到这里了,Flag包提供的功能对于Clis来说只能说是够用,但想要功能强大,还需要自己在下功夫。对于Clis有很多的专业的库来做。
看这个文档
下面介绍一下cobra库的使用。
cobra
简而言之,很好用的Clis的库。
创建go项目,下载库
go get -u github.com/spf13/cobra/cobra
有两种方式,一种是自己写,一种是通过cobra-cli来生成。
这里我采用的是 cobra-cli 来生成。
开始
安装cobra-cli
go install github.com/spf13/cobra-cli@latest
安装完成之后输入 cobra-cli可以看到一下内容就说明安装好了
初始化项目
先创建一个项目,写好mod之后,执行下面的命令
cobra-cli init .
它会初始化一个cobra的一个初始化项目。也可以执行cobra-cli init -h来查看帮助信息
初始化的项目在简单了,将上面的rootCmd中的Use修改为kla,并且给他增加一个子命令 start。
增加子命令
cobra-cli add start
在看项目结构
重新编译运行项目结果如下:
运行子命令
发现,比起上面的flag,这感觉已经很好了。从这个例子中可以看到,它会自动生成help信息,并且很丰富,支持父子命令和flag。
简介
cobra是一个cli的框架,利用他可以创建强大的现代化的CLI的应用程序并且可以生成命令文件,他包含一下特点
- 支持子命令和命令嵌套。
- 完全的遵从的POSIX的flag。
- 全局和局部的flag。
- 智能建议(app server... did you mean app server ?)。
- 自动生成help命令和flag(-h --help)
- 支持命令别名。
- .... 其余的可以看官网
概念:
三个概念,命令(command),参数(args),Flags
最好的语法模式读起来就像句子一样,遵从下面的语法模式
appname 动词 名词 --形容词 或 appname 命令 参数 --Flag
比如:
hugo server --port=1313
git clone URL --bare
etcdctl get age --discovery-srv=127.0.0.1:2379
从一个例子来详细的了解它的使用方式
例子引导
kla 有两个子命令,start和stop,并且在start和stop的时候可以支持一些flag kla start 支持的flag如下:
- f 配置文件的路径
- ip 主机的ip
- port 主机的port
- namespace 名称空间
- username
- password
还可以传递参数,并且要求如果指定了username就必须指定password kla stop 支持的flag如下:
- f 配置文件的路径
- retry-delay 重试间隔
ps:别在意这里的逻辑是否正确,这逻辑肯定是错误的,旨在说明情况
stop命令最少传递两个参数,并且start和stop都需要指定f
定义cmd和声明flag
在开始,需要定义rootCommand,作为根命令,之后声明的子命令,start,stop都是他的子命令,在每个子命令声明完自己的flag之后,在init函数中将其添加到rootCmd中,cobra推荐的文件结构如下::
上面例子写的代码如下:
main.go
func main() {
cmd.Execute()
}
root.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var (
rootCmd = &cobra.Command{
Use: "kla",
Short: "kla就是一个测试",
Long: `晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。
林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。`,
// 下面的注释是此命令关联的动作,此命令作为根,不需要动作,所有的动作都是在子命令上绑定的
// Run: func(cmd *cobra.Command, args []string) { },
}
configFilePath 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() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringVar(&configFilePath,"f","","configPath")
// 将一个持久性的flag表示为必填,持续的意思是在他的子命令里面都有这个flag
// flag有两种,本地和持久
// 1. 本地(local)只属于此命令
// 2. 持续(Persistent)属于此命令及其子命令
rootCmd.MarkPersistentFlagRequired("f")
}
start.go
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// startCmd represents the start command
var (
startCmd = &cobra.Command{
Use: "start", // 命令名字
Short: "start kla", // 短一点的描述
Long: `这就是一个用来测试的,start 命令可以启动kla,
一个尝尝的描述信息`, // 详细描述
Aliases: []string{"run"}, // 别名
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("启动kla,args:%v\n",args)
fmt.Printf("ip:%s\n",ip)
fmt.Printf("port:%d\n",port)
fmt.Printf("namespace:%s\n",namespace)
fmt.Printf("username:%s\n",username)
fmt.Printf("password:%s\n",password)
fmt.Printf("f:%s\n",configFilePath)
},
}
ip string
port int
namespace string
username string
password string
)
func init() {
rootCmd.AddCommand(startCmd)
startCmd.Flags().StringVar(&ip,"ip","127.0.0.1","ip地址")
startCmd.Flags().IntVarP(&port,"port","p",8000,"port")
startCmd.Flags().StringVarP(&namespace,"namespace","n","default","namespace")
startCmd.Flags().StringVar(&username,"username","admin","username")
startCmd.Flags().StringVar(&password,"password","admin","password")
startCmd.MarkFlagsRequiredTogether("username","password") // 表示有userName就必须得有password,他俩绑定在一起了
}
stop.go
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
)
// stopCmd represents the stop command
var (
stopCmd = &cobra.Command{
Use: "stop",
Short: "stop kla",
Long: `这是一个很详细的描述
用他可以来描述stop命令的详细试用`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("stop called,args:%v\n", args)
fmt.Printf("retryDelay:%v\n", retryDelay)
fmt.Printf("f:%s\n", configFilePath)
},
// 参数校验,最少需要两个参数
Args: cobra.MinimumNArgs(2),
}
retryDelay time.Duration
)
func init() {
rootCmd.AddCommand(stopCmd)
stopCmd.Flags().DurationVarP(&retryDelay,"retry-delay","d",time.Second*10,"重试间隔时间")
}
编译查看
在命令行运行,编译
go build -o kla
运行kla
-> % ./kla -h
晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。
林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。
Usage:
kla [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
// 上面两个是自动帮我们添加的
// 下面两个是我们自己的子命令
start start kla
stop stop kla
Flags:
--f string configPath
-h, --help help for kla
Use "kla [command] --help" for more information about a command.
查看start命令的帮助信息
-> % ./kla start -h
这就是一个用来测试的,start 命令可以启动kla,
一个尝尝的描述信息
Usage:
kla start [flags]
Aliases:
start, run
Flags:
-h, --help help for start
--ip string ip地址 (default "127.0.0.1")
-n, --namespace string namespace (default "default")
--password string password (default "admin")
-p, --port int port (default 8000)
--username string username (default "admin")
Global Flags:
--f string configPath
查看stop的帮助信息
-> % ./kla stop -h
这是一个很详细的描述
用他可以来描述stop命令的详细试用
Usage:
kla stop [flags]
Flags:
-h, --help help for stop
-d, --retry-delay duration 重试间隔时间 (default 10s)
Global Flags:
--f string configPath
运行
start
-
没有指定f必传参数
-> % ./kla start param1 param2 Error: required flag(s) "f" not set Usage: kla start [flags] Aliases: start, run Flags: -h, --help help for start --ip string ip地址 (default "127.0.0.1") -n, --namespace string namespace (default "default") --password string password (default "admin") -p, --port int port (default 8000) --username string username (default "admin") Global Flags: --f string configPath -
指定f必传参数,正常运行
-> % ./kla start param1 param2 --f configPath1 启动kla,args:[param1 param2] ip:127.0.0.1 port:8000 namespace:default username:admin password:admin f:configPath1 -
指定username,没password
-> % ./kla start --f configpath1 --username lalal Error: if any flags in the group [username password] are set they must all be set; missing [password] Usage: kla start [flags] Aliases: start, run Flags: -h, --help help for start --ip string ip地址 (default "127.0.0.1") -n, --namespace string namespace (default "default") --password string password (default "admin") -p, --port int port (default 8000) --username string username (default "admin") Global Flags: --f string configPath
stop
- 没指定f
-> % ./kla stop param1 param2
Error: required flag(s) "f" not set
Usage:
kla stop [flags]
Flags:
-h, --help help for stop
-d, --retry-delay duration 重试间隔时间 (default 10s)
Global Flags:
--f string configPath
- 参数个数不够
-> % ./kla stop --f configpath1
Error: requires at least 2 arg(s), only received 0
Usage:
kla stop [flags]
Flags:
-h, --help help for stop
-d, --retry-delay duration 重试间隔时间 (default 10s)
Global Flags:
--f string configPath
- 正常运行
-> % ./kla stop param1 param2 --f configpath1 -d 20s
stop called,args:[param1 param2]
retryDelay:20s
f:configpath1
智能提示:
-> % ./kla stap
Error: unknown command "stap" for "kla"
Did you mean this?
start
stop
Run 'kla --help' for usage.
自定义help信息
在之前的基础上增加一个子命令:myhelp
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// myhelpCmd represents the myhelp command
var myhelpCmd = &cobra.Command{
Use: "myhelp",
Short: "自定义help",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("myhelp called")
},
}
func init() {
rootCmd.AddCommand(myhelpCmd)
//设置自己的help信息
myhelpCmd.SetHelpTemplate("这是我自己的help")
}
需要注意:
因为在根命令上声明了必要flag,所以在cobra自动添加的命令也会有,并且还是必须的。也就是说,在运行上面的help,completion的时候都需要指定f
详细的讲解
上面的例子已经包含了他的日常使用,下面对一些方法说明一下:
Flag声明和使用
有两种flag
- local(本地)只属于这个command
- Persistent(持续)属于这个command和它子命令
不同的类型下面各有三种方式
startCmd.Flags().String("ip","127.0.0.1","ip地址") // 返回指针
startCmd.Flags().StringVar(&ip,"ip","127.0.0.1","ip地址") // flag 是 --开头,比如这里就是 --ip
startCmd.Flags().StringVarP(&ip,"ip","p","127.0.0.1","ip地址") // 支持短名, 比如这里 可以是--ip,也可以是 -p
lookup := startCmd.Flags().Lookup("ip") // 可以通过lookUp查找,需要注意的是本地的在 flags里面查找,持续性的在PersistentFlags()里面查找
flag必填
在用flag之前得先分清楚是local还是persistent
command.MarkFlagRequired("ip") // local
command.MarkPersistentFlagRequired("f") // persistent
a和b flag绑定在一起,必填
a和b flag是绑定在一起的,指定a就必须指定b
startCmd.MarkFlagsRequiredTogether()
参数验证
在声明Command的时候支持对args的校验
cobra.Command的Args属性
它提供了几个校验函数:
NoArgs- the command will report an error if there are any positional args.ArbitraryArgs- the command will accept any args.OnlyValidArgs- the command will report an error if there are any positional args that are not in theValidArgsfield ofCommand.MinimumNArgs(int)- the command will report an error if there are not at least N positional args.MaximumNArgs(int)- the command will report an error if there are more than N positional args.ExactArgs(int)- the command will report an error if there are not exactly N positional args.ExactValidArgs(int)= the command will report and error if there are not exactly N positional args OR if there are any positional args that are not in theValidArgsfield ofCommandRangeArgs(min, max)- the command will report an error if the number of args is not between the minimum and maximum number of expected args.
不满足可以自定义
startCmd = &cobra.Command{
Use: "start", // 命令名字
Short: "start kla", // 短一点的描述
Long: `这就是一个用来测试的,start 命令可以启动kla,
一个尝尝的描述信息`, // 详细描述
Aliases: []string{"run"}, // 别名
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("启动kla,args:%v\n",args)
fmt.Printf("ip:%s\n",ip)
fmt.Printf("port:%d\n",port)
fmt.Printf("namespace:%s\n",namespace)
fmt.Printf("username:%s\n",username)
fmt.Printf("password:%s\n",password)
fmt.Printf("f:%s\n",configFilePath)
},
Args: func(cmd *cobra.Command, args []string) error {
// 他提供的函数式一个必包
nArgsFunc := cobra.MinimumNArgs(1)
// 先利用他的来做校验
err := nArgsFunc(cmd, args)
if err != nil {
return err
}
// 这是我自定义的校验
if args[0] != "testParam1" {
return fmt.Errorf("Illegal param")
}
// 没有问题就直接返回nil
return nil
},
}
分组
支持在 -h(帮助输出)里面将命令分组
使用分组前,得先声明group,并将他添加到父命令中,并且在自己的command声明中,用GroupID指定所在的group名
代码:
-
在root.go中声明group
startGroup = &cobra.Group{ ID: "startGroup", Title: "开始命令", } stopGroup = &cobra.Group{ ID: "stopGroup", Title: "结束命令", } -
添加到rootCmd中
rootCmd.AddGroup(startGroup,stopGroup) -
在子命令中声明所在groupId
startCmd = &cobra.Command{ Use: "start", // 命令名字 Short: "start kla", // 短一点的描述 Long: `这就是一个用来测试的,start 命令可以启动kla, 一个尝尝的描述信息`, // 详细描述 Aliases: []string{"run"}, // 别名 GroupID: "startGroup", Run: func(cmd *cobra.Command, args []string) { fmt.Printf("启动kla,args:%v\n",args) fmt.Printf("ip:%s\n",ip) fmt.Printf("port:%d\n",port) fmt.Printf("namespace:%s\n",namespace) fmt.Printf("username:%s\n",username) fmt.Printf("password:%s\n",password) fmt.Printf("f:%s\n",configFilePath) }, -
重新编译,查看帮助信息即可
-
groupId是可以重复的,但是仅限于同一个父命令中
-
首先在start命令在增加一个子命令
/* Copyright © 2022 NAME HERE <EMAIL ADDRESS> */ package cmd import ( "fmt" "github.com/spf13/cobra" ) // substartCmd represents the substart command var substartCmd = &cobra.Command{ Use: "substart", Short: "A brief description of your command", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. 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.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("substart called") }, } func init() { startCmd.AddCommand(substartCmd) } -
将之前的的两个group添加到startCmd中,并且在subStartCmd中声明所属groupId
// 添加 startCmd.AddGroup(startGroup,stopGroup) // 声明 var substartCmd = &cobra.Command{ Use: "substart", Short: "A brief description of your command", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. 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.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("substart called") }, GroupID: "startGroup", } -
重新编译,查看start的帮助信息
-> % ./kla start -h 这就是一个用来测试的,start 命令可以启动kla, 一个尝尝的描述信息 Usage: kla start [flags] kla start [command] Aliases: start, run 开始命令 substart A brief description of your command 结束命令 Flags: -h, --help help for start --ip string ip地址 (default "127.0.0.1") -n, --namespace string namespace (default "default") --password string password (default "admin") --username string username (default "admin") Global Flags: --f string configPath Use "kla start [command] --help" for more information about a command.
-
到这,文章结束了。
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。