go-cli

647 阅读15分钟

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)
}

图片.png

下面来介绍一下flag包的一些使用。

定义步骤

在使用flag的时候,一般来说,有两个步骤

  1. 定义flag。
  2. 调用flag.parse()解析。
  3. 正常使用。

声明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())
}

图片.png

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包

  1. flag只是来做解析的。
  2. 抽象flag,flag只是一个标志,标志是放在FlagSet中的,所有的Flag的解析都是基于FlagSet的数据源开始的。
  3. 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有很多的专业的库来做。

看这个文档

go.dev/solutions/c…

下面介绍一下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可以看到一下内容就说明安装好了

图片.png

初始化项目

先创建一个项目,写好mod之后,执行下面的命令

cobra-cli init .

它会初始化一个cobra的一个初始化项目。也可以执行cobra-cli init -h来查看帮助信息

图片.png

初始化的项目在简单了,将上面的rootCmd中的Use修改为kla,并且给他增加一个子命令 start。

增加子命令

cobra-cli add start

在看项目结构

图片.png

重新编译运行项目结果如下:

图片.png

运行子命令

图片.png

发现,比起上面的flag,这感觉已经很好了。从这个例子中可以看到,它会自动生成help信息,并且很丰富,支持父子命令和flag。

简介

cobra是一个cli的框架,利用他可以创建强大的现代化的CLI的应用程序并且可以生成命令文件,他包含一下特点

  1. 支持子命令和命令嵌套。
  2. 完全的遵从的POSIX的flag。
  3. 全局和局部的flag。
  4. 智能建议(app server... did you mean app server ?)。
  5. 自动生成help命令和flag(-h --help)
  6. 支持命令别名。
  7. .... 其余的可以看官网

概念:

三个概念,命令(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推荐的文件结构如下::

图片.png

上面例子写的代码如下:

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
  1. 没有指定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
    
  2. 指定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
    
  3. 指定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
  1. 没指定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
  1. 参数个数不够
-> % ./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
  1. 正常运行
-> % ./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

myhelp.go

/*
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

  1. local(本地)只属于这个command
  2. 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.CommandArgs属性

图片.png

它提供了几个校验函数:

  • 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 the ValidArgs field of Command.
  • 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 the ValidArgs field of Command
  • RangeArgs(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(帮助输出)里面将命令分组

图片.png

使用分组前,得先声明group,并将他添加到父命令中,并且在自己的command声明中,用GroupID指定所在的group名

代码:

  1. 在root.go中声明group

    	startGroup = &cobra.Group{
    		ID:    "startGroup",
    		Title: "开始命令",
    	}
    
    	stopGroup = &cobra.Group{
    		ID:    "stopGroup",
    		Title: "结束命令",
    	}
    
  2. 添加到rootCmd中

    	rootCmd.AddGroup(startGroup,stopGroup)
    
  3. 在子命令中声明所在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)
    	},
    
  4. 重新编译,查看帮助信息即可

  5. groupId是可以重复的,但是仅限于同一个父命令中

    1. 首先在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)
      }
      
      
    2. 将之前的的两个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",
      }
      
    3. 重新编译,查看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.
      

到这,文章结束了。


关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。