go-arg
基于结构体的Go语言参数解析
通过定义结构体来声明程序的命令行参数。
var args struct {
Foo string
Bar bool
}
arg.MustParse(&args)
fmt.Println(args.Foo, args.Bar)
$ ./example --foo=hello --bar
hello true
安装
go get github.com/alexflint/go-arg
必需参数
var args struct {
ID int `arg:"required"`
Timeout time.Duration
}
arg.MustParse(&args)
$ ./example
用法: example --id ID [--timeout TIMEOUT]
错误: --id 是必需的
位置参数
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional"`
}
arg.MustParse(&args)
fmt.Println("输入:", args.Input)
fmt.Println("输出:", args.Output)
$ ./example src.txt x.out y.out z.out
输入: src.txt
输出: [x.out y.out z.out]
环境变量
var args struct {
Workers int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("工作线程:", args.Workers)
$ WORKERS=4 ./example
工作线程: 4
$ WORKERS=4 ./example --workers=6
工作线程: 6
你也可以覆盖环境变量的名称:
var args struct {
Workers int `arg:"env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("工作线程:", args.Workers)
$ NUM_WORKERS=4 ./example
工作线程: 4
你可以使用CSV(RFC 4180)格式提供多个值:
var args struct {
Workers []int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("工作线程:", args.Workers)
$ WORKERS='1,99' ./example
工作线程: [1 99]
你还可以使用与参数名不匹配的环境变量:
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("工作线程:", args.Workers)
$ NUM_WORKERS=6 ./example
工作线程: 6
$ NUM_WORKERS=6 ./example --count 4
工作线程: 4
使用说明字符串
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v,--verbose" help:"详细程度"`
Dataset string `help:"使用的数据集"`
Optimize int `arg:"-O" help:"优化级别"`
}
arg.MustParse(&args)
$ ./example -h
用法: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
位置参数:
INPUT
OUTPUT
选项:
--verbose, -v 详细程度
--dataset DATASET 使用的数据集
--optimize OPTIMIZE, -O OPTIMIZE
优化级别
--help, -h 打印此帮助信息
默认值
var args struct {
Foo string `default:"abc"`
Bar bool
}
arg.MustParse(&args)
默认值(v1.2之前)
var args struct {
Foo string
Bar bool
}
arg.Foo = "abc"
arg.MustParse(&args)
结合命令行选项、环境变量和默认值
你可以结合使用命令行参数、环境变量和默认值。命令行参数优先于环境变量,环境变量优先于默认值。这意味着我们首先检查某个选项是否在命令行上提供,如果没有,我们检查环境变量(仅当提供了env
标签时),如果仍未找到,我们检查包含默认值的default
标签。
var args struct {
Test string `arg:"-t,env:TEST" default:"something"`
}
arg.MustParse(&args)
忽略环境变量和/或默认值
通过忽略环境变量和/或默认值,可以保持现有结构中的值不变。
var args struct {
Test string `arg:"-t,env:TEST" default:"something"`
}
p, err := arg.NewParser(arg.Config{
IgnoreEnv: true,
IgnoreDefault: true,
}, &args)
err = p.Parse(os.Args)
具有多个值的参数
var args struct {
Database string
IDs []int64
}
arg.MustParse(&args)
fmt.Printf("从%s获取以下ID: %q", args.Database, args.IDs)
./example -database foo -ids 1 2 3
从foo获取以下ID: [1 2 3]
可以多次指定的参数,与位置参数混合使用
var args struct {
Commands []string `arg:"-c,separate"`
Files []string `arg:"-f,separate"`
Databases []string `arg:"positional"`
}
arg.MustParse(&args)
./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3
命令: [cmd1 cmd2 cmd3]
文件: [file1 file2 file3]
数据库: [db1 db2 db3]
具有键和值的参数
var args struct {
UserIDs map[string]int
}
arg.MustParse(&args)
fmt.Println(args.UserIDs)
./example --userids john=123 mary=456
map[john:123 mary:456]
自定义验证
var args struct {
Foo string
Bar string
}
p := arg.MustParse(&args)
if args.Foo == "" && args.Bar == "" {
p.Fail("你必须提供--foo或--bar")
}
./example
用法: samples [--foo FOO] [--bar BAR]
错误: 你必须提供--foo或--bar
版本字符串
type args struct {
...
}
func (args) Version() string {
return "someprogram 4.3.0"
}
func main() {
var args args
arg.MustParse(&args)
}
$ ./example --version
someprogram 4.3.0
注意 如果在
args
或任何子命令中定义了--version
标志,它将覆盖内置的版本控制。
覆盖选项名称
var args struct {
Short string `arg:"-s"`
Long string `arg:"--custom-long-option"`
ShortAndLong string `arg:"-x,--my-option"`
OnlyShort string `arg:"-o,--"`
}
arg.MustParse(&args)
$ ./example --help
用法: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
选项:
--short SHORT, -s SHORT
--custom-long-option CUSTOM-LONG-OPTION
--my-option MY-OPTION, -x MY-OPTION
-o ONLYSHORT
--help, -h 显示此帮助并退出
嵌入式结构体
嵌入式结构体的字段与常规字段一样处理:
type DatabaseOptions struct {
Host string
Username string
Password string
}
type LogOptions struct {
LogFile string
Verbose bool
}
func main() {
var args struct {
DatabaseOptions
LogOptions
}
arg.MustParse(&args)
}
如常,任何标记为arg:"-"
的字段都会被忽略。
支持的类型
以下类型可以用作参数:
- 内置整数类型:
int, int8, int16, int32, int64, byte, rune
- 内置浮点类型:
float32, float64
- 字符串
- 布尔值
- 表示为
url.URL
的URL - 表示为
time.Duration
的时间持续 - 表示为
mail.Address
的电子邮件地址 - 表示为
net.HardwareAddr
的MAC地址 - 以上任何类型的指针
- 以上任何类型的切片
- 使用以上任何类型作为键和值的映射
- 任何实现
encoding.TextUnmarshaler
的类型
自定义解析
实现encoding.TextUnmarshaler
来定义你自己的解析逻辑。
// 接受形如"head.tail"的命令行参数
type NameDotName struct {
Head, Tail string
}
func (n *NameDotName) UnmarshalText(b []byte) error {
s := string(b)
pos := strings.Index(s, ".")
if pos == -1 {
return fmt.Errorf("在 %s 中缺少句点", s)
}
n.Head = s[:pos]
n.Tail = s[pos+1:]
return nil
}
func main() {
var args struct {
Name NameDotName
}
arg.MustParse(&args)
fmt.Printf("%#v\n", args.Name)
}
$ ./example --name=foo.bar
main.NameDotName{Head:"foo", Tail:"bar"}
$ ./example --name=oops
用法: example [--name NAME]
错误: 处理 --name 时出错: 在 "oops" 中缺少句点
带默认值的自定义解析
实现 encoding.TextMarshaler
来定义你自己的默认值字符串:
// 接受形如 "head.tail" 的命令行参数
type NameDotName struct {
Head, Tail string
}
func (n *NameDotName) UnmarshalText(b []byte) error {
// 与前一个例子相同
}
// 只有当你想在使用说明中显示默认值时才需要这个
func (n *NameDotName) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil
}
func main() {
var args struct {
Name NameDotName `default:"file.txt"`
}
arg.MustParse(&args)
fmt.Printf("%#v\n", args.Name)
}
$ ./example --help
用法: test [--name NAME]
选项:
--name NAME [默认值: file.txt]
--help, -h 显示此帮助信息并退出
$ ./example
main.NameDotName{Head:"file", Tail:"txt"}
自定义占位符
在1.3.0版本中引入
使用 placeholder
标签来控制在使用说明中使用的占位符文本。
var args struct {
Input string `arg:"positional" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" help:"优化级别" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" help:"最大同时作业数" placeholder:"N"`
}
arg.MustParse(&args)
$ ./example -h
用法: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
位置参数:
SRC
DST
选项:
--optimize LEVEL, -O LEVEL
优化级别
--maxjobs N, -j N 最大同时作业数
--help, -h 显示此帮助信息并退出
描述字符串
通过实现返回字符串的 Description
函数,可以在帮助文本顶部添加描述性消息。
type args struct {
Foo string
}
func (args) Description() string {
return "该程序执行这个和那个"
}
func main() {
var args args
arg.MustParse(&args)
}
$ ./example -h
该程序执行这个和那个
用法: example [--foo FOO]
选项:
--foo FOO
--help, -h 显示此帮助信息并退出
类似地,通过实现 Epilogue
函数,可以在帮助文本末尾添加结语。
type args struct {
Foo string
}
func (args) Epilogue() string {
return "更多信息请访问 github.com/alexflint/go-arg"
}
func main() {
var args args
arg.MustParse(&args)
}
$ ./example -h
用法: example [--foo FOO]
选项:
--foo FOO
--help, -h 显示此帮助信息并退出
更多信息请访问 github.com/alexflint/go-arg
子命令
在1.1.0版本中引入
子命令常用于将多个功能分组到单个程序中的工具。一个例子是 git
工具:
$ git checkout [特定于检出代码的参数]
$ git commit [特定于提交的参数]
$ git push [特定于推送的参数]
"checkout"、"commit" 和 "push" 这些字符串与简单的位置参数不同,因为用户可用的选项会根据他们选择的子命令而改变。
这可以在 go-arg
中如下实现:
type CheckoutCmd struct {
Branch string `arg:"positional"`
Track bool `arg:"-t"`
}
type CommitCmd struct {
All bool `arg:"-a"`
Message string `arg:"-m"`
}
type PushCmd struct {
Remote string `arg:"positional"`
Branch string `arg:"positional"`
SetUpstream bool `arg:"-u"`
}
var args struct {
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
Commit *CommitCmd `arg:"subcommand:commit"`
Push *PushCmd `arg:"subcommand:push"`
Quiet bool `arg:"-q"` // 这个标志对所有子命令都是全局的
}
arg.MustParse(&args)
switch {
case args.Checkout != nil:
fmt.Printf("请求检出分支 %s\n", args.Checkout.Branch)
case args.Commit != nil:
fmt.Printf("请求提交,消息为 \"%s\"\n", args.Commit.Message)
case args.Push != nil:
fmt.Printf("请求从 %s 推送到 %s\n", args.Push.Branch, args.Push.Remote)
}
使用子命令时还有一些额外的规则:
subcommand
标签只能用于指向结构体的指针字段- 包含子命令的任何结构体都不能包含任何位置参数
这个包允许程序接受子命令,但在没有指定子命令时也可以执行其他操作。 另一方面,如果你希望在没有指定子命令时程序终止,推荐的方式是:
p := arg.MustParse(&args)
if p.Subcommand() == nil {
p.Fail("缺少子命令")
}
API 文档
https://godoc.org/github.com/alexflint/go-arg
原理
Go 有很多命令行参数解析库,包括标准库中的一个,那为什么还要构建另一个呢?
标准库中的 flag
库对我来说似乎有些尴尬。位置参数必须在选项之前,所以 ./prog x --foo=1
会如你所期望的那样工作,但 ./prog --foo=1 x
则不会。它也不允许参数同时具有长格式(--foo
)和短格式(-f
)。
许多第三方参数解析库非常适合编写复杂的命令行界面,但对于一个只有几个标志的简单脚本来说,对我而言感觉有些过度。
go-arg
背后的想法是,Go 已经有了一种使用结构体描述数据结构的优秀方式,所以没有必要开发额外的抽象层次。go-arg
用单个结构体替代了指定程序接受哪些参数的 API 和获取这些参数值的另一个 API。
向后兼容性说明
此库的早期版本要求帮助文本是 arg
标签的一部分。这仍然受支持,但现在已被弃用。相反,你应该使用上面描述的单独的 help
标签,这消除了你可以编写的文本的大部分限制。特别是,如果你的帮助文本包含任何逗号,你将需要使用新的 help
标签。