8

标准库—命令行参数解析flag

 3 years ago
source link: http://blog.studygolang.com/2013/02/%e6%a0%87%e5%87%86%e5%ba%93-%e5%91%bd%e4%bb%a4%e8%a1%8c%e5%8f%82%e6%95%b0%e8%a7%a3%e6%9e%90flag/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
标准库—命令行参数解析flag

标准库—命令行参数解析flag

评论有人提到没有例子,不知道讲的是什么。因此,为了大家能够更好地理解,特意加了一个示例。其实本文更多讲解的是 flag 的实现原理,加上示例之后,就更好地知道怎么使用了。建议阅读 《Go语言标准库》一书的对应章节:flag – 命令行参数解析

在写命令行程序(工具、server)时,对命令参数进行解析是常见的需求。各种语言一般都会提供解析命令行参数的方法或库,以方便程序员使用。如果命令行参数纯粹自己写代码解析,对于比较复杂的,还是挺费劲的。在go标准库中提供了一个包:flag,方便进行命令行解析。

注:区分几个概念
1)命令行参数(或参数):是指运行程序提供的参数
2)已定义命令行参数:是指程序中通过flag.Xxx等这种形式定义了的参数
3)非flag(non-flag)命令行参数(或保留的命令行参数):后文解释

一、使用示例

我们以 nginx 为例,执行 nginx -h,输出如下:

1nginx version: nginx/1.10.0
2Usage: nginx [-?hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]
3 
4Options:
5-?,-h         : this help
6-v            : show version and exit
7-V            : show version and configure options then exit
8-t            : test configuration and exit
9-T            : test configuration, dump it and exit
10-q            : suppress non-error messages during configuration testing
11-s signal     : send signal to a master process: stop, quit, reopen, reload
12-p prefix     : set prefix path (default: /usr/local/nginx/)
13-c filename   : set configuration file (default: conf/nginx.conf)
14-g directives : set global directives out of configuration file

我们通过 `flag` 实现类似 nginx 的这个输出,创建文件 nginx.go,内容如下:

1package main
2 
3import (
4"flag"
5"fmt"
6"os"
7)
8 
9// 实际中应该用更好的变量名
10var (
11h bool
12 
13v, V bool
14t, T bool
15q    *bool
16 
17s string
18p string
19c string
20g string
21)
22 
23func init() {
24flag.BoolVar(&h, "h", false, "this help")
25 
26flag.BoolVar(&v, "v", false, "show version and exit")
27flag.BoolVar(&V, "V", false, "show version and configure options then exit")
28 
29flag.BoolVar(&t, "t", false, "test configuration and exit")
30flag.BoolVar(&T, "T", false, "test configuration, dump it and exit")
31 
32// 另一种绑定方式
33q = flag.Bool("q", false, "suppress non-error messages during configuration testing")
34 
35// 注意 `signal`。默认是 -s string,有了 `signal` 之后,变为 -s signal
36flag.StringVar(&s, "s", "", "send `signal` to a master process: stop, quit, reopen, reload")
37flag.StringVar(&p, "p", "/usr/local/nginx/", "set `prefix` path")
38flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`")
39flag.StringVar(&g, "g", "conf/nginx.conf", "set global `directives` out of configuration file")
40 
41// 改变默认的 Usage
42flag.Usage = usage
43}
44 
45func main() {
46flag.Parse()
47 
48if h {
49flag.Usage()
50}
51}
52 
53func usage() {
54fmt.Fprintf(os.Stderr, `nginx version: nginx/1.10.0
55Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]
56 
57Options:
58`)
59flag.PrintDefaults()
60}

执行:go run nginx.go -h,(或 go build -o nginx && ./nginx -h)输出如下:

1nginx version: nginx/1.10.0
2Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]
3 
4Options:
5-T    test configuration, dump it and exit
6-V    show version and configure options then exit
7-c file
8set configuration file (default "conf/nginx.conf")
9-g directives
10set global directives out of configuration file (default "conf/nginx.conf")
11-h    this help
12-p prefix
13set prefix path (default "/usr/local/nginx/")
14-q    suppress non-error messages during configuration testing
15-s signal
16send signal to a master process: stop, quit, reopen, reload
17-t    test configuration and exit
18-v    show version and exit

仔细理解以上例子,如果有不理解的,看完下文的讲解再回过头来看。

二、flag包概述

flag包实现了命令行参数的解析。

定义flags有两种方式:

1)flag.Xxx(),其中Xxx可以是Int、String等;返回一个相应类型的指针,如:

1var ip = flag.Int("flagname", 1234, "help message for flagname")

2)flag.XxxVar(),将flag绑定到一个变量上,如:

1var flagvar int
2flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")

另外,还可以创建自定义flag,只要实现flag.Value接口即可(要求receiver是指针),这时候可以通过如下方式定义该flag:

1flag.Var(&flagVal, "name", "help message for flagname")

例如,解析我喜欢的编程语言,我们希望直接解析到 slice 中,我们可以定义如下 Value:

1type sliceValue []string
2 
3func newSliceValue(vals []string, p *[]string) *sliceValue {
4*p = vals
5return (*sliceValue)(p)
6}
7 
8func (s *sliceValue) Set(val string) error {
9*s = sliceValue(strings.Split(val, ","))
10return nil
11}
12 
13func (s *sliceValue) Get() interface{} { return []string(*s) }
14 
15func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }

之后可以这么使用:

1var languages []string
2flag.Var(newSliceValue([]string{}, &languages), "slice", "I like programming `languages`")

这样通过 -slice “go,php” 这样的形式传递参数,languages 得到的就是 [go, php]。

flag 中对 Duration 这种非基本类型的支持,使用的就是类似这样的方式。

在所有的flag定义完成之后,可以通过调用flag.Parse()进行解析。

命令行flag的语法有如下三种形式:
-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型

其中第三种形式只能用于非bool类型的flag,原因是:如果支持,那么对于这样的命令 cmd -x *,如果有一个文件名字是:0或false等,则命令的原意会改变(之所以这样,是因为bool类型支持-flag这种形式,如果bool类型不支持-flag这种形式,则bool类型可以和其他类型一样处理。也正因为这样,Parse()中,对bool类型进行了特殊处理)。默认的,提供了-flag,则对应的值为true,否则为flag.Bool/BoolVar中指定的默认值;如果希望显示设置为false则使用-flag=false。

int类型可以是十进制、十六进制、八进制甚至是负数;bool类型可以是1, 0, t, f, true, false, TRUE, FALSE, True, False。Duration可以接受任何time.ParseDuration能解析的类型

三、类型和函数

在看类型和函数之前,先看一下变量
ErrHelp:该错误类型用于当命令行指定了-help参数但没有定义时。
Usage:这是一个函数,用于输出所有定义了的命令行参数和帮助信息(usage message)。一般,当命令行参数解析出错时,该函数会被调用。我们可以指定自己的Usage函数,即:flag.Usage = func(){}

go标准库中,经常这么做:

定义了一个类型,提供了很多方法;为了方便使用,会实例化一个该类型的实例(通用),这样便可以直接使用该实例调用方法。比如:encoding/base64中提供了StdEncoding和URLEncoding实例,使用时:base64.StdEncoding.Encode()

在flag包中,进行了进一步封装:将FlagSet的方法都重新定义了一遍,也就是提供了一序列函数,而函数中只是简单的调用已经实例化好了的FlagSet实例:commandLine 的方法,这样commandLine实例便不需要export。这样,使用者是这么调用:flag.Parse()而不是flag.commandLine.Parse()。(Go 1.2 版本起改为了 CommandLine)

这里不详细介绍各个函数,在类型方法中介绍。

2、类型(数据结构)

1)ErrorHandling

1type ErrorHandling int

该类型定义了在参数解析出错时错误处理方式定义了。三个该类型的常量:

1const (
2ContinueOnError ErrorHandling = iota
3ExitOnError
4PanicOnError
5)

三个常量在源码的 FlagSet 的方法 parseOne() 中使用了。

2)Flag

1// A Flag represents the state of a flag.
2type Flag struct {
3Name     string // name as it appears on command line
4Usage    string // help message
5Value    Value  // value as set
6DefValue string // default value (as text); for usage message
7}

Flag类型代表一个flag的状态。

比如:autogo -f abc.txt,代码 flag.String(“f”, “a.txt”, “usage”),则该Flag实例(可以通过flag.Lookup(“f”)获得)相应的值为:f, usage, abc.txt, a.txt。

3)FlagSet

1// A FlagSet represents a set of defined flags.
2type FlagSet struct {
3// Usage is the function called when an error occurs while parsing flags.
4// The field is a function (not a method) that may be changed to point to
5// a custom error handler.
6Usage func()
7 
8name string // FlagSet的名字。CommandLine 给的是 os.Args[0]
9parsed bool // 是否执行过Parse()
10actual map[string]*Flag // 存放实际传递了的参数(即命令行参数)
11formal map[string]*Flag // 存放所有已定义命令行参数
12args []string // arguments after flags // 开始存放所有参数,最后保留 非flag(non-flag)参数
13exitOnError bool // does the program exit if there's an error?
14errorHandling ErrorHandling // 当解析出错时,处理错误的方式
15output io.Writer // nil means stderr; use out() accessor
16}

4)Value接口

1// Value is the interface to the dynamic value stored in a flag.
2// (The default value is represented as a string.)
3type Value interface {
4String() string
5Set(string) error
6}

所有参数类型需要实现Value接口,flag包中,为int、float、bool等实现了该接口。借助该接口,我们可以自定义flag

四、主要类型的方法(包括类型实例化)

flag包中主要是FlagSet类型。

1、实例化方式

NewFlagSet()用于实例化FlagSet。预定义的FlagSet实例 CommandLine 的定义方式:

1// The default set of command-line flags, parsed from os.Args.
2var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

可见,默认的FlagSet实例在解析出错时会退出程序。

由于FlagSet中的字段没有export,其他方式获得FlagSet实例后,比如:FlagSet{}或new(FlagSet),应该调用Init()方法,初始化name和errorHandling。

2、定义flag参数方法

这一序列的方法都有两种形式,在一开始已经说了两种方式的区别。这些方法用于定义某一类型的flag参数。

3、解析参数(Parse)

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

从参数列表中解析定义的flag。参数arguments不包括命令名,即应该是os.Args[1:]。事实上,flag.Parse()函数就是这么做的:

1// Parse parses the command-line flags from os.Args[1:].  Must be called
2// after all flags are defined and before flags are accessed by the program.
3func Parse() {
4// Ignore errors; CommandLine is set for ExitOnError.
5CommandLine.Parse(os.Args[1:])
6}

该方法应该在flag参数定义后而具体参数值被访问前调用。

如果提供了-help参数(命令中给了)但没有定义(代码中),该方法返回ErrHelp错误。默认的CommandLine,在Parse出错时会退出(ExitOnError)

为了更深入的理解,我们看一下Parse(arguments []string)的源码:

1func (f *FlagSet) Parse(arguments []string) error {
2f.parsed = true
3f.args = arguments
4for {
5seen, err := f.parseOne()
6if seen {
7continue
8}
9if err == nil {
10break
11}
12switch f.errorHandling {
13case ContinueOnError:
14return err
15case ExitOnError:
16os.Exit(2)
17case PanicOnError:
18panic(err)
19}
20}
21return nil
22}

真正解析参数的方法是非导出方法 parseOne。

结合parseOne方法,我们来解释non-flag以及包文档中的这句话:

Flag parsing stops just before the first non-flag argument (“-” is a non-flag argument) or after the terminator “–“.

我们需要了解解析什么时候停止

根据Parse()中for循环终止的条件(不考虑解析出错),我们知道,当parseOne返回false, nil时,Parse解析终止。正常解析完成我们不考虑。看一下parseOne的源码发现,有两处会返回false, nil。

1)第一个non-flag参数

1s := f.args[0]
2if len(s) == 0 || s[0] != '-' || len(s) == 1 {
3return false, nil
4}

也就是,当遇到单独的一个”-“或不是”-“开始时,会停止解析。比如:

./autogo – -f或./autogo build -f

这两种情况,-f都不会被正确解析。像该例子中的”-“或build(以及之后的参数),我们称之为non-flag参数

2)两个连续的”–“

1if s[1] == '-' {
2num_minuses++
3if len(s) == 2 { // "--" terminates the flags
4f.args = f.args[1:]
5return false, nil
6}
7}

也就是,当遇到连续的两个”-“时,解析停止。

说明:这里说的”-“和”–“,位置和”-f”这种的一样。也就是说,下面这种情况并不是这里说的:

./autogo -f —

这里的”–“会被当成是f的值

parseOne方法中接下来是处理-flag=x,然后是-flag(bool类型)(这里对bool进行了特殊处理),接着是-flag x这种形式,最后,将解析成功的Flag实例存入FlagSet的actual map中。

另外,在parseOne中有这么一句:

1f.args = f.args[1:]

也就是说,每执行成功一次parseOne,f.args会少一个。所以,FlagSet中的args最后留下来的就是所有non-flag参数。

4、Arg(i int)和Args()、NArg()、NFlag()

Arg(i int)和Args()这两个方法就是获取non-flag参数的;NArg()获得non-flag个数;NFlag()获得FlagSet中actual长度(即被设置了的参数个数)。

5、Visit/VisitAll

这两个函数分别用于访问FlatSet的actual和formal中的Flag,而具体的访问方式由调用者决定。

6、PrintDefaults()

打印所有已定义参数的默认值(调用VisitAll),默认输出到标准错误,除非指定了FlagSet的output(通过SetOutput()设置)

7、Set(name, value string)

设置某个flag的值(通过Flag的name)

使用建议:虽然上面讲了那么多,一般来说,我们只简单的定义flag,然后parse,就如同开始的例子一样。

如果项目需要复杂或更高级的命令行解析方式,可以试试goptions

如果想要像go工具那样的多命令(子命令)处理方式,可以试试command

2017-10-14:复杂的命令解析,推荐 https://github.com/urfave/cli 或者 https://github.com/spf13/cobra

欢迎关注我的公众号:

polarisxu-qrcode-soso-s.png

好差啊挺差的一般般还行很赞 (4 人打了分, 平均分:4.50,总分:5)

15 thoughts on “标准库—命令行参数解析flag”

  1. fccae289d3c27acc97ab18c1e860c231?s=50&d=mm&r=gshajiquan说道:
  2. ?s=50&d=mm&r=g匿名说道:

    完全不知道在讲什么

  3. ?s=50&d=mm&r=g匿名说道:

    是啊,没有例子

  4. ?s=50&d=mm&r=g匿名说道:

    哎,同感,不得不再去其他地方找。。。

  5. b12f412ddfb48d0126816c853919966d?s=50&d=mm&r=gchenbjin说道:

    这里写得不是很详细,相信通过例子大家会更容易接受点。比如说,我想实现cat功能,假设命令行输入:./cat -n hello.go,那么我在代码中就得设置bool的flag,判断-n存在。
    var numberFlag = flag.Bool(“n”, false, “number each line”)
    其他内容同理。

  6. ?s=50&d=mm&r=g匿名说道:

    确实啊,像是乱讲一通

  7. ?s=50&d=mm&r=g匿名说道:

    作者你到底在讲什么。 分析不像分析,demo不像demo

  8. ?s=50&d=mm&r=g匿名说道:
  9. 90da592a84af25b539027079e6e05f8b?s=50&d=mm&r=giralance说道:

    不错,直接看源代码。后面有些点还没讲

  10. ?s=50&d=mm&r=gMurphy说道:

    普通玩家,上面的例子就够用了

发表评论 取消回复

电子邮件地址不会被公开。

评论

姓名

电子邮件

站点

在此浏览器中保存我的名字、电邮和网站。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK