Golang Argparse 开发记录

发布 : 2021-05-08 分类 : basics 浏览 : --

在用了几个 golang 的命令行解析包之后,还是决定自己造一个轮子,仿照python版本的 argparse, 用来解析命令行

项目地址: https://github.com/hellflame/argparse

一、项目结构

命令行解析只有两个主要部分,一部分是和命令行参数相关的结构体,另一部分是关于命令行语义解析的方法。前者是命令行参数的承载体,后者是驱动解析进行下去的动力,也是人机交互的入口。即 argparse 被刚好分成 arg & parse 两部分。

命令行参数的结构体作用比较简单,核心流程是注册 -> 遍历 -> 解析 -> 赋值 。核心在于保管命令行解析结果的指针,用于返回命令行解析结果,其他的附属功能包括创建命令行、创建命令的正确性检查、解析命令行中字符串的值以及执行预设动作等。对应于 argparse 中的 arg 部分。

命令行的语义解析是项目中最复杂的部分,复杂程度大概是因为有一个大概6层的代码缩进和一个递归。虽然原理本身并不复杂,但需要同时处理 位置参数可选参数标志参数副命令 以及处理解析过程中的异常情况,难免需要很多分支和循环,甚至递归。

二、语义解析

命令行的核心就是语义解析,就像一门编程语言中的令牌解析一样。这里采用了将命令行关键词提前注册,通过遍历命令行的形式来完成整个过程。

Parser 结构体声明像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Parser struct {
name string
description string
showHelp *bool
config *ParserConfig

entries []*arg
entryMap map[string]*arg
positionArgs []*arg

entryGroupOrder []string
entryGroup map[string][]*arg

subParser []*Parser
subParserMap map[string]*Parser
parentList []string
}

结构体中除了 name, description 等基本设置之外,有两个位置无关的注册字典以及1个位置参数列表,分别如下:

  1. entryMap 字典会保存位置参数,将类似 -h--help 的字符串作为 key , 与之对应的参数结构体实例作为 value ,对应的方法用来处理剩下来的参数
  2. subParserMap 字典会保存 副命令 参数,将类似 testmod 类似的副命令入口作为 key ,保存下另一个 Parser 实例,一个全新的解析环境
  3. positionArgs 列表会保存位置参数列表,比较特别的是这个列表内容的添加顺序就是位置参数的解析顺序,在实际解析的过程中需要保存位置参数的解析位置

还有一个没有说的字典 entryGroup,这个字典主要保存命令行参数的分类信息,但因为参数的分组信息有保存在 arg 实例上,所以这算是一种冗余。这个字典主要是方便生成帮助信息,后续可以考虑去掉,在每次生成帮助信息的时候再构建分组信息。

在遍历用户输入参数的时候,需要一个参数一个参数的处理,决定对接下来的输入参数作何处理,以及接下来参数的位置 (这里说了两个 参数, 一个是用户输入的参数,如 python entry.py --help --date 20210101 中的 --help --date 20210101 都是用户输入的参数,一个是程序解析的参数,表示代码中的每一个 arg 实例)

针对可选参数,即 entryMap 里注册的参数,在匹配到参数时有三种处理接下来参数的情况,一种是不需要任何参数,即 Flag 类型的标志性参数,这种参数一旦出现,即表示 true ,比如 --help ;一种是只需要一个参数,即接受单个参数的可选参数,如 --date 20210101 中的 --date ,与之绑定的 arg 实例需要一个参数参与解析,得到结果,此时需要判断 --date 之后是否有需要的参数,如果没有,那么就是用户输入有误,如果有,那么尝试处理这个参数,并且将下一个即将解析的用户参数索引号更新到20210101 之后;最后一种是可以接受多个参数的可选参数,如 --date-list 20210101 20200501 ,和前者除了将解析的用户参数索引更新到 20200501 之后外,基本一致。

在处理可选参数过程中,有可能遇到 Flag 类型后面还有额外的未注册参数,或者在只接受一个参数的可选参数获取一个参数之后依然有未注册参数,那么这个时候就要考虑这个多出来的值是否为 位置参数 了。位置参数也分为两种,一种仅接受一个参数作为值,一种则可接受多个值。对于位置参数,需要在解析时记录已经解析的位置参数,因为可能存在创建了多个位置参数的情况,甚至更复杂一点的创建了单参数位置参数+多参数位置参数的情况。

对于 subParserMap ,每次解析都需要判断第一个用户输入的参数是否在 副命令 注册表里存在,如果存在,那么剩下的参数都要用于副命令的参数解析。对的,这里需要用到递归,执行副命令的解析动作。

在每次解析动作的结尾,需要对以上提到的可选参数注册表以及位置参数列表进行便利,检查对 Required 的必填项是否已填以及 Default 默认参数的处理,还有副命令相关的 RequiredDefault 相关的检查和处理,反馈解析结果。

三、参数解析

参数解析主要就是对已经匹配到的用户输入参数处理并通过指针,一层层传递到用户创建这个参数的入口去。

相关结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type arg struct {
short string
full string
target interface{}
assigned bool
Option
}

// Option is the only type to config when creating argument
type Option struct {
Meta string // meta value for help/usage generate
multi bool // take more than one argument
Default string // default argument value if not given
isFlag bool // use as flag
Required bool // require to be set
Positional bool // is positional argument
Help string // help message
Group string // argument group info, default to be no group
Action func(args []string) error // bind actions when the match is found, 'args' can be nil to be a flag
Choices []interface{} // input argument must be one/some of the choice
Validate func(arg string) error // customize function to check argument validation
Formatter func(arg string) (interface{}, error) // format input arguments by the given method
}

针对参数处理,只要按照正确的流程进行即可,这里包括:

  1. 标记已解析, assigned = true
  2. 既定 Action 响应,一旦匹配,即返回执行结果
  3. Flag 类型立即绑定 true 并结束解析
  4. 如果该参数接受参数但无输入,并且存在默认值 Default,则将默认值添加到待解析参数中
  5. 如果存在 Validate,则通过该函数依次判断每一个待解析参数的有效性
  6. 如果存在 Formatter,则依次通过该函数依次处理每一个待解析参数,否则按照绑定参数的类型处理每一个待解析参数,如字符串转换为 IntFloat32 类型等
  7. 如果存在 Choices ,则依次检查上一步处理好的参数是否都在 Choice 中存在,否则提示用户输入有误
  8. 通过指针传递已解析的结果到创建参数的入口

以上流程对应 func (a *arg) parseValue(values []string) error 这个方法的实现过程

四、最后亿点点

有一些小细节的处理:

  1. 帮助信息中对每一个参数的帮助说明换行缩进处理
  2. 创建参数时的名称有效性判定,包括名称的有效性和矛盾检查
  3. interface 指针传值时的类型转换
  4. metavar 支持
  5. 帮助信息的出现时机判定,甚至是否出现
  6. 帮助信息 Flag 的注册
  7. 自定义的使用缩略和自动生成的使用缩略
  8. shell自动补全脚本生成
  9. 编辑距离提示参数修正
  10. 单元测试 + 覆盖率

还要处理时不时有人提出意见,取舍,维护 =.= 就很难

本文作者 : hellflame
原文链接 : https://hellflame.github.io/2021/05/08/golang-argparse/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
留下足迹
点击通过issue留言