flag - 命令行参数解析

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

注:区分几个概念

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

1.1. 使用示例

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

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

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

package main

import (
"flag"
"fmt"
"os"
) // 实际中应该用更好的变量名
var (
h bool v, V bool
t, T bool
q *bool s string
p string
c string
g string
) func init() {
flag.BoolVar(&h, "h", false, "this help") flag.BoolVar(&v, "v", false, "show version and exit")
flag.BoolVar(&V, "V", false, "show version and configure options then exit") flag.BoolVar(&t, "t", false, "test configuration and exit")
flag.BoolVar(&T, "T", false, "test configuration, dump it and exit") // 另一种绑定方式
q = flag.Bool("q", false, "suppress non-error messages during configuration testing") // 注意 `signal`。默认是 -s string,有了 `signal` 之后,变为 -s signal
flag.StringVar(&s, "s", "", "send `signal` to a master process: stop, quit, reopen, reload")
flag.StringVar(&p, "p", "/usr/local/nginx/", "set `prefix` path")
flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`")
flag.StringVar(&g, "g", "conf/nginx.conf", "set global `directives` out of configuration file") // 改变默认的 Usage
flag.Usage = usage
} func main() {
flag.Parse() if h {
flag.Usage()
}
} func usage() {
fmt.Fprintf(os.Stderr, `nginx version: nginx/1.10.
Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives] Options:
`)
flag.PrintDefaults()
}

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

nginx version: nginx/1.10.
Usage: nginx [-hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives] Options:
-T test configuration, dump it and exit
-V show version and configure options then exit
-c file
set configuration file (default "conf/nginx.conf")
-g directives
set global directives out of configuration file (default "conf/nginx.conf")
-h this help
-p prefix
set prefix path (default "/usr/local/nginx/")
-q suppress non-error messages during configuration testing
-s signal
send signal to a master process: stop, quit, reopen, reload
-t test configuration and exit
-v show version and exit

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

1.2. flag 包概述

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

1.2.1. 定义 flags 有两种方式

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

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

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

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

1.2.2. 自定义 Value

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

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

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

type sliceValue []string

func newSliceValue(vals []string, p *[]string) *sliceValue {
*p = vals
return (*sliceValue)(p)
} func (s *sliceValue) Set(val string) error {
*s = sliceValue(strings.Split(val, ","))
return nil
} func (s *sliceValue) Get() interface{} { return []string(*s) } func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }

之后可以这么使用:

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

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

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

1.2.3. 解析 flag

在所有的 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 能解析的类型。

1.3. 类型和函数

在看类型和函数之前,先看一下变量。

ErrHelp:该错误类型用于当命令行指定了 ·-help` 参数但没有定义时。

Usage:这是一个函数,用于输出所有定义了的命令行参数和帮助信息(usage message)。一般,当命令行参数解析出错时,该函数会被调用。我们可以指定自己的 Usage 函数,即:flag.Usage = func(){}

1.3.1. 函数

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

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

在 flag 包使用了有类似的方法,比如 CommandLine 实例,只不过 flag 进行了进一步封装:将 FlagSet 的方法都重新定义了一遍,也就是提供了一序列函数,而函数中只是简单的调用已经实例化好了的 FlagSet 实例:CommandLine 的方法。这样,使用者是这么调用:flag.Parse() 而不是 flag. CommandLine.Parse()。(Go 1.2 起,将 CommandLine 导出,之前是非导出的)

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

1.3.2. 类型(数据结构)

1)ErrorHandling

type ErrorHandling int

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

const (
ContinueOnError ErrorHandling = iota
ExitOnError
PanicOnError
)

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

2)Flag

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

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

比如,对于命令:./nginx -c /etc/nginx.conf,相应代码是:

flag.StringVar(&c, "c", "conf/nginx.conf", "set configuration `file`")

则该 Flag 实例(可以通过 flag.Lookup("c") 获得)相应各个字段的值为:

&Flag{
Name: c,
Usage: set configuration file,
Value: /etc/nginx.conf,
DefValue: conf/nginx.conf,
}

3)FlagSet

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

4)Value 接口

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

所有参数类型需要实现 Value 接口,flag 包中,为int、float、bool等实现了该接口。借助该接口,我们可以自定义flag。(上文已经给了具体的例子)

1.4. 主要类型的方法(包括类型实例化)

flag 包中主要是 FlagSet 类型。

1.4.1. 实例化方式

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

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

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

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

1.4.2. 定义 flag 参数的方法

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

1.4.3. 解析参数(Parse)

func (f *FlagSet) Parse(arguments []string) error
从参数列表中解析定义的 flag。方法参数 arguments 不包括命令名,即应该是os.Args[:]。事实

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

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

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

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

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

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:
os.Exit()
case PanicOnError:
panic(err)
}
}
return nil
}

真正解析参数的方法是非导出方法 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 参数

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

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

./nginx - -c 或 ./nginx build -c

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

2)两个连续的"--"

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

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

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

./nginx -c --

这里的"--"会被当成是 c 的值

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

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

f.args = f.args[1:]

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

1.4.4. Arg(i int) 和 Args()、NArg()、NFlag()

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

1.4.5. Visit/VisitAll

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

1.4.6. PrintDefaults()

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

1.4.7. Set(name, value string)

设置某个 flag 的值(通过 name 查找到对应的 Flag)

1.5. 总结

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

如果项目需要复杂或更高级的命令行解析方式,可以使用 https://github.com/urfave/cli 或者 https://github.com/spf13/cobra 这两个强大的库。

go语言中文网

golang-flag - 命令行参数解析的更多相关文章

  1. [Go] 命令行参数解析包(flag 包)使用详解

    Go 的 flag 包可以解析命令行的参数. 一.命令行语法 命令行语法主要有以下几种形式: cmd -flag       // 只支持bool类型 cmd -flag=xxx cmd -flag ...

  2. python命令行参数解析模块argparse和docopt

    http://blog.csdn.net/pipisorry/article/details/53046471 还有其他两个模块实现这一功能,getopt(等同于C语言中的getopt())和弃用的o ...

  3. gflags命令行参数解析

    gflags库是google开源的命令行参数解析工具. 安装 官方没有提供二进制库,但是Debian/Ubuntu平台本身提供了二进制库,可以直接git clone https://github.co ...

  4. $命令行参数解析模块argparse的用法

    argparse是python内置的命令行参数解析模块,可以用来为程序配置功能丰富的命令行参数,方便使用,本文总结一下其基本用法. 测试脚本 把以下脚本存在argtest.py文件中: # codin ...

  5. Google开源命令行参数解析库gflags

    Google开源命令行参数解析库gflags http://blog.csdn.net/lming_08/article/details/25072899 CMDLINE的解析 http://blog ...

  6. PHP 命令行参数解析工具类

    <?php/** * 命令行参数解析工具类 * @author guolinchao * @email luoyecb@163.com */class CommandLine{ // store ...

  7. Python命令行参数解析模块getopt使用实例

    Python命令行参数解析模块getopt使用实例 这篇文章主要介绍了Python命令行参数解析模块getopt使用实例,本文讲解了使用语法格式.短选项参数实例.长选项参数实例等内容,需要的朋友可以参 ...

  8. python命令行参数解析OptionParser类用法实例

    python命令行参数解析OptionParser类用法实例 本文实例讲述了python命令行参数解析OptionParser类的用法,分享给大家供大家参考. 具体代码如下:     from opt ...

  9. Python 中命令行参数解析工具 docopt 安装和应用

    什么是 docopt? 1.docopt 是一种 Python 编写的命令行执行脚本的交互语言. 它是一种语言! 它是一种语言! 它是一种语言! 2.使用这种语言可以在自己的脚本中,添加一些规则限制. ...

随机推荐

  1. git add提交时关于 LF will be replaced by CRLF in 问题出现的原因以及解决方式

    最近在新创建的github项目中add新框架代码时,出现了LF will be replaced by CRLF in的问题,以下为问题截图 查阅资料才知道,LF和FRLF是两种不同的换行格式,这个警 ...

  2. Golang-使用md5对字符串进行加密

    方式一: func md5Test1(str string) string { m5 := md5.New() _,err := m5.Write([]byte(str)) if err != nil ...

  3. Mycat分布式数据库架构解决方案--schema.xml详解

    echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加echo微信(微信号:t2421499075)交流学习. 百战不败,依不自称常胜,百败不颓,依能奋力前行.--这才是真正的堪称强大!!! 该文件 ...

  4. C++遍历磁盘驱动器

    #include <stdio.h> #include <windows.h> typedef struct tagDRIVER { // (1)磁盘盘符 wchar_t di ...

  5. Centos7安装Openresty和orange

    1.说明 以下全部操作均已root用户执行 2.安装 2.1 安装依赖 yum install readline-devel pcre-devel openssl-devel gcc 2.2 下载op ...

  6. 快速为不同 Git 平台配置用户

    在 ~ 目录下创建 config 文件可以为项目配置默认的用户信息,但如果希望经常切换,那么最好就是通过命令为项目单独设置用户. 我使用的 shell 是 zsh, 所以我在 ~/.zshrc 文件中 ...

  7. Java重试机制

    重试作用: 对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法.写操作等(要考虑写是否幂等)都不适合重试. 远程调用超时.网络突然中断可以重试.在微服务治理框架中,通常都有自己的重试与 ...

  8. Linux中通过ssh将客户端与服务端的远程连接

    前提需要:1.在VMware中装上两台linux虚拟机,本博客使用的都是CentOS 7.2.两部虚拟机可以通过命令ping通.3.两部虚拟机中已经通过yum本地仓库安装了sshd服务. 首先  1. ...

  9. C++(三十七) — 字符串的函数重载—案例

    1.MyString.h 头文件 #pragma once #include <iostream> using namespace std; class MyString { public ...

  10. pip---cannot import name main解决方案

    .先来看下Python不同版本对应的路径 which python3. which python3. .使用update-alternatives --install建立链接 sudo update- ...