命令行解析——flag
在介绍 Viper 库的那一篇文章中我们有提到过 viper 在设置键值时的优先级,依次是 调用Set
显示设置的 > 命令行选项 > 环境变量 > 配置文件 > 默认值。
在实际项目开发中一般不会直接 Set
来设置,因为要使用的配置现象过多。因此,在考虑直接读取配置文件之前,我们要先做一件事,那就是解析命令行,看看有没有相应的键值设置。
Go语言内置的flag
包实现了命令行参数的解析,flag
包使得开发命令行工具更为简单。
如何获取命令行参数?
如果只是简单的想要获取命令行参数,可以直接使用 os.Args
来获取命令行参数。
1 | package main |
将上面的代码执行go build -o "args_demo"
编译之后,执行:
1 | $ ./args_demo a b c d |
os.Args
是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称。
Flag
flag
用于解析命令行选项。
命令行选项在实际开发中很常用,特别是在写工具的时候。
- 指定配置文件的路径,如
redis-server ./redis.conf
以当前目录下的配置文件redis.conf
启动 Redis 服务器; - 自定义某些参数,如
python -m SimpleHTTPServer 8080
启动一个 HTTP 服务器,监听 8080 端口。如果不指定,则默认监听 8000 端口。
基本使用
flag包支持的命令行参数类型有bool
、int
、int64
、uint
、uint64
、float
float64
、string
、duration
。
flag参数 | 有效值 |
---|---|
字符串flag | 合法字符串 |
整数flag | 1234、0664、0x1234等类型,也可以是负数。 |
浮点数flag | 合法浮点数 |
bool类型flag | 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。 |
时间段flag | 任何合法的时间段字符串。如”300ms”、”-1.5h”、“2h45m”。合法的单位有”ns”、“us” /“µs”、“ms”、“s”、“m”、“h”。 |
flag.Type()
基本格式如下:
flag.Type(flag名, 默认值, 帮助信息)*Type
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
1 | name := flag.String("name", "张三", "姓名") |
需要注意的是,此时name
、age
、married
、delay
均为对应类型的指针。
flag.TypeVar()
基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
1 | var name string |
flag.Parse()
通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()
来对命令行参数进行解析。
支持的命令行参数格式有以下几种:
-flag xxx
(使用空格,一个-
符号)--flag xxx
(使用空格,两个-
符号)-flag=xxx
(使用等号,一个-
符号)--flag=xxx
(使用等号,两个-
符号)
其中,布尔类型的参数必须使用等号的方式指定。
遇到第一个非选项参数(即不是以-
和--
开头的)或终止符--
,解析停止。
总结一下,使用flag
库的一般步骤:
- 定义一些全局变量存储选项的值;
- 在
init
方法中使用flag.TypeVar
方法定义选项,这里的Type
可以为基本类型Int/Uint/Float64/Bool
,还可以是时间间隔time.Duration
。定义时传入变量的地址、选项名、默认值和帮助信息; - 在
main
方法中调用flag.Parse
从os.Args[1:]
中解析选项。因为os.Args[0]
为可执行程序路径,会被剔除。
注意点:
flag.Parse
方法必须在所有选项都定义之后调用,且flag.Parse
调用之后不能再定义选项。如果按照前面的步骤,基本不会出现问题。 因为init
在所有代码之前执行,将选项定义都放在init
中,main
函数中执行flag.Parse
时所有选项都已经定义了。
flag
其他函数
1 | flag.Args() ////返回命令行参数后的其他参数,以[]string类型 |
高级用法
定义短选项
flag
库并没有显示支持短选项,但是可以通过给某个相同的变量设置不同的选项来实现。即两个选项共享同一个变量。 由于初始化顺序不确定,必须保证它们拥有相同的默认值。否则不传该选项时,行为是不确定的。
1 | package main |
自定义选项
除了使用flag
库提供的选项类型,我们还可以自定义选项类型。我们分析一下标准库中提供的案例:
1 | package main |
首先定义一个新类型,这里定义类型interval
。
新类型必须实现flag.Value
接口:
1 | // src/flag/flag.go |
其中String
方法格式化该类型的值,flag.Parse
方法在执行时遇到自定义类型的选项会将选项值作为参数调用该类型变量的Set
方法。 这里将以,
分隔的时间间隔解析出来存入一个切片中。
自定义类型选项的定义必须使用flag.Var
方法。
解析程序中的字符串
有时候选项并不是通过命令行传递的。例如,从配置表中读取或程序生成的。这时候可以使用flag.FlagSet
结构的相关方法来解析这些选项。
实际上,我们前面调用的flag
库的方法,都会间接调用FlagSet
结构的方法。flag
库中定义了一个FlagSet
类型的全局变量CommandLine
专门用于解析命令行选项。 前面调用的flag
库的方法只是为了提供便利,它们内部都是调用的CommandLine
的相应方法:
1 | // src/flag/flag.go |
同样的,我们也可以自己创建FlagSet
类型变量来解析选项。
1 | package main |
NewFlagSet
方法有两个参数,第一个参数是程序名称,输出帮助或出错时会显示该信息。第二个参数是解析出错时如何处理,有几个选项:
ContinueOnError
:发生错误后继续解析,CommandLine
就是使用这个选项;ExitOnError
:出错时调用os.Exit(2)
退出程序;PanicOnError
:出错时产生 panic。
随便看一眼flag
库中的相关代码:
1 | // src/flag/flag.go |
与直接使用flag
库的方法有一点不同,FlagSet
调用Parse
方法时需要显示传入字符串切片作为参数。因为flag.Parse
在内部调用了CommandLine.Parse(os.Args[1:])
。
总结
至此,本项目的解析配置文件功能已经基本完成了,该功能先是使用 flag 库来解析命令行中的内容,当命令行内容不为空时,则使用输入值来进行配置;若为空,则通过解析原有的配置文件来实现。