当我们写一些命令行小程序的时候,我们会需要解析命令行参数,以及可能会处理后面的参数。像ls等:
ls -a
要是自己写这些参数解析啥的话真是十分的难受,还好golang已经帮我们写好了相关工具。这就是flag工具包。下面我们来详细说说。
命令行参数语法golang的命令行flag语法如下:
-flag
-flag=x
-flag x // 仅限非布尔类型的flag
一个或两个"-"没有差别,都可以使用。最后那个形式不允许布尔类型使用是因为这条命令:
cmd -x *
-flag=false
-flag
当遇到第一个非flag参数(“-”是一个非flag参数)或者在遇到终结符“–”时,flag转换会停止。
整数flag可以接受1234、0664、0x1234以及负数。布尔flag可以是:
1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False
time.ParseDuration
快速入门
我们先来看看flag包中最常被使用的那些全局函数。这些全局函数是用来处理系统输入的。
对于一个直接处理命令行输入的简单程序,只需要直接使用这些方法即可。
Parse()
这个函数是用来实际触发转换的。底层实际上就是转换了命令行从第二个开始的所有参数。
- 如果有转换失败的情况,则会直接导致程序退出,并像标准输出打印错误原因。
- 如果输入的是-help或-h,且没有直接配置,则会输出使用帮助
需要在设置完各种配置后再调用此方法。
对于最简单的一个命令行程序,直接在main后立刻调用其即可:
import "flag"
func main() {
flag.Parse()
// 后续处理
}
Parsed()
对应的,在调用过Parse()进行转换后,Parsed()会返回true,否则会返回false。
flag相关
配置flag
配置flag主要有两类方法:
func XXX(name string, value xxx, usage string) *xxx
func XXXVar(p *xxx, name string, value xxx, usage string)
其中XXX为各个类型名,有:bool、duration、float64、int64、int、uint、uint64、string等
区别是,前者直接返回指向对应类型指针并分配对应指针指向的对象,我们可以直接通过解引用来获取转换后的对应值。
package main
import (
"flag"
"fmt"
)
func main() {
pi := flag.Int("a", 10, "apple")
flag.Parse()
fmt.Printf("%v\n", *pi)
}
$ go run main.go -a 1
1
而后者则由我们来指定指针指向的对象,等价的写法:
package main
import (
"flag"
"fmt"
)
func main() {
var i int
flag.IntVar(&i, "a", 10, "apple")
flag.Parse()
fmt.Printf("%v\n", i)
}
name
两个方法中的name是干嘛用的呢?我们刚刚的示例中其实已经蛮清楚的了,就是flag的名字,会用来和"-“直到空格或者”="前的字符串进行匹配用,命中了就会使用对应的flag配置。
注意,name是区分大小写的。
usage
那么flag参数中的usage是咋用的呢?这是用来自动生成每个flag的帮助用的。我们可以通过-help选项(或者故意转换错误)来看到这个默认的帮助:
$ go run main.go -help
Usage of ……/go-build794326495/b001/exe/main:
-a int
apple (default 10)
shorthand
经常我们会想要为flag创建快捷方式,就像-h等价于-help一样,如果两个flag设为不同的变量然后在分别判断,未免写起来有点复杂,XXXVar那个方法就很适用于这种情况:
package main
import (
"flag"
"fmt"
)
func main() {
var i int
usage := "apple"
defaultValue := 10
flag.IntVar(&i, "a", defaultValue, usage+"(shorthand)")
flag.IntVar(&i, "apple", defaultValue, usage)
flag.Parse()
fmt.Printf("%v\n", i)
}
这样就可以让两个flag都绑定到同一个参数上。
注意:由于无法保证两个flag处理的先后顺序,快捷和非快捷的flag的默认值应该完全一致。
来看看默认的帮助文档的效果:
$ go run main.go -help
Usage of ……/go-build279426576/b001/exe/main:
-a int
apple(shorthand) (default 10)
-apple int
apple (default 10)
以及设置两个flag:
$ go run main.go -a=100
100
$ go run main.go -apple=200
200
Args
处理完flag后,后面的参数都会当作普通参数处理。
NArg()func Arg(i int) stringfunc Args() []string
package main
import (
"flag"
"fmt"
)
func main() {
flag.Parse()
fmt.Printf("Total arguments count:%v\n", flag.NArg())
// 与下面等价
//for i, arg := range flag.Args() {
// fmt.Printf("%d:%s\n", i, arg)
//}
for i := 0; i < flag.NArg(); i++ {
fmt.Printf("%d:%s\n", i, flag.Arg(i))
}
}
$ go run main.go 200 10 -30
Total arguments count:3
0:200
1:10
2:-30
这些知识足够我们写出一个支持flag的简单命令行程序了,但是如果想要更加深入使用的话,我们还需要继续深入学习。
FlagSet其实上面的这些全局方法都是对一个特殊的FlagSet(CommandLine)的shorthand。FlagSet类才是更通用的那个flag转换工具类。
CommandLine把系统输入的第一个参数,也就是程序名作为自己FlagSet的名字。
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
从系统输入的第二个参数开始都作为用来Parse的参数。
func Parse() {
// Ignore errors; CommandLine is set for ExitOnError.
CommandLine.Parse(os.Args[1:])
}
其他对应的各个全局函数也都是对CommandLine变量的封装而已。
NewFlagSet
func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet
FlagSet的工厂方法。name会影响FlagSet.Name()的返回值,主要关注ErrorHandling方法,其影响parse失败后的处理方法。
我们看看Parse函数就大概明白了
func (f *FlagSet) Parse(arguments []string) error {
f.parsed = true
f.args = arguments
for {
seen, err := f.parseOne()
if seen {
continue
}
if err == nil {
break
}
switch f.errorHandling {
case ContinueOnError:
return err
case ExitOnError:
if err == ErrHelp {
os.Exit(0)
}
os.Exit(2)
case PanicOnError:
panic(err)
}
}
return nil
}
对于ContinueOnError、ExitOnError和PanicOnError这三个选项,在转换失败的时候,分别会返回错误。直接调用os.Exit函数。以及直接panic。
CommandLine选择的是ExitOnError应为这符合它的场景,转换失败时直接退出程序即可。
FlagSet的ErrorHandling配置可以通过以下方法取出:
func (f *FlagSet) ErrorHandling() ErrorHandling
flag相关
返回变量指针的那个方法实际上就是自己内部分配了下对应的变量,然后还是调用的xxxVar方法。
func (f *FlagSet) Int(name string, value int, usage string) *int {
p := new(int)
f.IntVar(p, name, value, usage)
return p
}
而xxxVar方法实际上又是调用的Var()方法
func (f *FlagSet) IntVar(p *int, name string, value int, usage string) {
f.Var(newIntValue(value, p), name, usage)
}
使用Var定制flag解析
func (f *FlagSet) Var(value Value, name string, usage string)
这个方法才是所有设置flag方法的爹,其接受一个实现了Value接口的参数,然后存到内部的Flag结构体的map中。
func (f *FlagSet) Var(value Value, name string, usage string) {
// Remember the default value as a string; it won't change.
flag := &Flag{name, usage, value, value.String()}
_, alreadythere := f.formal[name]
if alreadythere {
……
panic(msg) // Happens only if flags are declared with identical names
}
……
f.formal[name] = flag
}
我们可以看到,如果重复赋值同一个flag,会直接panic。
Value接口如下:
type Value interface {
String() string
Set(string) error
}
其实还有个更大的接口:
type Getter interface {
Value
Get() interface{}
}
这主要是历史原因,最早的版本里的Value没有Get方法,所以只好在后续版本中再封一个Getter。
我们来看看IntValue是咋实现的:
// -- int Value
type intValue int
func newIntValue(val int, p *int) *intValue {
*p = val
return (*intValue)(p)
}
func (i *intValue) Set(s string) error {
v, err := strconv.ParseInt(s, 0, strconv.IntSize)
if err != nil {
err = numError(err)
}
*i = intValue(v)
return err
}
func (i *intValue) Get() interface{} { return int(*i) }
func (i *intValue) String() string { return strconv.Itoa(int(*i)) }
用默认值来初始化指向的对象,然后返回指向其的指针。Set就是转换字符串后修改自己的值为对应的int,Get就是返回自己的值。
这样,在后面parse的时候,就可以直接调用接口来设置其值:
// parseOne parses one flag. It reports whether a flag was seen.
func (f *FlagSet) parseOne() (bool, error) {
……
flag, alreadythere := m[name]
……
if err := flag.Value.Set(value); err != nil {
return false, f.failf("invalid value %q for flag -%s: %v", value, name, err)
}
……
}
所以其实我们可以自己实现Value接口,来定制化的解析一个flag的参数,这是官方的示例:
package main
import (
"flag"
"fmt"
"net/url"
)
type URLValue struct {
URL *url.URL
}
func (v URLValue) String() string {
if v.URL != nil {
return v.URL.String()
}
return ""
}
func (v URLValue) Set(s string) error {
if u, err := url.Parse(s); err != nil {
return err
} else {
*v.URL = *u
}
return nil
}
var u = &url.URL{}
func main() {
fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
fs.Var(&URLValue{u}, "url", "URL to parse")
fs.Parse([]string{"-url", "https://golang.org/pkg/flag/"})
fmt.Printf(`{scheme: %q, host: %q, path: %q}`, u.Scheme, u.Host, u.Path)
}
// output:
//{scheme: "https", host: "golang.org", path: "/pkg/flag/"}
Func(after Go 1.16)
func (f *FlagSet) Func(name, usage string, fn func(string) error)
1.16后有个更方便的方法来实现定制解析,就是这个Func方法,直接在fn中处理入参即可轻松实现参数转换等目的了,很依赖于闭包。
官方的示例如下:
package main
import (
"errors"
"flag"
"fmt"
"net"
"os"
)
func main() {
fs := flag.NewFlagSet("ExampleFunc", flag.ContinueOnError)
fs.SetOutput(os.Stdout)
var ip net.IP
fs.Func("ip", "`IP address` to parse", func(s string) error {
ip = net.ParseIP(s)
if ip == nil {
return errors.New("could not parse IP")
}
return nil
})
fs.Parse([]string{"-ip", "127.0.0.1"})
fmt.Printf("{ip: %v, loopback: %t}\n\n", ip, ip.IsLoopback())
// 256 is not a valid IPv4 component
fs.Parse([]string{"-ip", "256.0.0.1"})
fmt.Printf("{ip: %v, loopback: %t}\n\n", ip, ip.IsLoopback())
}
// Output:
//{ip: 127.0.0.1, loopback: true}
//
//invalid value "256.0.0.1" for flag -ip: could not parse IP
//Usage of ExampleFunc:
// -ip IP address
// IP address to parse
//{ip: <nil>, loopback: false}
遍历所有flag
func VisitAll(fn func(*Flag))
用于按字典序遍历所有flag,不管有没有被设置值。随时可以调用。默认的Usage方法里有个对它用法很好的示例。后面我们再看。先看看和它类似的另一个方法。
func Visit(fn func(*Flag))
用于按字典序遍历所有被设置了值的flag,应该在Parse()后调用。比如我们可以打印出来所有被设置了值的flag
package main
import (
"flag"
"fmt"
"time"
)
func main() {
fs := flag.NewFlagSet("test", flag.ExitOnError)
_ = fs.Int("a", 10, "apple")
_ = fs.Duration("b", time.Second, "brand")
fs.Parse([]string{"-a=100"})
fs.Visit(func(f *flag.Flag) {
fmt.Printf("Flag %q is set as %v\n", f.Name, f.Value)
})
}
$ go run main.go
Flag "a" is set as 100
flagset.actual
其他方法
func (f *FlagSet) NFlag() int
用来获取已经设置的Flag的个数
func Set(name, value string) error
可以直接设置指定flag的value。内部就是通过flag的map找到对应flag,然后调用它的Set方法。
Parse()
其实没什么好说的,前面基本都说完了。
flagset的Parse是直接给argument list的。
func (f *FlagSet) Parse(arguments []string) error
flag.ErrHelp
输出重定向
func (f *FlagSet) SetOutput(output io.Writer)
func (f *FlagSet) Output() io.Writer
os.Stderr
在内部实现中,到处可见
fmt.Fprintf(f.Output(),"xxxxx")
这样的语句,这也是flagset的标准的输出方式,所有的内部实现都是像这样打印输出的。
当直接使用flagset的时候,我们一般不会希望直接往标准输出打印,而是希望能够截获输出的内容,这时可以这么写:
package main
import (
"flag"
"fmt"
"strings"
)
func main() {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
buf := &strings.Builder{}
fs.SetOutput(buf)
fs.Int("aaa", 10, "help of aaa")
// 会返回ErrHelp,这里知道它会返回ErrHelp了,直接没处理。
fs.Parse([]string{"-help"})
fmt.Print(buf.String())
}
$ go run main.go
Usage of test:
-aaa int
help of aaa (default 10)
Usage
FlagSet有一个Usage成员,可以通过它来定制help输出
type FlagSet struct {
Usage func()
……
}
我们可以学习下默认的Usage实现
// defaultUsage is the default function to print a usage message.
func (f *FlagSet) defaultUsage() {
if f.name == "" {
fmt.Fprintf(f.Output(), "Usage:\n")
} else {
fmt.Fprintf(f.Output(), "Usage of %s:\n", f.name)
}
f.PrintDefaults()
}
func (f *FlagSet) PrintDefaults() {
f.VisitAll(func(flag *Flag) {
s := fmt.Sprintf(" -%s", flag.Name) // Two spaces before -; see next two comments.
name, usage := UnquoteUsage(flag)
if len(name) > 0 {
s += " " + name
}
// Boolean flags of one ASCII letter are so common we
// treat them specially, putting their usage on the same line.
if len(s) <= 4 { // space, space, '-', 'x'.
s += "\t"
} else {
// Four spaces before the tab triggers good alignment
// for both 4- and 8-space tab stops.
s += "\n \t"
}
s += strings.ReplaceAll(usage, "\n", "\n \t")
if !isZeroValue(flag, flag.DefValue) {
if _, ok := flag.Value.(*stringValue); ok {
// put quotes on the value
s += fmt.Sprintf(" (default %q)", flag.DefValue)
} else {
s += fmt.Sprintf(" (default %v)", flag.DefValue)
}
}
fmt.Fprint(f.Output(), s, "\n")
})
}
默认的Usage实现很好地给我们示例了往Output输出结果,以及怎么VisitAll。
如果我们不想使用默认的help打印,可以直接:
package main
import (
"flag"
"fmt"
"strings"
)
func main() {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
buf := &strings.Builder{}
fs.SetOutput(buf)
fs.Usage = func() {
fmt.Fprintln(buf, "this is the help")
}
fs.Parse([]string{"-help"})
fmt.Print(buf.String())
}
这样,在需要输出help的地方,就会打印你定制的那个help了。
$ go run main.go
this is the help
我们看到打印arguments部分的usage的方法PrintDefaults()是公开的,甚至我们可以混着来实现Usage:
package main
import (
"flag"
"fmt"
"strings"
)
func main() {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
buf := &strings.Builder{}
fs.SetOutput(buf)
fs.String("hahaha", "default", "help of hahaha")
fs.Usage = func() {
fmt.Fprintln(buf, "this is the help")
fs.PrintDefaults()
}
fs.Parse([]string{"-help"})
fmt.Print(buf.String())
}
$ go run main.go
this is the help
-hahaha string
help of hahaha (default "default")
结语
这次,带大家基本过了一遍Golang的flag包的主要细节。应该已经足够满足日常使用了,如果想要更进一步的话可以自己读一遍源码,再都使用使用,相信可以加深理解,成为flag通。hhhhh
reference