Go 命令行解析 flag 包之经过子命令实现看 go 命令源码

上篇文章 介绍了 flag 中如何扩展一个新的类型支持。本篇介绍如何使用 flag 实现子命令,总的来讲,这篇才是这个系列的核心,前两篇只是铺垫。git

前两篇文章连接以下:github

Go 命令行解析 flag 包之快速上手
Go 命令行解析 flag 包之扩展新类型golang

但愿看完本篇文章,若是再阅读 go 命令的实现源码,至少在总体结构上不会迷失方向了。数组

FlagSet

正式介绍子命令的实现以前,先了解下 flag 包中的一个类型,FlagSet,它表示了一个命令。bash

从命令的组成要素上看,一个命令由命令名、选项 Flag 与参数三部分组成。相似以下:app

$ cmd --flag1 --flag2 -f=flag3 arg1 arg2 arg3
复制代码

FlagSet 的定义也正符合了这一点,以下:框架

type FlagSet struct {
	// 打印命令的帮助信息
	Usage func() // 命令名称 name string parsed bool // 实际传入的 Flag actual map[string]*Flag // 会被使用的 Flag,经过 Flag.Var() 加入到了 formalformal map[string]*Flag // 参数,Parse 解析命令行传入的 []string, // 第一个不知足 Flag 规则的(如不是 - 或 -- 开头), // 从这个位置开始,后面都是 args []string // arguments after flags // 发生错误时的处理方式,有三个选项,分别是 // ContinueOnError 继续 // ExitOnError 退出 // PanicOnError panic errorHandling ErrorHandling output io.Writer // nil means stderr; use out() accessor } 复制代码

包含字段有命令名 name,选项 Flag 有 formalactual,参数 args函数

若是有人说,FlagSet 是命令行实现的核心,仍是比较认同的。之因此前面一直没有提到它,主要是 flag 包为了简化命令行的处理流程,在 FlagSet 上作了进一步的封装,简单的使用能够直接无视它的存在。oop

flag 中定义了一个全局的 FlagSet 类型变量,CommandLine,用它表示整个命令行。能够说,CommandLineFlagSet 的一个特例,它的使用模式较为固定,因此在它之上能提供了一套默认的函数。fetch

前面已经用过的一些,好比下面这些函数。

func BoolVar(p *bool, name string, value bool, usage string) {
	CommandLine.Var(newBoolValue(value, p), name, usage)
}

func Bool(name string, value bool, usage string) *bool {
	return CommandLine.Bool(name, value, usage)
}

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

更多的,这里不一一列举了。

接下来,咱们来脱掉这层外衣,梳理下命令行的整个处理流程吧。

流程解读

CommandLine 的整个使用流程主要由三部分组成,分别是获取命令名称、定义命令中的实际选项和解析选项。

命令名称在 CommandLine 建立的时候就已经指定了,以下:

CommandLine = NewFlagSet(os.Args[0], ExitOnError)
复制代码

名称由 os.Args[0] 指定,即命令行的第一个参数。除了命令名称,同时指定的还有出错时的处理方式,ExitOnError

接着是定义命令中实际会用到的 Flag

核心的代码是 FlagSet.Var(),以下所示:

func (f *FlagSet) Var(value Value, name string, usage string) {
	// Remember the default value as a string; it won't change.
	flag := &Flag{name, usage, value, value.String()}

	// ...
	// 省略部分代码
	// ...

	if f.formal == nil {
		f.formal = make(map[string]*Flag)
	}
	f.formal[name] = flag
}
复制代码

以前使用过的 flag.BoolVarflag.Bool 都是经过 CommandLine.Var(),即 FlagSet.Var(), 将 Flag 保存到 FlagSet.formal 中,以便于以后在解析的时候能将值成功设置到定义的变量中。

最后一步是从命令行中解析出选项 Flag。因为 CommandLine 表示的是整个命令行,因此它的选项和参数必定是从 os.Args[1:] 中解析。

flag.Parse 的代码以下:

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

如今的重点是要了解 flag 中选项和参数的解析规则,如 gvg -v list,按什么规则肯定 -v 是一个 Flag,而 list 是参数的呢?

若是继续向下追 Parse 的源码,在 FlagSet.parseOne 中将发现 Flag 的解析规则。

func (f *FlagSet) ParseOne() 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
		}
	}
	// ...
}
复制代码

三种状况下会终止解析 Flag,分别是当命令行参数所有解析结束,即 len(f.args) == 0,或长度小于 2,但第一位字符不是 -,或者参数长度等于 2,且第二个字符是 -。以后的内容会继续看成命令行参数处理。

若是没有子命令,命令的解析工做到此就基本完成了,再日后就是业务代码的开发了。那若是 CommandLine 还有子命令呢?

子命令

子命令和 CommandLine 不管是形式仍是逻辑上,基本没什么差别。形式上,子命令一样包含选项和参数,逻辑上,子命令的选项和参数的解析规则与 CommandLine 相同。

一个包含子命令的命令行,形式以下:

$ cmd --flag1 --flag2 subcmd --subflag1 --subflag2 arg1 arg2
复制代码

从上面能够看出,若是 CommandLine 包含了子命令,能够理解为自己也就没了参数,由于 CommandLine 的第一个参数便是子命令的名称,而以后的参数要解析为子命令的选项参数了。

如今,子命令的实现就变得很是简单了,建立一个新的 FlagSet,将 CommandLine 中的参数按前面介绍的流程从新处理一下。

第一步,获取 CommandLine.Arg(0),检查是否存在相应的子命令。

func main() {
	flag.Parse()
	if h {
		flag.Usage()
		return
	}

	cmdName := flag.Arg(0)
	switch cmdName {
	case "list":
		_ = list.Exec(cmdName, flag.Args()[1:])
	case "install":
		_ = install.Exec(cmdName, flag.Args()[1:])
	}
}
复制代码

子命令的实现定义在另一个包中,以 list 命令为例。 代码以下:

var flagSet *flag.FlagSet

var origin string

func init() {
	flagSet = flag.NewFlagSet("list", flag.ExitOnError)
	val := newStringEnumValue("installed", &origin, []string{"installed", "local", "remote"})
	flagSet.Var(
		val, "origin",
		"the origin of version information, such as installed, local, remote",
	)
}
复制代码

上面的代码中,定义了 list 子命令的 FlagSet,并在 Init 方法为其增长了一个选项 Flagorigin

Run 函数是真正执行业务逻辑的代码。

func Run(args []string) error {
	if err := flagSet.Parse(args); err != nil {
		return err
	}

	fmt.Println("list --oriign", origin)
	return nil
}
复制代码

最后的 Exec 函数组合 InitRun 函数,已提供给 main 调用。

func Run(name string, args []string) error {
	Init(name)
	if err := Run(args); err != nil {
		return err
	}

	return nil
}
复制代码

命令行的解析完成,若是子命令还有子命令,处理的逻辑依然相同。接下来的工做,就能够开始在 Run 函数中编写业务代码了。

Go 命令

如今,阅读下 Go 命令的实现代码吧。

因为大佬们写的代码是基于 flag 包实现纯手工打造,没用任何的框架,在可读性上会有点差。

源码位于 go/src/cmd/go/cmd/main.go 下,经过 base.Go 变量初始化了 Go 支持的全部命令,以下:

base.Go.Commands = []*base.Command{
	bug.CmdBug,
	work.CmdBuild,
	clean.CmdClean,
	doc.CmdDoc,
	envcmd.CmdEnv,
	fix.CmdFix,
	fmtcmd.CmdFmt,
	generate.CmdGenerate,
	modget.CmdGet,
	work.CmdInstall,
	list.CmdList,
	modcmd.CmdMod,
	run.CmdRun,
	test.CmdTest,
	tool.CmdTool,
	version.CmdVersion,
	vet.CmdVet,

	help.HelpBuildmode,
	help.HelpC,
	help.HelpCache,
	help.HelpEnvironment,
	help.HelpFileType,
	modload.HelpGoMod,
	help.HelpGopath,
	get.HelpGopathGet,
	modfetch.HelpGoproxy,
	help.HelpImportPath,
	modload.HelpModules,
	modget.HelpModuleGet,
	modfetch.HelpModuleAuth,
	modfetch.HelpModulePrivate,
	help.HelpPackages,
	test.HelpTestflag,
	test.HelpTestfunc,
}
复制代码

不管是 go 命令,仍是它的子命令,都是 *base.Command 类型。能够看一下 *base.Command 的定义。

type Command struct {
	Run func(cmd *Command, args []string) UsageLine string Short string Long string Flag flag.FlagSet CustomFlags bool Commands []*Command } 复制代码

主要的字段有三个,分别是 Run,主要负责业务逻辑的处理,FlagSet,负责命令行的解析,以及 []*Command, 所支持的子命令。

再来看看 main 函数中的核心逻辑。以下:

BigCmdLoop:
for bigCmd := base.Go; ; {
	for _, cmd := range bigCmd.Commands {
		// ...
		// 主要逻辑代码
		// ...
	}

	// 打印帮助信息
	helpArg := ""
	if i := strings.LastIndex(cfg.CmdName, " "); i >= 0 {
		helpArg = " " + cfg.CmdName[:i]
	}
	fmt.Fprintf(os.Stderr, "go %s: unknown command\nRun 'go help%s' for usage.\n", cfg.CmdName, helpArg)
	base.SetExitStatus(2)
	base.Exit()
}
复制代码

从最顶层的 base.Go 开始,遍历 Go 的全部子命令,若是没有相应的命令,则打印帮助信息。

省略的那段主要逻辑代码以下:

for _, cmd := range bigCmd.Commands {
	// 若是找不到命令,继续下次循环
	if cmd.Name() != args[0] {
		continue
	}
	// 检查是否存在子命令
	if len(cmd.Commands) > 0 {
		// 将 bigCmd 设置为当前的命令
		// 好比 go tool compile,cmd 即为 compile
		bigCmd = cmd
		args = args[1:]
		// 若是没有命令参数,则说明不符合命令规则,打印帮助信息。
		if len(args) == 0 {
			help.PrintUsage(os.Stderr, bigCmd)
			base.SetExitStatus(2)
			base.Exit()
		}
		// 若是命令名称是 help,打印这个命令的帮助信息
		if args[0] == "help" {
			// Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
			help.Help(os.Stdout, append(strings.Split(cfg.CmdName, " "), args[1:]...))
			return
		}
		// 继续处理子命令
		cfg.CmdName += " " + args[0]
		continue BigCmdLoop
	}
	if !cmd.Runnable() {
		continue
	}
	cmd.Flag.Usage = func() { cmd.Usage() }
	if cmd.CustomFlags {
		// 解析参数和选项 Flag
		// 自定义处理规则
		args = args[1:]
	} else {
		// 经过 FlagSet 提供的方法处理
		base.SetFromGOFLAGS(cmd.Flag)
		cmd.Flag.Parse(args[1:])
		args = cmd.Flag.Args()
	}

	// 执行业务逻辑
	cmd.Run(cmd, args)
	base.Exit()
	return
}
复制代码

主要是几个部分,分别是查找命令,检查是否存在子命令,选项和参数的解析,以及最后是命令的执行。

经过 cmd.Name() != args[0] 判断是否查找到了命令,若是找到则继续向下执行。

经过 len(cmd.Commands) 检查是否存在子命令,存在将 bigCmd 覆盖,并检查是否符合命令行是否符合规范,好比检查 len(args[1:]) 若是为 0,则说明传入的命令行没有提供子命令。若是一切就绪,经过 continue 进行下一次循环,执行子命令的处理。

接着是命令选项和参数的解析。能够自定义处理规则,也能够直接使用 FlagSet.Parse 处理。

最后,调用 cmd.Run 执行逻辑处理。

总结

本文介绍了 Go 中如何经过 flag 实现子命令,从 FlagSet 这个结构体讲起,经过 flag 包中默认提供的 CommandLine 梳理了 FlagSet 的处理逻辑。在基础上,实现了子命令的相关功能。

本文最后,分析了 Go 源码中 go 如何使用 flag 实现。由于是纯粹使用 flag 包裸写,读起来稍微有点难度。本文只算是一个引子,至少帮助你们在大的方向不至于迷路,里面更多的细节还须要本身挖掘。


相关文章
相关标签/搜索