文章目录
序言
当我们写一些命令行小程序的时侯,我们会须要解析命令行参数,以及可能会处理旁边的参数。像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:
$ 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]