另外一个go命令行参数处理器 - cmdr

cmdr 是另外一个命令行参数处理器。react

Golang 本身带有 flags 进行命令行参数处理,算是便利的,然而和 Google 一向的作法相同,很是独,很是反人类。linux

在计算机人机交互界面的历史上,命令行的交互方式只有一种是贯穿始终,获得传承和延续的,那就是 getopt 以及 getopt_long。提及 getopt 来也能够讲述一个怪长的故事,然而本文不作此打算。不管如何,你须要知道的就是,getopt及其交互界面已是POSIX的一部分,一个卓有成效的程序员、开发者、科学家,或者计算机从业者,对于这个界面都已是训练有素,无需成本了。你可能在用着它,但你或许只是没有意识到它的存在而已。GNU的大部分命令行小刀都采用了这样的界面,因此,例如tar, gwk, gzip, ls, rm, …,以及没法列举的那些工具都是这样的界面。git

因此,自行其是,本身搞一套,并不是不能够。但我能够不买帐。程序员

那么,这并不是我独自一人的自赏。咱们只须要知道,在 Golang 的开源圈子里,已经有了数十种 getopt-like 的复刻本,用觉得 Golang 开发的应用程序提供更好的命令行界面。这里面不乏 viper/cobra, cli 那样的巨做,也有一些小巧精干的实现。github

cmdr 也是这么一个 getopt-like 的实现。和已有的其它实现不一样之处在于,cmdr基本上原样复制了 getopt 的表现。也就是说,一个典型的 Unix/Linux 应用程序,例如 cp,mv 等等,是怎么作的,那么基于 cmdr 的应用程序也就是怎么作的。这里讲的固然是关于命令行参数怎么被解释的问题,而非应用程序的具体逻辑。golang

让咱们来看看都有哪些具体方面。docker

POSIX 约定

POSIX 表示可移植操做系统接口(英语:Portable Operating System Interface,缩写为POSIX)是 IEEE(电气和电子工程师协会,Institute of Electrical and Electronics Engineers)为要在各类UNIX操做系统上运行软件,而定义API的一系列互相关联的标准的总称,其正式称呼为IEEE Std 1003,而国际标准名称为ISO/IEC 9945。此标准源于一个大约开始于1985年的项目。POSIX这个名称是由 理查德·斯托曼(RMS)应IEEE的要求而提议的一个易于记忆的名称。它基本上是Portable Operating System Interface(可移植操做系统接口)的缩写,而X则代表其对Unix API的传承。 电气和电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)最初开发 POSIX 标准,是为了提升 UNIX 环境下应用程序的可移植性。然而,POSIX 并不局限于 UNIX。许多其它的操做系统,例如 DEC OpenVMS 和 Microsoft Windows NT,都支持 POSIX 标准。npm

下面是 POSIX 标准中关于程序名、参数的约定:bash

  • 程序名不宜少于2个字符且很少于9个字符;
  • 程序名应只包含小写字母和阿拉伯数字;
  • 选项名应该是单字符活单数字,且以短横‘-‘为前綴;
  • 多个不须要选项参数的选项,能够合并。(譬如:foo -a -b -c ---->foo -abc
  • 选项与其参数之间用空白符隔开;
  • 选项参数不可选。
  • 若选项参数有多值,要将其并为一个字串传进来。譬如:myprog -u "arnold,joe,jane"。这种状况下,须要本身解决这些参数的分离问题。
  • 选项应该在操做数出现以前出现。
  • 特殊参数 ‘--' 指明全部参数都结束了,其后任何参数都认为是操做数。
  • 选项如何排列没有什么关系,但对互相排斥的选项,若是一个选项的操做结果覆盖其余选项的操做结果时,最后一个选项起做用;若是选项重复,则顺序处理。
  • 容许操做数的顺序影响程序行为,但须要做文档说明。
  • 读写指定文件的程序应该将单个参数'-'做为有意义的标准输入或输出来对待。

GNU长选项约定

  • 对于已经遵循POSIX约定的GNU程序,每一个短选项都有一个对应的长选项。
  • 额外针对GNU的长选项不须要对应的短选项,仅仅推荐要有。
  • 长选项能够缩写成保持唯一性的最短的字串。
  • 选项参数与长选项之间或经过空白字符活经过一个'='来分隔。
  • 选项参数是可选的(只对短选项有效)。
  • 长选项容许以一个短横线为前缀。

getopt 界面

如下对 getopt 以及 getopt_long 提供的界面进行描述,cmdr 具有相同的能力。app

在如下的行文中,短参数短选项是等同的概念,其它词汇也相似如此,再也不赘述。

短参数

单个短横线引导的单个字符的参数,被称为短参数。例如:-v-d,等等。有的时候,短参数也可能有两个字符甚至更多个字母。然而,短参数的用意就在于缩略,所以多字符的短参数不多见,且一般被用于组合,更像是典型的单字符短参数后缀以一个取值。例如 rar 的选项中有 -ep, -ep1, -ep3:

ep            Exclude paths from names
  ep1           Exclude base directory from names
  ep3           Expand paths to full including the drive letter
复制代码

然而在实现其处理器时,咱们能够提供 -ep<n> 的处理器就够了,因此你仍然能够将其视为 -ep 短参数的变形。

长参数

两个短横线引导的多个字符的参数,被称为长参数。例如:--debug--version 等等。

通常来讲,长参数更具有描述性,一般使用单词、词组来构成长参数。例如 docker 的子命令 docker checkpoint create

$ docker checkpoint create --help

Usage:	docker checkpoint create [OPTIONS] CONTAINER CHECKPOINT

Create a checkpoint from a running container

Options:
      --checkpoint-dir string   Use a custom checkpoint storage directory
      --leave-running           Leave the container running after checkpoint
复制代码

参数描述

每条命令或参数选项能够被一段文字以描述。

参数重复堆叠

不管长短参数,能够以任意顺序出现,也能够任意出现屡次。对于屡次出现的参数,通常来讲是最后一次出现的为准,以前出现过的会被覆盖。

例如命令行:-1 -a yy -a dd -a cc,则对于参数a来讲,其有效值为 ”cc“,此前出现的都被覆盖了。

bool型短参数的组合

对于getopt不带值的参数,例如 "1abc" ,如下的命令行都是有效的:

  • -1 -a -b -c
  • -abc1
  • -ac -1b
  • ...

顺序是不敏感的,组合是任意的。

必须带值的参数

getopt的定义是参数后加一个冒号,例如 “1a:b::" 中的参数 a,对它你须要指定命令行形如 -1 -a xxx

可选值的参数

getopt的定义是参数后加两个冒号,例如 “1a​:b:​:" ​中的参数 b,对它你须要指定命令行形如 -1 -b 或者 -1 -bvalue

在 getopt 界面上的加强

命令和子命令

以 docker 的子命令 checkpoint 为例:

graph LR
A[docker] -->|Commands| B(checkpoint)
B --> D[create]
B --> E[ls]
B --> F[rm]
复制代码

事实上,命令与子命令是没有区别的,若是有必要,能够创建任意多级的命令和子命令嵌套层次。不过在实际的 Command-Line UI 设计中,超过4层的子命令嵌套都是极少数,由于这也会给使用工具的人带来麻烦。

Shell自动完成

在现代的命令行界面中,自动完成(Shell Completion)已是一个关键性特性了。流行的命令行界面例如 Bash、Zsh、Fish 都提供了自动完成的特性。一般一个应用程序须要面向这个Shells 提供配套的自动完成脚本,从而得到自动完成能力。

一个已经支持自动完成的应用程序的命令行输入多是这样子的:

Bash 的自动完成

docker 的 自动完成

Zsh 的自动完成

docker 在 zsh 中的自动完成。能够注意到 zsh 的 TAB 按键次数更简练,并且列表选择界面也更有效和更具备提示性。固然,zsh的自动完成也存在一些bug,例如一级命令列表超出终端屏幕可视行数时列表选择界面就被破碎掉了。

cmdr 的使用方法

cmdr 的使用方法尽量简单化了,接下来咱们作一个简明的介绍。

一个简单的入口能够这样:

package main

import (
	"fmt"
	"github.com/hedzr/cmdr"
)

func main() {
	// logrus.SetLevel(logrus.DebugLevel)
	// logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true,})

    // 可选的四个选项:
	cmdr.EnableVersionCommands = true
	cmdr.EnableVerboseCommands = true
	cmdr.EnableHelpCommands = true
	cmdr.EnableGenerateCommands = true
    
	if err := cmdr.Exec(rootCmd); err != nil {
		fmt.Printf("Error: %v", err) // or log, logrus
	}
}

var(

	rootCmd = &cmdr.RootCommand{
		Command: cmdr.Command{
			BaseOpt: cmdr.BaseOpt{
				Name: "short",
				Flags: []*cmdr.Flag{

				},
			},
			SubCommands: []*cmdr.Command{
				serverCommands,
				// msCommands,
			},
		},

		AppName:    "short",
		Version:    cmdr.Version,
		VersionInt: cmdr.VersionInt,
		Copyright:  "austr is an effective devops tool",
		Author:     "Your Name <yourmail@gmail.com>",
	}

	serverCommands = &cmdr.Command{
		BaseOpt: cmdr.BaseOpt{
			Short:       "s",
			Full:        "server",
			Aliases:     []string{"serve", "svr",},
			Description: "server ops: for linux service/daemon.",
			Flags: []*cmdr.Flag{
				{
					BaseOpt: cmdr.BaseOpt{
						Short:       "f",
						Full:        "foreground",
						Aliases:     []string{"fg",},
						Description: "running at foreground",
					},
				},
			},
		},
		SubCommands: []*cmdr.Command{
			{
				BaseOpt: cmdr.BaseOpt{
					Short:       "s",
					Full:        "start",
					Aliases:     []string{"run", "startup",},
					Description: "startup this system service/daemon.",
					Action: func(cmd *cmdr.Command, args []string) (err error) {
						return
					},
				},
			},
			{
				BaseOpt: cmdr.BaseOpt{
					Short:       "t",
					Full:        "stop",
					Aliases:     []string{"stp", "halt", "pause",},
					Description: "stop this system service/daemon.",
				},
			},
			{
				BaseOpt: cmdr.BaseOpt{
					Short:       "r",
					Full:        "restart",
					Aliases:     []string{"reload",},
					Description: "restart this system service/daemon.",
				},
			},
			{
				BaseOpt: cmdr.BaseOpt{
					Full:        "status",
					Aliases:     []string{"st",},
					Description: "display its running status as a system service/daemon.",
				},
			},
			{
				BaseOpt: cmdr.BaseOpt{
					Short:       "i",
					Full:        "install",
					Aliases:     []string{"setup",},
					Description: "install as a system service/daemon.",
				},
			},
			{
				BaseOpt: cmdr.BaseOpt{
					Short:       "u",
					Full:        "uninstall",
					Aliases:     []string{"remove",},
					Description: "remove from a system service/daemon.",
				},
			},
		},
	}
)
复制代码

能够看到的是,cmdr.RootCommand 和 cmdr.Command 的区别不大,只是多了应用程序信息的成员字段。而 cmdr.Command 和 cmdr.Flag 的区别也不大,它们都有相同的 BaseOpt 嵌入结构。

所以,定义命令和定义选项是很类似的,而后你须要进行正确的结构嵌套。若是感到嵌套结构迷乱了眼睛,则能够抽出一个子命令、或者一组子命令到一个独立的变量中,而后使用引用的方式嵌入到上级命令的恰当位置。

这种抽出的方式也适合于进行类似结构的共享,但要注意引用和深度拷贝的区别。此处不作进一步讨论了,总之,若是感到没有把握,不妨一级一级地老老实实地完成定义,通常的工具开发也不会有太多的嵌套的吧。

言归正传,完成了上面的定义以后,就能够编译成执行文件运行了(或者用 go run main.go)。你能够在终端中尝试使用它:

bin/short
bin/short --help
bin/short --version
bin/short -#
bin/short --debug --verbose server --help
bin/short svr --help --debug -v
bin/short s start -f --help ~~debug
复制代码

指定 Action

每条命令(cmdr.Command)均可以指定 Action,一个 func 对象。你将要为命令编写的业务逻辑就在这里。

若是有必要的话,一个选项(cmdr.Flag)也能够被提供一个 Action,若是你须要在选项被扫描到时触发点其余逻辑的话。

对于命令而言,你能够提供额外的 PreActionPostAction,它们分别是在命令的 Action 被执行的先后被调用的。特别是 PreAction 容许返回一个特别的错误值 cmdr.ErrShouldBeStopException 来告诉 cmdr 终止后续处理,因此你能够有机会避免 命令的 Action 部分被执行。

因为 RootCommand 也是一个 Command,因此定义在 RootCommand 中的 PreActionpostAction 有着特别的处理逻辑:

RootCommand.PreAction 将会在具体 Command.PreAction 执行以前被执行;RootCommand.PostAction 将会在具体 Command.PostAction 执行以后被执行。

这样的特别逻辑是为了便于开发者定义本身的前置、退出逻辑。例如一个微服务应该在开始提供服务以前完成注册中心登记,以及在中止服务时撤销登记,这些任务适合于在 RootCommandPre/PostAction 中来作。

~~debug

~~debug 是一个隐藏性的标志。~~ 和 long 参数 是类似的,不过它的不一样在于,相应的参数的入口不会被创建在 标准名字空间中,所以你须要在顶级名字空间中抽取它的值。

~~debug 有着一个特别的做用,在调试阶段,这个选项将会使得正常处理逻辑结束后,附加一段调试性的信息输出,其中包含 全部有效的选项及其最终值,还包含这些选项的 yaml 文本形式。

一个式样是:

你能够经过:

bin/wget-demo ~~debug
复制代码

来查看类似的输出结果。

我相信这个功能能够帮助你解决不少问题,没必要再来猜来猜去的了。

名字空间

全部的选项值都被放在标准名字空间中,cmdr.RxxtPrefix 定义了标准名字空间的层级,其默认值为 app。为了

这意味着 RootCommand 的 Flags,例如 --version ,能够用 cmdr.GetBool("app.version") 来抽取其值。相似的,--debug 的抽取语句为 cmdr.GetBool("app.debug")

前面说过 ~~debug 有点特殊,这样的不加前缀的选项的值能够直接抽取:cmdr.GetBool("debug")

每一级命令或子命令就会创建一个嵌套的名字空间,其名称取自命令的 Full 字段,也就是长参数名。所以 bin/short server start -f-f 的抽取语句为 cmdr.GetBool("app.server.start.foreground")

你固然能够执行不一样的 cmdr.RxxtPrefix,例如:

cmdr.RxxtPrefix = []string{"server",}
// 等价于使用 ”server.xxx" 而不是 “app.xxx”
复制代码

一个选项的值是能够多种形态的,但总的来讲咱们支持四种数据类型:

  • bool
  • int
  • string
  • string slice

更多的类型,咱们暂不直接支持。将来或会予以加强。

环境变量重载

可使用环境变量重载去覆盖命令行参数。

因此:

CMDR_APP_SERVER_START_FOREGROUND=1 bin/short server start 等价于 bin/short server start -f

若是你但愿使用非 ”CMDR_“ 的环境变量前缀,你能够设置 cmdr.EnvPrefix 来自行控制前缀。例如

cmdr.EnvPrefix = []string{ "Rx", "cd", }
// 等价于使用 RX_CD_ 前缀
复制代码
当前版本的问题

环境变量的优先级较低,若是配置文件或者命令行参数有指定值,则环境变量的设定值就被掩盖了。

这不符合惯例,咱们考虑在下一版本中解决此问题。

咱们将会实现的优先级为:defaultValue -> config-file -> env-var -> command-line opts。

配置文件的自动加载

默认状况下,cmdr 自动查看以下文件:

  • /etc/<appname>/<appname>.yml
  • /usr/local/etc/<appname>/<appname>.yml
  • $HOME/.<appname>/<appname>.yml

cmdr 也会自动装载相应的 conf.d 子目录中的全部 yaml 文件,并依次载入和覆盖选项的定义值。所以你能够切分大型配置文件到多个小文件中,以便于运维部署和管理。

对于开发者来讲,cmdr 还会首先检查项目目录下的 ./ci/etc/<appname>/<appname>.yml 是否有效并试图自动加载它及其 conf.d 子目录。

cmdr 支持 conf.d 文件夹的监视,其中的变化会被传送给全部注册的 listeners。关于这个方面的细节,能够查看:

  • cmdr.AddOnConfigLoadedListener(c)
  • cmdr.RemoveOnConfigLoadedListener(c)
  • cmdr.SetOnConfigLoadedListener(c, enabled)
当前版本的问题

没法定制加载位置、没法忽略加载位置,等等。

其余的配置文件格式也暂时不支持。

实例 wget-demo

咱们已经实现了一个 wget 的命令行界面复刻版本,可是仅提供小部分命令行参数的处理,由于完整的复刻版本基本上只是一个重复的劳做了,做为示例咱们已经实现了足够多的选项,足以说明 cmdr 的能力了。

wget-demo 的帮助屏是这样的:

和 gnu 的 wget 相比较而言,看起来也算是没有区别了。

wget-demo 的源码能够在这里找到:

github.com/hedzr/cmdr/…

cmdr 的版本规划方式

semver是符合规范的。

关于 semver 的含义能够查看以下两个连接,无需多言:

更多的介绍

cmdr 是在早前若干个非正式实现的基础上重写的一个新的实现,其首要目标就是完彻底全地 Unix/Linux 命令行界面,而不是 golang 风格的、或者其它的部分实现的风格。

getopt 以及 getopt_long 都有本身的参数定义方式,不过在这个方面,cmdr 不打算实现它们的仿真风格,由于那并不方便也不算直观。

cmdr 尽力作到的是,命令和参数定义完成以后就完成了一切。除此而外,你无需作别的事就能获得:

  • 自动的帮助屏
  • 自动的配置文件载入
  • 配置文件切分到 conf.d 子目录,且自动监视其变动
  • 彻底的 Unix/Linux Command-Line UI
  • 容许环境变量重载到选项
  • 支持 Shell 自动完成特性
  • 更多特性...

目前已经实现的是主体的大部分特性,细节还没有打磨完美,还须要继续投入力量进行改善。然而做为建设的主要目标已经可做为已达成了。

更多 cmdr 用法,从此继续进行描述。

相关文章
相关标签/搜索