文章目录

序言

当我们写一些命令行小程序的时侯,我们会须要解析命令行参数,以及可能会处理旁边的参数。像ls等:

ls -a

要是自己写那些参数解析啥的话真是非常的恶心,还好golang早已帮我们写好了相关工具。这就是flag工具包。下边我们来详尽谈谈。

命令行参数句型

golang的命令行flag句型如下:

-flag
-flag=x
-flag x // 仅限非布尔类型的flag

一个或两个”-“没有差异linux学习,都可以使用。最后那种方式不容许布尔类型使用是由于这条命令:

cmd -x *

其中*是Unixshell的键值,假如一个文件名称作如0、false等,这条命令的含意会对应改变。你必须使用-flag=false这个方式来关掉一个布尔flag。

而-flag会促使对应布尔类型flag的值为true。

当遇见第一个非flag参数(“-”是一个非flag参数)或则在碰到终结符“–”时,flag转换会停止。

整数flag可以接受1234、0664、0x1234以及正数。布尔flag可以是:

1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False

durationflag可以接受任何可以被time.ParseDuration转换的参数。

快速入门

我们先来瞧瞧flag包中最常被使用的这些全局函数。这种全局函数是拿来处理系统输入的。

对于一个直接处理命令行输入的简单程序,只须要直接使用这种方式即可。

Parse()

这个函数是拿来实际触发转换的。底层实际上就是转换了命令行从第二个开始的所有参数。

须要在设置完各类配置后再调用此方式。

对于最简单的一个命令行程序,直接在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("%vn", *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("%vn", 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一样format命令参数有哪些,假如两个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("%vn", 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:

format格式化命令_format命令参数_format命令参数有哪些

$ go run main.go -a=100
100
$ go run main.go -apple=200
200

Args

处理完flag后,旁边的参数就会当成普通参数处理。

为了晓得有多少个普通参数,可以调用NArg()。

可以使用funcArg(iint)string来访问其中某个参数。

其实也可以使用funcArgs()[]string来一次性拿回所有的参数。

package main
import (
	"flag"
	"fmt"
)
func main() {
	flag.Parse()
	fmt.Printf("Total arguments count:%vn", flag.NArg())
	// 与下面等价
	//for i, arg := range flag.Args() {
	//	fmt.Printf("%d:%sn", i, arg)
	//}
	for i := 0; i < flag.NArg(); i++ {
		fmt.Printf("%d:%sn", 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应为这符合它的场景linux多线程,转换失败时直接退出程序即可。

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)) }

用默认值来初始化指向的对象format命令参数有哪些,之后返回指向其的表针。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(afterGo1.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}nn", ip, ip.IsLoopback())
	// 256 is not a valid IPv4 component
	fs.Parse([]string{"-ip", "256.0.0.1"})
	fmt.Printf("{ip: %v, loopback: %t}nn", 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: , 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 %vn", f.Name, f.Value)
	})
}

$ go run main.go
Flag "a" is set as 100

本质上,在parse的时侯每位被设置了值的flag就会被计入flagset.actual(map[string]*Flag)中,Visit似乎就是遍历了这个map。而VisitAll则是遍历了称作formal的会记录所有flag的map。

其他方式

func (f *FlagSet) NFlag() int

拿来获取早已设置的Flag的个数

func Set(name, value string) error

可以直接设置指定flag的value。内部就是通过flag的map找到对应flag,之后调用它的Set方式。

Parse()

虽然没哪些好说的,上面基本都说完了。

flagset的Parse是直接给argumentlist的。

func (f *FlagSet) Parse(arguments []string) error

若果是用户给的-h或-help,而且自己没有设置这个flag的话,会返回flag.ErrHelp,在选择ContinueOnError时可以对应的处理这个Error。

输出重定向

func (f *FlagSet) SetOutput(output io.Writer)

func (f *FlagSet) Output() io.Writer

这两个函数分别用于设置和取出flagset的输出流。

默认地,往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

[1]

Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复