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定义
基本上分三大类:
- *flag.Xxx(name,value,usage) Xxx Xxx表示相应的数据类型,如Bool,Float64,Int,String等等,参数都是三个:名称,默认值,用法介绍。返回值1是相应类型的一个指针变量。例子如下:
dir := flag.String("dir","/home/user","data directory")
- flag.XxxVar(p,name,value,usage) Xxx也是表示相应的数据类型,和上面那个一样,区别是多了一个参数,需要传入一个变量的引用,然后解析时会把值赋给这个变量,没有返回值,例子如下
var dir string
flag.StringVar(&dir,"dir","/home/user","data directory")
- 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,--被当做终止符
其他方法
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
flag.Parsed()bool
判断参数是否解析过
Set(name, value string) error
给指定的flag赋值
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主页有详细的使用说明,这里就不一一赘述了,只简单说一下常用的使用流程
- 实例化App对象
app := cli.NewApp()
- 配置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 */
- 定义程序执行逻辑
这里是指程序运行的逻辑。主要是配置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 都是有效的
- 配置子命令
如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就不会执行
- 启动程序
所有配置都配置完成后,就需要启动程序,不然是不会生效的
err := app.Run(os.Args)
if err != nil {
log.Fatal("aaa",err)
}
最后给出一个详细例子,这是给出的,基本上涵盖了所有配置要点:例子