go的CLI程序开发

go语言学习笔记:CLI程序开发

CLI(command-line interface)就是命令行界面,我们在linux命令行中输入的一个个指令都是CLI程序,典型如tar命令,一般使用go开发一个命令行程序有以下几种方法

Arguments

这个算是最基本的方法,但也是最繁琐的。主要借助os.Args,查看源码,介绍如下

// Args hold the command-line arguments, starting with the program name.
var Args []string

这里告诉我们这个数组切片保存了命令行的参数,且第一个参数程序名。这里需要注意的一点是,就算一个参数不附加,os.Args这个数组也是有一个值的,就是当前的程序,所以我们在通过len(os.Args)判断是否有额外参数时,len(os.Args)>1才能说明有额外参数

既然我们可以通过这个数组获取所有参数,那么就可以通过一系列参数判断,开发出一个命令行程序,但无疑是很繁琐的,所以go标准库中提供了一个简单的库来帮助我们

flag

我们先引入一个小例子

func main() {
	dir := flag.String("dir","/home/user","data directory")
	flag.Parse()
	fmt.Println(*dir)
}

之后再命令行输入下面命令去运行,

go run example.go   
输出/home/user

go run example.go -dir /etc/opt     
输出/etc/opt

go run in.go -h
输出:
Usage of .../example.exe
    -dir string
        data directory (default "/home/user")
        
go run example.go -dirs /etc
输出:
flag provided but not defined: -dirs
Usage of .../example.exe
    -dir string
        data directory (default "/home/user")

可以看出这已经是一个比较完善的命令程序,我们指定了参数名:dir,默认值:”/home/user”,以及参数解释:”data directory”。之后flag包自动帮我们解析参数,并附带了-h帮助信息,以及未定义参数的提示信息

通过上面的例子基本可以了解flag大体使用方法,首先定义一些参数,之后flag.Parse()进行解析,最后使用解析到的数据即可,关于Parse()源码如下

func Parse() {
	CommandLine.Parse(os.Args[1:])
}

可见也是用的os.Args[1:]作为输入,只不过这个包帮我们做好了匹配及错误处理而已。接下来详细学习一下用法

flag定义

基本上分三大类:

  1. *flag.Xxx(name,value,usage) Xxx Xxx表示相应的数据类型,如Bool,Float64,Int,String等等,参数都是三个:名称,默认值,用法介绍。返回值1是相应类型的一个指针变量。例子如下:
dir := flag.String("dir","/home/user","data directory")
  1. flag.XxxVar(p,name,value,usage) Xxx也是表示相应的数据类型,和上面那个一样,区别是多了一个参数,需要传入一个变量的引用,然后解析时会把值赋给这个变量,没有返回值,例子如下
var dir string
flag.StringVar(&dir,"dir","/home/user","data directory")
  1. flag.Var(value,name,usage) 当包中预定义的数据类型不能满足要求时,就需要这个方法了,第一个参数是一个引用,其类是实现flag.Value 接口,剩下的两个参数意义和上边的一样。先看一下这个接口
type Value interface {
	String() string
	Set(string) error
}

基本上就是要定义存取方法,只不过存取的值都必须是string类型,举一个简单的例子

type student struct {
	name string
	age int64
}

func (s *student) String()string{
	return s.name+string(s.age)
}

func (s *student) Set(str string)error{
	slice:=strings.Split(str,",")
	if len(slice)!=2 {
		return errors.New("bad format")
	}
	i,err:=strconv.ParseInt(slice[1],10,64)
	if err!=nil {
		return err
	}
	s.name = slice[0]
	s.age = i
	return nil
}

func main() {
	var dir student
	flag.Var(&dir,"stu","student info")
	flag.Parse()
	fmt.Println(dir.name,"+++",dir.age)
}

//用法
//go run example.go -stu wang,21

flag格式

一般形式如下:

-flag
-flag = x
-flag x //仅适用于非boolean类型flag

其中-和–都是允许的

flag解析会在遇到第一个非flag参数或单独的–之后停止,例

func main() {
	n := flag.Int("n",0,"number")
	flag.Parse()
	fmt.Println(*n)
	fmt.Println(flag.NArg())
}

下面的命令都会由于提前停止解析得不到所要的值

go run example.go 45 -n 1 //flag.NArg()会返回3
go run example.go - -n 1 //flag.NArg()会返回3
go run example.go -- -n 1 //flag.NArg()会返回2,--被当做终止符

其他方法

  1. Arg(i int)string , Args()[]string , NArg()int , NFlag()int

    Arg(i int)返回的是在被flag解析后,第i个剩余的参数,没有的话返回空字符串

    Args()返回的是被flag解析后,剩余参数数组切片

    NArg()返回的是被flag解析后,剩余参数的个数

    NFlag()返回的是接收到的flag参数个数(并不是定义的个数)

n := flag.Int("n",0,"number")
flag.Parse()
fmt.Println(n)

fmt.Println(flag.Arg(1))//输入>go run example.go -n 1 454 555,返回555
                        //输入>go run example.go -n 1 454 ,返回空
                        
fmt.Println(flag.Args())//输入>go run example.go -n 1 454 555,返回[454,555]

fmt.Println(flag.NArg())//输入>go run example.go -n 1 454 555,返回2

fmt.Println(flag.NFlag())//输入>go run example.go -n 1 454 555,返回1
                        //输入>go run example.go 454 555,返回0
  1. flag.Parsed()bool

    判断参数是否解析过

  2. Set(name, value string) error

    给指定的flag赋值

  3. flag.Usage

    库里已经帮我们自动生成了一套帮助信息,可以使用-h或-help查看,另外我们也可以自己定制,重写Usage例

       flag.Usage = func() {
    	fmt.Println("hello world")
    }

    另外我们也可以看一下源码,原来的帮助信息是怎么生成的

var Usage = func() {
	fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n", os.Args[0])
	PrintDefaults()
}

可见,先是打印了os.Args[0],也就是程序信息,之后调用了PrintDefaults(),打印了所有flag的信息

urfave/cli

其实官方给出的flag已能满足大部分要求,如果有更复杂的需要,可以借助这个强大的第三方包urfave/cli

安装与导包

go get github.com/urfave/cli
import (
  "gopkg.in/urfave/cli.v1"
)

简单使用

该包的github主页有详细的使用说明,这里就不一一赘述了,只简单说一下常用的使用流程

  1. 实例化App对象
    app := cli.NewApp()
  2. 配置App信息
    //这个包可以配置丰富的App描述信息,如名称,版本号,作者,版权信息,程序简介等
    app.Name = "HelloWorld"
    app.Version = "1.0.0"
    app.Authors = []cli.Author{
    	cli.Author{
    		Name:  "Tom",
    		Email: "Tom@example.com",
    	},
    }
    app.Copyright = "(c) 1999 Serious Enterprise"
    app.Usage = "greet"
    app.UsageText = "Example program"
    //输入go run example.go -h后显示如下
    /*
    NAME:
       HelloWorld - greet
    
    USAGE:
       Example program
    
    VERSION:
       1.0.0
    
    AUTHOR:
       Tom <Tom@example.com>
    
    COMMANDS:
         help, h  Shows a list of commands or help for one command
    
    GLOBAL OPTIONS:
       --help, -h     show help
       --version, -v  print the version
    
    COPYRIGHT:
       (c) 1999 Serious Enterprise
    */
  3. 定义程序执行逻辑

这里是指程序运行的逻辑。主要是配置app.Action,例:

app.Action = func(c *cli.Context) {
		fmt.Println("hello world")
	}
//go run example.go   
//输出hello world

当然我们也可以不在这里定义主程序逻辑,在这里定义的一个好处是cli.Context携带了许多有用的上下文环境变量供我们使用,后面可以见到。

app.Action是执行程序时执行的逻辑,我们也可以定义在程序执行前后所要插入的逻辑,定义app.Before与app.After即可,例

func main() {
	app := cli.NewApp()
	app.Before = func(context *cli.Context) error {
		fmt.Println("before hello world")
		return nil;
	}
	app.Action = func(c *cli.Context) {
		fmt.Println("hello world")
	}
	app.After = func(context *cli.Context) error {
		fmt.Println("after hello world")
		return nil;
	}
	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}
//执行go run example.go
/* 输出:
before hello world
hello world
after hello world
*/

注意:如果app.Before返回的error不为空,app.Action的内容将不会执行,而不管app.Action与app.Before中是否有错误发生,app.After的内容都会执行,app.After可用于收尾工作。
4. 定义flag

这里的flag概念和上文中go的标准包中flag类似,直接看一个例子:

func main() {
	app := cli.NewApp()
	app.Flags = []cli.Flag{
		cli.StringFlag{
			Name:"path",
			Value:"/home/",
			Usage:"setting path",
		},
	}
	app.Action = func(c *cli.Context) {
		fmt.Println(c.String("path"))
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal("aaa",err)
	}
}
//输入go run example.go -path /home/base
//输出:/home/base
//输入go run example.go
//输出:/home/

定义起来很简单,关键几个要素就是Name和Value,取值时使用cli.Context提供的对应取值方法即可。包内预定义了许多种类型的flag,基本涵盖了所有基本类型,详见这里

另外在取值时,除了调用如c.Int(),c.String()之类的方法,还可以在定义flag时直接绑定到某些变量上,如:

var age int
cli.IntFlag{
	Name:"age",
	Value:100,
	Destination:&age,
}

另外,还可以配置flag的简写或别名,只需在定义Name时定义多个名称,中间用逗号隔开即可,例:

cli.IntFlag{
	Name:"age,a,ege",
	Value:100,
	Destination:&age,
},
//-age -a -ege 都是有效的
  1. 配置子命令

如git push …中push就是一个子命令,这个包为我们提供了便捷定义子命令及其动作的方法

app.Commands = []cli.Command{
		{
			Name:    "push",
			Aliases: []string{"p"},
			Usage:   "push a file to the server",
			Action: func(c *cli.Context) error {
				fmt.Println("push flie: ", c.Args().First())//c.Args().First()取命令后的第一个参数
				return nil
			},
		},
	}
//执行go run example.go push test.txt
//输出:push flie:  test.txt

用法很简单,指定命名名,别名用法,以及相应动作即可。另外子命令可以像它的一个程序一样,有自己flag,Before,After,甚至是自己的子命令,使用Subcommands定义

注意,如果即定义了app的action,又定义了子命令的action,同一时间只能执行一个,如调用子命令时,app的action就不会执行

  1. 启动程序

所有配置都配置完成后,就需要启动程序,不然是不会生效的

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

最后给出一个详细例子,这是给出的,基本上涵盖了所有配置要点:例子