《Go语言编程之旅》学习01--flag

日之朝矣

flag

os.Args

os.Args,是一个字符串类型的切片,是程序获取运行时给出的参数,第一个字符串是程序的位置,剩下的为参数,这也就是为什么后面总会取os.Args[1:],下面的 基本使用与长选项 第一个示例代码会打印一次os.Args,观察一下方便理解

基本使用与长短项

长短项指的就是在输入命令时,参数的长短选项,长短项的功能一般是相同的,短项是简单的写法

比如

1
2
3
# 这两个功能一般是一样的
-n
--name

至于前面短杠的数量,是命令行参数的风格,一个短杠加字母是unix风格,两个短杠加个单词或短语是GNU风格

第一种方便写,第二种更清楚

比如

1
2
-v
--version

以及,支持如下三种命令行标志语法,分别如下:

  • -flag:仅支持布尔类型。
  • -flag x :仅支持非布尔类型。
  • -flag=x:均支持
1
2
3
4
5
flag.StringVar(p *string, name string, value string, usage string)
// StringVar 定义了一个带有指定名称、默认值和用法字符串的字符串flag。 参数 p 指向一个string变量,用于存储flag的值。

flag.Parse()
// Parse 解析来自 os.Args[1:] 的命令行flag。 必须在定义所有flag之后和程序访问flag之前调用。

基本使用:

1
2
3
4
5
6
7
8
9
func main() {
var name string
flag.StringVar(&name, "name", "Go 语言编程之旅", "输出参数name")
flag.StringVar(&name, "n", "Go 语言编程之旅", "输出参数n")
flag.Parse()

log.Printf("args: %#v", os.Args) // 打印一下os.Args
log.Printf("name: %s", name)
}

在控制台执行

1
2
3
4
5
6
go run main.go -n 诶嘿
# []string{"C:\\Users\\XXX\\AppData\\Local\\Temp\\go-build1589801064\\b001\\exe\\main.exe", "-n", "诶嘿"}
# name: 诶嘿
go run main.go --name 诶嘿啊
# []string{"C:\\Users\\XXX\\AppData\\Local\\Temp\\go-build2749373213\\b001\\exe\\main.exe", "--name", "诶嘿啊"}
# name: 诶嘿啊

子命令

子命令从字面理解即可,看下下面的例子就能明白

1
2
flag.NewFlagSet(name string, errorHandling ErrorHandling)
// NewFlagSet 返回具有指定名称和错误处理属性的新的空标志集。 如果名称不为空,它将打印在默认用法消息和错误消息中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
flag.Parse() // 先解析一下参数

args := flag.Args() // flag.Args() 也就是解析后的命令行参数,即os.Args[1:]
if len(args) <= 0 {
return
}

switch args[0] {
case "go":
goCmd := flag.NewFlagSet("go", flag.ExitOnError)
goCmd.StringVar(&name, "name", "go语言", "输出参数name")
_ = goCmd.Parse(args[1:])
case "php":
phpCmd := flag.NewFlagSet("php", flag.ExitOnError)
phpCmd.StringVar(&name, "n", "php语言", "输出参数n")
_ = phpCmd.Parse(args[1:]) // FlagSet的Parse方法可以输入参数
}
log.Printf("name: %s", name)
}

执行

1
2
3
4
5
6
7
8
9
10
go run main.go go -name 诶嘿
# name: 诶嘿
go run main2.go php -n 诶嘿啊
# name: 诶嘿啊
go run main2.go php -name 诶嘿啊
# flag provided but not defined: -name
# Usage of php:
# -n string
# 输出参数n (default "php语言")
# exit status 2

run命令又何尝不是go命令的子命令呢

在调用flag.NewFlagSet()方法时,第二个参数是ErrorHandling,指定错误处理属性的空命令集,我们使用了flag.ExitOnError,也就是错误后直接退出程序,一共有三个

1
2
3
4
5
6
7
8
const{
// 返回错误描述
ContinueOnError ErrorHandling = iota
// 调用 os.Exit(2) 退出程序
ExitOnError
// 调用 panic 语句抛出错误异常
PanicOnError
}

深入分析flag

流程

下面代码中,英文注释均为源码自带,中文注释是我写的

flag.Parse与FlagSet.Parse

查看flag.Parse的代码,我们会发现是CommandLine.Parse()去解析

1
2
3
4
5
6
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func Parse() {
// Ignore errors; CommandLine is set for ExitOnError.
CommandLine.Parse(os.Args[1:])
}

CommandLine是通过NewFlagSet生成的一个FlagSet,而且默认使用了ExitOnError作为错误处理方式,程序一旦遇到错误会退出

进入FlagSet.Parse()会发现,这仍旧不是解析参数的主要部分,而是承担了 parse 方法的异常分流处理,而解析过程中遇到的一些特殊情况,例如:重复解析、异常处理等,均直接由该方法处理。实质的解析逻辑放在 parseOne 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
}

FlagSet.ParseOne()

最后到了FlagSet.ParseOne()中,这部分我们一段一段的来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (f *FlagSet) parseOne() (bool, error) {
if len(f.args) == 0 {
return false, nil
}
s := f.args[0]
if len(s) < 2 || s[0] != '-' {
return false, nil
}
numMinuses := 1
if s[1] == '-' {
numMinuses++
if len(s) == 2 { // "--" terminates the flags
f.args = f.args[1:]
return false, nil
}
}
name := s[numMinuses:]
if len(name) == 0 || name[0] == '-' || name[0] == '=' {
return false, f.failf("bad flag syntax: %s", s)
}
...
}

如果这个命令行参数长度为0

如果这个命令行参数长度小于2或第一个字符不是flag标识符-

如果这个命令行参数第二个字符也是-,就跳过这个-,进行后续处理,但此时如果该参数只有这两个-也就是长度为2,那便直接中断处理

跳过---后剩余字符长度为0 或第一个字符仍为- 或第一个字符为=这三种情况,直接按参数名不符合规则处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
// 接上面代码
// it's a flag. does it have an argument?
f.args = f.args[1:] // 注意此时f.args已经去掉了第一个参数
hasValue := false
value := ""
for i := 1; i < len(name); i++ { // equals cannot be first
if name[i] == '=' { // name是上一部分代码中,已经将'-'去掉的命令行参数
value = name[i+1:]
hasValue = true
name = name[0:i]
break
}
}
...

这一部分主要是处理命令行参数带=的情况,比如--name=rzzy,需要将参数名和参数值分开

1
2
3
4
5
6
7
8
9
m := f.formal // f.formal 为 map[string]*Flag,保存已经定义过的flag与对应的名字
flag, alreadythere := m[name] // BUG
if !alreadythere {
if name == "help" || name == "h" { // special case for nice help message.
f.usage()
return false, ErrHelp
}
return false, f.failf("flag provided but not defined: -%s", name)
}

这一部分是确定该参数是否定义过,如果没有定义过 或 没定义但该参数为h或者help,返回对应错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg
if hasValue {
if err := fv.Set(value); err != nil {
return false, f.failf("invalid boolean value %q for -%s: %v", value, name, err)
}
} else {
if err := fv.Set("true"); err != nil {
return false, f.failf("invalid boolean flag %s: %v", name, err)
}
}
} else {
// It must have a value, which might be the next argument.
if !hasValue && len(f.args) > 0 {
// value is the next arg
hasValue = true
value, f.args = f.args[0], f.args[1:] // 拿出此参数,并切换到下一个参数,下次循环就从新参数开始
}
if !hasValue {
return false, f.failf("flag needs an argument: -%s", name)
}
if err := flag.Value.Set(value); err != nil {
return false, f.failf("invalid value %q for flag -%s: %v", value, name, err)
}
}

这一部分是判断该命令行参数定义是否为布尔型,通过该 flag 所提供的 Value.Set 方法将参数值设置到对应的 flag 中去

非布尔型的,也同样将其flag 所提供的 Value.Set 方法将参数值设置到对应的 flag 中去

1
2
3
4
5
if f.actual == nil { // f.actual 为 map[string]*Flag,保存本次解析时用到的命令行参数名与flag对象
f.actual = make(map[string]*Flag)
}
f.actual[name] = flag
return true, nil

最后将参数名与该flag建立映射关系

自定义参数类型

判断命令行参数是否为布尔型时用到了flag.Value的断言,说明flag.Value是接口类型,也就是说我们可以自定义参数的类型,只需要实现一下接口方法即可

1
2
3
4
type Value interface {
String() string // 值的字符串形式
Set(string) error // 设置值
}

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Name string

func (n *Name) String() string {
return fmt.Sprint(*n)
}

func (n *Name) Set(value string) error {
*n = Name(value)
return nil
}

func main() {
var name Name
flag.Var(&name, "name", "帮助信息")
flag.Parse()

log.Printf("name: %s", name)
}

执行结果

1
2
go run main.go --name 测试
# name: 诶嘿:测试
  • 标题: 《Go语言编程之旅》学习01--flag
  • 作者: 日之朝矣
  • 创建于 : 2023-02-21 21:47:08
  • 更新于 : 2024-08-18 09:25:27
  • 链接: https://blog.rzzy.fun/2023/02/21/go-flag/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论