前言

当我们写一些命令行小程序的时候,我们会需要解析命令行参数,以及可能会处理后面的参数。像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