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

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

前两篇文章链接如下:

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

希望看完本篇文章,如果再阅读 go 命令的实现源码,至少在整体结构上不会迷失方向了。

FlagSet

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

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

$ 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() 加入到了 formal 中
	formal        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 上做了进一步的封装,简单的使用可以直接无视它的存在。

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

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

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 包裸写,读起来稍微有点难度。本文只算是一个引子,至少帮助大家在大的方向不至于迷路,里面更多的细节还需要自己挖掘。

博文地址:Go 命令行解析 flag 包之通过子命令实现看 go 命令源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/353018.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网络原理——传输层1

1. 端口号 端口号标识了一个主机上运行的不同程序。在TCP/IP协议中&#xff0c;使用"源IP地址"、"源端口号"、"目的IP地址"、"目的端口号"和"协议号"这样一个五元组来标识一个通信。 端口号划分&#xff1a; 0 - 1023&am…

pytest教程-7-用例前后置方法

上一小节&#xff0c;我们学习了pytest跳过测试用例的方法&#xff0c;本小节我们讲解一下pytest用例的前后置方法。 在unittest中就有前置setup和后置teardown来处理测试用例执行前的准备工作&#xff08;浏览器驱动实例化&#xff0c;数据库连接等&#xff09;以及执行后的处…

常见の算法5

位图 一个int类型32字节&#xff0c;可以表示0-31这32个数出没出现过&#xff0c;出现过1没出现0&#xff0c;再扩大一点搞个数组&#xff0c;就可以表示0-1023出没出现过&#xff0c;一个long类型可储存64位 如何把10位组成的数&#xff0c;第四位由1改成零 package class05…

mcu短时间内发生多次中断,如何解决中断丢失问题?

问题 嵌入式开发中&#xff0c;如果中断A的处理函数执行时间长&#xff0c;某段时间内&#xff0c;快速来了2个中断A(例如&#xff1a;外部管脚输入信号变化)&#xff0c;则会导致第2个中断丢失。 我有几个疑问&#xff1a; 1.目前市面上的芯片&#xff0c;是否支持缓存中断标志…

【docker】linux系统docker的安装及使用

一、docker应用的安装 1.1 安装方式 Docker的自动化安装&#xff0c;即使用提供的一键安装的脚本&#xff0c;进行安装。 官方的一键安装方式&#xff1a;curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun 国内 daocloud一键安装命令&#xff1a;curl -s…

JavaWeb:商品管理系统(Vue版)

文章目录 1、功能介绍2、技术栈3、环境准备3.1、数据库准备3.2、在新建web项目中导入依赖3.3、编写Mybatis文件3.4、编写pojo类3.5、编写Mybatis工具类3.6、导入前端素材&#xff08;element-ui & vue.js & axios.js&#xff09;3.7、前端页面 4、功能实现4.1、查询所有…

机器学习---无偏估计

1. 如何理解无偏估计 无偏估计&#xff1a;就是我认为所有样本出现的概率⼀样。 假如有N种样本我们认为所有样本出现概率都是 1/N。然后根据这个来计算数学期望。此时的数学期望就是我们平常讲 的平均值。数学期望本质就 是平均值。 2. 无偏估计为何叫做“无偏”&#xff1…

Deeplearning

Numpy Deep Learning Basic 神经网络&#xff1a; #mermaid-svg-2N27H7C0XPrmd8HP {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-2N27H7C0XPrmd8HP .error-icon{fill:#552222;}#mermaid-svg-2N27H7C0XPrmd8HP .…

GPIO的8种工作模式

一、8种工作模式 二、IO端口的基本结构 下面是一张F1的IO的结构图。 圆圈 2是芯片内部的上下拉电阻&#xff0c; 输入数据寄存器简称IDR &#xff0c;cpu读IDR就可以知道外面的是高电平还是低电平&#xff0c;单片机IO口输出的高低电平主要依靠P-MOS和N-MOS&#xff0c;输出数据…

CHS_01.2.3.1+同步与互斥的基本概念

CHS_01.2.3.1同步与互斥的基本概念 知识总览什么是进程同步什么是进程互斥知识回顾 在这个小节中 我们会介绍进程同步和进程互斥相关的概念 知识总览 我们会结合一些具体的例子 让大家能够更形象的理解这两个概念 首先来看一下什么是进程同步 其实在聊进程同步之前 咱们已经接…

WPF自定义圆形百分比进度条

先看效果图 1.界面代码 <UserControl x:Class"LensAgingTest.CycleProcessBar1"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc"http://schemas.op…

STM32(更新中)

目录 1 时钟&#xff08;心跳&#xff09; 1.1 CubeMX基本配置 1.2 外设在时钟上的分配原理 1.3 时钟树 2 寄存器&#xff08;地址&#xff09; 3 GPIO 3.1 GPIO实物 3.2 GPIO两种结构&#xff08;推挽/开漏&#xff09; 3.3 LED 3.4 CUBEMX 3.5 常用函数 …

机器学习|ROC曲线和AUC值

概念AUC&#xff08;Area Under Curve&#xff09;被定义为ROC曲线下的面积。其中&#xff0c;ROC曲线全称为受试者工作特征曲线 &#xff08;receiver operating characteristic curve&#xff09;&#xff0c; 模型会计算出所判断事物为汉堡&#x1f354;的概率&#xff0c;而…

【游戏客户端开发的进阶路线】

*** 游戏客户端开发的进阶路线 春招的脚步越来越近&#xff0c;我们注意到越来越多的同学们都在积极学习游戏开发&#xff0c;希望能在这个充满活力的行业中大展拳脚。 当我们思考如何成为游戏开发领域的佼佼者时&#xff0c;关键在于如何有效规划学习路径。 &#x1f914; 我…

11.Elasticsearch应用(十一)

Elasticsearch应用&#xff08;十一&#xff09; 1.什么是自动补全 现代的搜索引擎&#xff0c;一般都会提供Suggest as you type的功能 帮助用户在输入搜索的过程中&#xff0c;进行自动补全或者纠错。通过协助用户输入更加精准的关键词&#xff0c;提高后续搜索阶段文档的…

看图说话:Git图谱解读

很多新加入公司的同学在使用Git各类客户端管理代码的过程中对于Git图谱解读不太理解&#xff0c;我们常用的Git客户端是SourceTree&#xff0c;配合P4Merge进行冲突解决基本可以满足日常工作大部分需要。不同的Git客户端工具对图谱展示会有些许差异&#xff0c;以下是SourceTre…

【教程】MobaXterm软件Keygen快速生成注册码

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 1、去官网安装正版软件&#xff0c;比如23.6版本的&#xff1a;MobaXterm free Xserver and tabbed SSH client for Windows 2、打开这个网站&#xff0c;输入信息&#xff1a;MobaXterm Keygen 3、将自动下载的C…

【原理图PCB专题】Allegro报封装Name is too long

在安装完成Cadence17.4版本后&#xff0c;在首次导入网表时发现PCB报了一些错误&#xff0c;就是名称太长 #1 ERROR(SPMHNI-189): Name is too long… ERROR(SPMHNI-189): Problems with the name of device ‘MT48LC2M32B2B5-6_SDRAMTSOP86_MT48LC2M32B2B5-6’: ‘Name is to…

vue-component组件

一、Component 组件 组件&#xff08;Component&#xff09;是自定义封装的功能。在前端开发过程中&#xff0c;经常出现多个网页的功能是重复的&#xff0c;而且很多不同的页面之间&#xff0c;也存在同样的功能。将相同的功能进行抽取,封装为组件,这样&#xff0c;前端人员就…

JavaWeb,Vue的学习(上)

概述 Vue的两个核心功能 声明式渲染&#xff1a;Vue 基于标准 HTML 拓展了一套模板语法&#xff0c;使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。响应性&#xff1a;Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM ViteVue3项目…