go读取命令行参数

背景

希望有一天可以使用go来写命令行软件

os包

os包提供简单的参数引用.

  1. 全部解析为字符串
  2. 不区分具名选项与其他参数
  3. 第0个是程序名
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
if len(os.Args) > 2 {
fmt.Println(reflect.TypeOf(os.Args[1]))
fmt.Println(os.Args)
}
}

/*
go run main.go 1 b c

string
[/tmp/xxxxx/xx/exe/main 1 b c]
*/

flag包

标准库中的flag包,借助os包获取参数.
并提供一套还算合适的API来定义,解析,获取参数.

功能包括

  1. 参数名,类型,默认值的设置
  2. 帮助文档的批量显示(支持自定义)
  3. 参数的解析

一些缺点:

  1. 不支持长短参数的区分
  2. 匿名参数支持不好
  3. 不负责解决参数的逻辑关系
    1. 相互冲突的参数
    2. 相互依赖的参数
    3. 不同名称,同一意义如何处理

基础使用

支持3种方法获取用户的输入

  1. flag.XxxVar 方法读入到预先定义的变量中
  2. flag.Xxx 方法,返回值是个指针,可以保存在预先定义的变量中
  3. 无论是否保存到变量中,后续使用 flag.Lookup("选项名") 获取flag的实例,其中 Value 代表flag的值.
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
26
27
28
29
30
31
32
33
34
var (
h bool
q bool
m *string
)

func init() {
// XxxVar方法,允许读取参数到一个变量中
flag.BoolVar(&h, "h", false, "help flag")
flag.BoolVar(&q, "q", false, "be quiet")

// 不带Var的版本,直接返回一个指针的类型
// 使用时也需要使用*m来表示参数的值
// 即便这只是一个简单类型string,有点违和
m = flag.String("mode", "debug", "app `mode`: debug, master")

flag.String("t", "", "test flag")
}

func main() {
// 不解析则无法获得用户的输入,只使用默认值
flag.Parse()

if h { // 直接使用变量
fmt.Println("h enabled")
}
if q {
fmt.Println("q enabled")
}
fmt.Println(*m) // 不得不使用指针

tFlag := flag.Lookup("t") // 直接获得实例的指针
fmt.Println(tFlag.Value) // 可用DefValue,Name,Value,Usage等属性
}

这样就能使用如下命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开关型参数
./main # h 为false
./main -h # h 为true

# 不支持短参数,一律认为长参数
./main -qh # 报错
./main -q -h # q,h分别为true
./main -mode master # m 为"master"
./main --mode master # m 为"master"

# 带不带等号均可
./main # m 为默认的"debug"
./main --mode master # m 为"master"
./main --mode=master # m 为"master"

显示参数帮助信息

flag.Usage() 是一个自带print功能的函数,使用之即可打印一套格式化后的帮助信息.
且该函数可以被自定义,替换时直接替换掉函数即可.
自定义函数内部,可以用 flag.PrintDefaults() 输出默认的格式化后的帮助信息(进参数部分)
默认的格式化信息包括

  1. 参数名
  2. 形式参数名
  3. 说明语句
  4. 默认值
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
26
27
28
29
30
31
32
33
var (
h bool
q bool
m string
s int
)

func init() {
flag.BoolVar(&h, "h", false, "help flag")

flag.StringVar(&m, "m", "debug", "mapp `mode` (shorthand)") // 反引号中内容会作为flag后的形式参数名
flag.StringVar(&m, "mode", "debug", "app `mode`: debug, master") // 会显示成看似没有关系的两个参数
// flag.StringVar(&s, "s", "", "test flag") // 如果没有反引号,会以类型作为形参名
flag.IntVar(&s, "s", 0, "test flag")

flag.Usage = customUsage
}

func main() {
// 不解析则无法获得用户的输入,只使用默认值
flag.Parse()

if h {
flag.Usage()
}
}

func customUsage() {
fmt.Fprintln(os.Stderr, `customize usage of main
Options:
`)
flag.PrintDefaults()
}

特殊情况处理

不同参数名解析到同一个变量

不负责解决冲突,只显示最后一个有效值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var (
m string
)

func init() {
flag.StringVar(&m, "m", "debug", "mapp `mode` (shorthand)")
flag.StringVar(&m, "mode", "debug", "app `mode`: debug, master")
}

func main() {
// 不解析则无法获得用户的输入,只使用默认值
flag.Parse()

fmt.Println(m)
}

使用时效果如下

1
2
./main -m v1 -mode v2                     # v2
./main -mode v1 -m v2 # v2

相同参数名指向不同的变量

报错

定义了int类型,但输入不是int的

报错

自定义参数解析方式

假设要定义一个类型为 []string

  1. 需要满足 flag.Value 接口,因此先起一个别名,方便定义方法
  2. 需要定义的方法有 Set, String
    1. Set 输入一个string,输出一个error,过程中要将处理结果存入自定义类型
    2. String 用于可视化
  3. 正常流程
    1. 声明参数类型
    2. 解析到该类型的指针中
    3. 取出内容使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type sliceValue []string

func (s *sliceValue) Set(val string) error {
*s = sliceValue(strings.Split(val, ","))
return nil
}

func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }

var languages sliceValue

func init() {
flag.Var(&languages, "l", "programming `languages` that I like")
}

func main() {
flag.Parse()
fmt.Println(*&languages)
}

匿名参数

flag对匿名参数提供有限的支持

  1. flag.NArgs() 表示匿名参数长度
  2. flag.Args() 获取所有匿名参数列表
  3. flag.Arg(i int) 表示第i个匿名参数

flag对匿名参数的支持不好,体现在

  1. 不能将匿名参数绑定到变量
  2. 一旦碰到匿名参数就会停止解析,即便之后有具名参数,写命令时需要非常小心
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var (
h bool
m string
)

func init() {
flag.BoolVar(&h, "h", false, "h flag")
flag.StringVar(&m, "m", "debug", "app `mode`: debug,master ")
}
func main() {
flag.Parse()
fmt.Println(h)
fmt.Println(m)

fmt.Println(flag.Args()) // 获取所有匿名参数

if flag.NArg() > 0 { // 匿名参数个数
for i := 0; i < flag.NArg(); i++ {
fmt.Println(flag.Arg(i)) // 获取每个匿名参数
}
}
}

使用时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 需要将匿名参数写在后边,才能让flag.Args()存储剩余的,未解析的参数
# 然后依靠位置关系0,1,2等确定每个参数的意义
./main -h -m master hello world
# true
# master
# [hello world]
# hello
# world


# 从第一个匿名参数起,flag不再解析所有参数
go run main.go -m master hello -h world
# false # h没能被解析到
# master # -m能够被解析
# [hello -h world] # hello之后的都没能解析
# hello
# -h
# world

FlagSet

flag包提供的功能本质上基于 FlagSet 类,
只不过向外暴露的一个个函数,调用内部的 FlagSet 实例上的方法.

默认的FlagSet实例使用os.Args中的相关信息来生成.

1
2
3
4
5
6
7
8
9
10
11
12
13
var CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
var (
h bool
)

func init() {
CommandLine.BoolVar(&h, "h", false, "h flag")
}

func main() {
CommandLine.Parse(os.Args[1:])
fmt.Println(h)
}

一些说明

  1. 在一个程序中自定义两套FlagSet不是特别容易的事情,
    用户几乎无法正好将一些意义的内容放在合适的次序上.
    直接用包默认提供的flagset即可
  2. FlagSet是一个较为复杂的结构体,暂时略过其他的成员和方法解说.

参考

  1. 主要参考
  2. 较为详尽的解释