Go命令行实现工具- flag大法详解

一、flag 是什么东西?

对于很多语言,都支持用命令行执行。
例如 PHP 支持用PHP命令 解析PHP脚本语言,Java 支持用 Java命令编译 Java代码,golang 也支持用 go 命令编译执行。

既然都支持用命令行执行,但是命令行又不像web 页面那样有输入框给我们填充参数,那么怎么让程序可以读取我们输入的值呢?

为了解决这样的问题,golang 为此提供了 flag 标准库。

它的主要功能是实现命令行参数的解析。

二、flag 入门?

我们简单举一个简单的例子:

package main

import "flag"
import "log"

func main() {
	var name string
	flag.StringVar(&name, "name", "default value", "this is help message")
	flag.StringVar(&name, "n", "default value", "this is help message")
	flag.Parse() 

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

}

我们定义了一个 string 类型的 name 变量,通过 flag 包的 StringVal 方法实现了对命令行参数 name 的解析和绑定

我们可以看下该方法官方的含义

// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
func StringVar(p *string, name string, value string, usage string) {
	CommandLine.Var(newStringValue(value, p), name, usage)
}

我们分别为对应的参数简单做一下解释

  • p*string :需传入一个string类型的值指针
  • name string:命令行参数的名称
  • value string :命令行参数的默认值
  • usage string: 命令行参数的解释说明

当我们用不同的方式去执行该程序时,会输出什么的结果呢?

go run main.go -name=MClink -n=xiaolu
name: xiaolu

go run main.go -name MClink
name: MClink

go run main.go
name: default value

o run main.go -name
flag needs an argument: -name
Usage of /var/folders/dr/tx8gmnj12ll499q3jydgp15h0000gn/T/go-build3300144254/b001/exe/main:
  -n string
    	this is help message (default "default value")
  -name string
    	this is help message (default "default value")
exit status 2

这里你会发现,我们对 name 这个命令行参数进行了两次绑定,一个是 name ,一个是 n。
这是因为大多数目前流行的命令都会有长短选项使用,例如常见的

xxx -help 和 xxx -h

三、flag 子命令实现

我们经常会发现,很多命令都会提供各种子命令。例如mac的brew 命令

brew
Example usage:
  brew search TEXT|/REGEX/
  brew info [FORMULA|CASK...]
  brew install FORMULA|CASK...
  brew update
  brew upgrade [FORMULA|CASK...]
  brew uninstall FORMULA|CASK...
  brew list [FORMULA|CASK...]

Troubleshooting:
  brew config
  brew doctor
  brew install --verbose --debug FORMULA|CASK

Contributing:
  brew create URL [--no-fetch]
  brew edit [FORMULA|CASK...]

Further help:
  brew commands
  brew help [COMMAND]
  man brew
  https://docs.brew.sh

那么 flag 如何实现这种命令呢,我们举个例子:

package main

import (
	"flag"
	"log"
)

func main() {
	var do string
	flag.Parse()
	args := flag.Args()
	if len(args) <= 0 { // 没有输入参数直接返回
		return
	}

	switch args[0] {
	case "study":
		studyCmd := flag.NewFlagSet("study", flag.ExitOnError)
		studyCmd.StringVar(&do, "name", "学习", "学习帮助") // study 子命令注册了 name
		_ = studyCmd.Parse(args[1:])
	case "game":
		gameCmd := flag.NewFlagSet("game", flag.ExitOnError)
		gameCmd.StringVar(&do, "n", "玩游戏", "游戏攻略") // game 子命令注册了 n
		_ = gameCmd.Parse(args[1:])
	}
	log.Printf("do %s ", do)
}

简单解释一下上面的代码。我们通过 flag.Args() 获取了输入的参数列表,第一个参数用来区别不同的子命令。
然后通过 flag.NewFlagSet 创建了一个新的命令集去支持子命令。再分别为两个不同的子命令定义了不同的命令参数。

其中,需要注意的是 flag.NewFlagSet 的第二个参数是用来指定处理异常错误的不同策略的。分别是:

// These constants cause FlagSet.Parse to behave as described if the parse fails.
const (
    // 返回错误的描述
	ContinueOnError ErrorHandling = iota // Return a descriptive error.
	// 退出程序
	ExitOnError                          // Call os.Exit(2) or for -h/-help Exit(0).
	// 抛出 Panic
	PanicOnError                         // Call panic with a descriptive error.
)

执行结果如下:

go run main.go study -name="学习啦"
2021/11/28 17:56:00 do 学习啦

go run main.go game -n="打游戏呀"
2021/11/28 17:56:34 do 打游戏呀

go run main.go game -name="打游戏呀"
flag provided but not defined: -name
Usage of game:
  -n string
        游戏攻略 (default "玩游戏")
exit status 2

四、源码理解

1. 错误部分

// ErrHelp当某个flag没有定义但是是直接调用-help或-h标志时返回的错误
var ErrHelp = errors.New("flag: help requested")

//如果一个标志的值无法解析,比如 Int 的整数无效,则由 Set 返回 errParse。
//可以通过失败来包装它以提供更多信息。
var errParse = errors.New("parse error")

//如果 flag 的值超出范围,则由Set返回errRange。
//然后通过失败来包装它以提供更多信息。
var errRange = errors.New("value out of range")

// 判断错误类型,返回自定义的类型错误
func numError(err error) error {
	ne, ok := err.(*strconv.NumError)
	if !ok {
		return err
	}
	if ne.Err == strconv.ErrSyntax {
		return errParse
	}
	if ne.Err == strconv.ErrRange {
		return errRange
	}
	return err
}

2.常用类型的封装

分别对 bool、int、int64、uint、uint64、string、float64、time.Duration、func(string) error 进行了封装

// -- int Value
type intValue int

func newIntValue(val int, p *int) *intValue {
	*p = val
	return (*intValue)(p)
}

func (i *intValue) Set(s string) error {
	v, err := strconv.ParseInt(s, 0, strconv.IntSize)
	if err != nil {
		err = numError(err)
	}
	*i = intValue(v)
	return err
}

func (i *intValue) Get() interface{} { return int(*i) }

func (i *intValue) String() string { return strconv.Itoa(int(*i)) }

我们以 int 类型为例。

  • 首先定义了 intValue 的 int 类型
  • 提供构造方法获取 intValue 的实例
  • 实现 Getter 接口的 Set、Get、String 方法
type Getter interface {
	Value
	Get() interface{}
}

type Value interface {
	String() string
	Set(string) error
}

3. FlagSet 和 Flag

// FlagSet表示一组已定义的 flag 。FlagSet的零值没有 name,但是有ContinueOnError错误处理。
//flag 的 name 在一个 flagset 中必须是唯一的。试图定义一个 flag ,其 name 已经被使用会引起 panic。
type FlagSet struct {
// Usage是在解析标志时发生错误时调用的函数。
//该字段是一个函数(而不是方法),可以更改为指向
//自定义错误处理程序。用法之后会发生什么取决于
//在ErrorHandling设置;对于命令行,默认值为ExitOnError,它在调用Usage后退出程序。
	Usage func()

	name          string
	parsed        bool
	actual        map[string]*Flag // 使用的时候存的
	formal        map[string]*Flag // 定义flag存的
	args          []string // arguments after flags
	errorHandling ErrorHandling
	output        io.Writer // nil means stderr; use Output() accessor
}

// 单个 flag 的相关属性定义
type Flag struct {
	Name     string // 在命令行中显示的名称
	Usage    string // 帮助信息
	Value    Value  // 设置的值
	DefValue string // 默认值
}

4.初始化

// CommandLine 是默认的命令行 flags, 参数解析来源是 os.Args.
// 顶层的方法例如 BoolVar, Arg 等等 都是为了 CommandLine 的方法包装的.
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func init() {
//可以覆盖默认的的 Usage。
//这里需要注意的是, CommandLine.Usage 并不是等于 Usage,
//因为我们希望最终调用使用任何更新的Usage值,不是运行这一行时的值。
	CommandLine.Usage = commandLineUsage
}

func commandLineUsage() {
	Usage()
}

五、 核心流程

我们在前面的栗子描述了怎么去实现 flag 的子命令。

1.核心调用链

其中的调用链如下:
Go命令行实现工具- flag大法详解_第1张图片

当我们调用 flag.parse 时,实际上是注册了一个最基础的命令行实例(实际上也是一个flagset)(你也可以先定义好相关的命令行参数再注册)
而其余的子命令都是通过创建新的 flagSet 去实现的。
其中的逻辑主要体现在 flagSet.ParseOne 方法中。

2.flagSet.ParseOne 解析

// parseOne parses one flag. It reports whether a flag was seen.
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] == '-' { // -- X的格式
		numMinuses++
		if len(s) == 2 { // 只有 -- 不给过
			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)
	}

	// 判断是否有参数
	f.args = f.args[1:]
	hasValue := false
	value := ""
	for i := 1; i < len(name); i++ { // 等于号不能是第一个
		if name[i] == '=' {
			value = name[i+1:]
			hasValue = true
			name = name[0:i]
			break
		}
	}
	m := f.formal
	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)
	}

	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)
		}
	}
	if f.actual == nil {
		f.actual = make(map[string]*Flag)
	}
	f.actual[name] = flag
	return true, nil
}

代码比较长,可以分成三部分;

1)参数校验

	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] == '-' { // -- X的格式
		numMinuses++
		if len(s) == 2 { // 只有 -- 不给过
			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 标识符不是 “-”
  • flag 标志位为 “–”, 直接跳过
  • 如果 -x x不符合规则,x没传,或者 x为 -,或者x 为 = ,则触发 failf 方法进行中断,其中 failf 方法中调用了 usage 方法,如下所示
// failf prints to standard error a formatted error and usage message and
// returns the error.
func (f *FlagSet) failf(format string, a ...interface{}) error {
	err := fmt.Errorf(format, a...)
	fmt.Fprintln(f.Output(), err)
	f.usage()
	return err
}

2)命令解析

...
	//   判断是否有参数
	f.args = f.args[1:]
	hasValue := false
	value := ""
	for i := 1; i < len(name); i++ { // 等于号不能是第一个
		if name[i] == '=' {
			value = name[i+1:]
			hasValue = true
			name = name[0:i]
			break
		}
	}
...

主要是解析出 name 和 value的值,通过判断 = 前后的元素

3)对帮助信息单独处理

    ...
	m := f.formal
	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)
	}
	...

当 name 是 help 或者是 h 时,特殊处理,调用 usage 方法,或者从 flag 池中没有找到提前定义的flag(使用val 定义的会存在 flag 池中),则中断报错,调用 failf 方法

4)设置参数值

...
	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)
		}
	}
...

在设置参数的时候,首先先对类型进行判断,如果是布尔类型则单独用定时话的boolFlag来进行处理。其他的就通过其对应类型的 Set 方法将参数值设置到对应的 flag 中。

你可能感兴趣的:(Go大法,golang,开发语言,后端)