对于很多语言,都支持用命令行执行。
例如 PHP 支持用PHP命令 解析PHP脚本语言,Java 支持用 Java命令编译 Java代码,golang 也支持用 go 命令编译执行。
既然都支持用命令行执行,但是命令行又不像web 页面那样有输入框给我们填充参数,那么怎么让程序可以读取我们输入的值呢?
为了解决这样的问题,golang 为此提供了 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)
}
我们分别为对应的参数简单做一下解释
当我们用不同的方式去执行该程序时,会输出什么的结果呢?
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
我们经常会发现,很多命令都会提供各种子命令。例如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
// 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
}
分别对 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 类型为例。
type Getter interface {
Value
Get() interface{}
}
type Value interface {
String() string
Set(string) error
}
// 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 // 默认值
}
// 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 的子命令。
当我们调用 flag.parse 时,实际上是注册了一个最基础的命令行实例(实际上也是一个flagset)(你也可以先定义好相关的命令行参数再注册)
而其余的子命令都是通过创建新的 flagSet 去实现的。
其中的逻辑主要体现在 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)
}
...
其实就是对入参进行各种方式的校验,其校验规则如下:
// 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 中。