cmdr 02 - Covered for wgetnginx
基于
cmdr
v0.2.11git
继 Getting Start 以后,咱们来介绍如何用 cmdr
复刻一个 wget 的命令行界面,并具体介绍 Command
和 Flag
的各个细节以及 cmdr
可以作到哪些别人作不到的事。github
此外,咱们也声明一下,Getting Start ('另外一个go命令行参数处理器 - cmdr') 的内容有了一些轻微的变化,由于这两周来,咱们已经不停地增长了不少特性来完善 cmdr
的能力,期间有一些不恰当的策略、衍生的命名、采用的算法都有所调整,虽然尽力避免变化,但它是不可免的。咱们是指望给你的编程界面愈来愈完美,让整个编写的流程流畅化,天然化。算法
wget
自己是一个 GNU 应用程序。它的命令行参数有长有短,短参数可能有两个字符,此外参数被分为若干个分组。请看一部分截取:编程
这将是咱们复刻的基准。数组
cmdr
都能作到些什么 - First咱们曾经作过多个应用,不一样的开发语言,不一样的目标,有的是练练手,有的是眼前有个事情有点烦、很差处理、一怒之下就干,有的是有特定的目的例如一个RESTful服务,等等。架构
因此,要想知足那么多的状况下命令行参数的组织和设定都能被很好地表示,不夸张地说,迄今数十年来,咱们没有找到一个命令参数解释器可以完成这个任务。把时间限定在最近几年,把开发语言限定在 Golang,C++,Python 等几种以内,依然没有谁真的能这么称呼本身。现有的命令行参数解释器都有这样那样的不如意:app
--progress=TYPE
”的式样,其中的 TYPE
还能够被复用;git -m
的效果,结果费尽了力,终于实现了一个,然而受制于既有命令行解释器的结构,实现的坑坑洼洼的,本身都难以满意;/etc/program
加载配置文件,结果累了;想要 /etc/nginx/sites.avaliable
那样的效果,本身 watch 了,却合并不了新的配置到已经加载和构建好的配置中,也没法有效地通知应用的业务层按需取用新的配置条目;遇到这些状况时,多数时候只能忍了,毕竟没有太多精力专门去搞参数问题,还有大把的业务须要去完成的对吧。函数
cmdr
选择和实现 wget-demo
也是为了展现本身大致上可以解决命令行参数处理的多数问题。不过和其它命令行参数的策略不一样地在于:别人一般会对参数值的类型作不少文章,例如支持 string/int/slice/map 的多种式样,或者提供 validator,或者采用 Golang 结构 Tag 方式来挂钩参数类型处理器等等。可是 cmdr
在参数类型方面只能说有且够,总体的重心并不在这些方面。post
cmdr
具备一个精悍短小的关键处理器 InternalExecFor()
,它负责处理组合短参数的各类状况。
例如:对于 -1acg -t3
来讲,cmdr
可以正确地识别到 -1 -c -c -g -t=3
的参数集合。
进一步地,对于 -4nva
来讲,cmdr
可以正确识别到 -4 - nv -a
的参数集合。
此外,-mmsg -m msg -m=msg -m'what msg' -m"msg" '-mmsg' "-mWhat msg"
都是对的。在这里,cmdr处理了多数变形形态,有的形态则没必要处理,由于 Shell 会负责处理其中一部分引号问题。
cmdr
也关注短参数的字母重复问题,在不一样层级的子命令之间,你能够同时使用 -a
这样的短参数,固然,-a
仍然不能在子命令内重复,也不能和子命令的上层命令的参数相冲突。长参数以及别名都有一样的处理逻辑。
wget-demo
的实现细节按照上一小节 cmdr
都能作到些什么 - First 提到的 cmdr
的专一点的说法,wget-demo 已经能够被很好地实现出来了。实际上,wget-demo 的代码很是简单(并不短),这也是 cmdr
想要给予开发者的方便。
在 这里 查阅 wget-demo 的目录。
在 这里 查阅 wget-demo 的单一代码文件。
main()
首先看 main:
func main() {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
// To disable internal commands and flags, uncomment the following codes
cmdr.EnableVersionCommands = false
cmdr.EnableVerboseCommands = false
cmdr.EnableHelpCommands = false
cmdr.EnableGenerateCommands = false
cmdr.EnableCmdrCommands = false
if err := cmdr.Exec(rootCmd); err != nil {
logrus.Errorf("Error: %v", err)
}
}
复制代码
line 2,3能够被忽略,那是便于 cmdr 开发阶段的内容。发布后的 cmdr 也依赖于 logrus,但实际上这是由于 cmdr 的 examples 的缘由,而 cmdr 自身是不作此依赖的,因此你仍是能够本身选择 logger。logger 问题之后或许会被 cmdr 慎重考虑,完全去除对任何 logger 的依赖。
line 5-10 是为了 wget-demo
专用的。由于 wget 没有命令和子命令,只有参数,所以 cmdr
内置的几个命令(组)被禁用了。
真正的代码,只有 line 12-14。无需解释。
rootCmd
因此你须要作的只是编排 rootCmd
结构。
var (
rootCmd = &cmdr.RootCommand{
Command: cmdr.Command{
BaseOpt: cmdr.BaseOpt{
Name: "wget",
Flags: append(
startupFlags,
append(loggerFlags,
downloadFlags...)...,
),
},
SubCommands: []*cmdr.Command{},
},
AppName: "wget-demo",
Version: wgetVersion,
VersionInt: 0x011400,
Header: `GNU Wget 1.20, a non-interactive network retriever. Usage: wget [OPTION]... [URL]... Mandatory arguments to long options are mandatory for short options too.`,
}
)
复制代码
rootCmd
包含一个 Command
嵌入结构。而后 rootCmd
包含 AppName
, Version
, Header
等等顶级宣告。看看 RootCommand
的定义:
type(
// RootCommand holds some application information
RootCommand struct {
Command
AppName string
Version string
VersionInt uint32
Copyright string
Author string
Header string // using `Header` for header and ignore built with `Copyright` and `Author`, and no usage lines too.
ow *bufio.Writer
oerr *bufio.Writer
}
)
复制代码
你能够编写本身的 Copyright
和 Author
字段,由 cmdr
为你构造 app 的 header 部分。你也能够单纯指定 Header
字段让 cmdr
原样输出。
为了复刻的更像一点,wget-demo
定制了 Header
字段。
此外,wget 的分组的参数选项,咱们选择实现了前三组,所以你能看到 line 6-9 使用了一个 append 嵌套组合这三组参数集定义。
Command
rootCmd
包含一个 Command
嵌入结构,其定义为:
type(
// BaseOpt is base of `Command`, `Flag`
BaseOpt struct {
Name string
// single char. example for flag: "a" -> "-a"
// Short rune.
Short string
// word string. example for flag: "addr" -> "--addr"
Full string
// more synonyms
Aliases []string
// group name
Group string
// to-do: Toggle Group
ToggleGroup string
owner *Command
strHit string
Flags []*Flag
Description string
LongDescription string
Examples string
Hidden bool
DefaultValuePlaceholder string
// Deprecated is a version string just like '0.5.9', that means this command/flag was/will be deprecated since `v0.5.9`.
Deprecated string
// Action is callback for the last recognized command/sub-command.
// return: ErrShouldBeStopException will break the following flow and exit right now
// cmd 是 flag 被识别时已经获得的子命令
Action func(cmd *Command, args []string) (err error) } // Command holds the structure of commands and subcommands Command struct {
BaseOpt
SubCommands []*Command
// return: ErrShouldBeStopException will break the following flow and exit right now
PreAction func(cmd *Command, args []string) (err error) // PostAction will be run after Action() invoked. PostAction func(cmd *Command, args []string) // be shown at tail of command usages line. Such as for TailPlaceHolder="<host-fqdn> <ipv4/6>": // austr dns add <host-fqdn> <ipv4/6> [Options] [Parent/Global Options] TailPlaceHolder string root *RootCommand allCmds map[string]map[string]*Command // key1: Commnad.Group, key2: Command.Full allFlags map[string]map[string]*Flag // key1: Command.Flags[#].Group, key2: Command.Flags[#].Full plainCmds map[string]*Command plainShortFlags map[string]*Flag plainLongFlags map[string]*Flag } ) 复制代码
Name
暂时没有什么用处,目前你老是能够忽略它。未来,它可能被更好地用在文档输出方面。
Short
, Full
, Aliases
无需再特别说明了,只是再强调一次,在上级命令的全部子命令中,它们不能重复。在多级子命令结构的不一样层级中,没有这个限制,你能够比较宽泛地定义本身的命令和子命令集合。
当命令被识别出来时,PreAction
被当即执行,此时,cmd.GetHitStr()
能够得到被命中的命令行参数中的命令字符串。你能够在这里创建 PreAction 逻辑,当特定条件不知足时,你的逻辑能够返回 cmdr.ErrShouldBeStopException
来通知当即退出。
Action
和 PostAction
的用法应该很明确,这里就不展开了。你对命令的实现逻辑一般应该老是利用 Action
字段来完成。
Command
的函数Command
也包含一些相似于 GetHitStr()
的函数:
PrintHelp(justFlags bool)
:输出帮助屏。PrintVersion()
:输出版本信息屏。GetRoot()
直接访问到 rootCmd
;若是想逐级回溯,经过 Owner
字段就能够了。IsRoot()
帮助你测试是否到达了顶级命令。HasParent()
帮助你测试是否还有 Owner/Parent。Group
字段被用于命令分组。相同的字符串会被组织为一个命令组,显示的效果像这样:
若是你不指定Group,那么它们会被自动归属于一个名为 cmdr.UnsortedGroup
的特殊组中,图示中的 ms, s, t 都是这样的未指定分组,它们不会有组标题输出,并且老是被做为第一个被输出的分组。
若是你想要归属到 “Misc” 分组,那么你能够指定 Group
字段为 cmdr.SysMgmtGroup
,其特殊之处在于老是被最后输出(v0.2.11及前可能存在不一样的表现,下一版本会予以确认,但想要最后输出也很容易,稍后描述)。
对于分组谁先谁后,实际上有一个方案:指定你的Group字符串时使用两段结构“a.b
”。a被用于排序,你可使用字母和数字,例如:“001”,“011”,“091”等等。又或者:“A01”,“B01"等等。b被用做分组名并被用于显示。
ToggleGroup
暂未实现,由于其功能能够暂时使用 PreAction 来代替。
since 0.2.13,ToggleGroup 已被移出 BaseOpt 结构,移入 Flag 中。
since 0.2.15 (待发布),ToggleGroup 已被实现。
Description
和 LongDescription
,是命令的描述性文字。你必须提供 Description
字段,在上面的图示中,它被显示在命令的后半段。若是你提供了 LongDescription
,它将会在命令的 --help
屏中被显示,另外,在 man page 或者文档输出中,LongDescription
也会被输出以便更细致地进行描述。
Examples
是命令的用例。实际上咱们限定了用例的格式:
Examples:` $ {{.AppName}} start make program running as a daemon background. $ {{.AppName}} start --foreground make program running in current tty foreground. $ {{.AppName}} run make program running in current tty foreground. $ {{.AppName}} stop stop daemonized program. $ {{.AppName}} reload send signal to trigger program reload its configurations. $ {{.AppName}} status display the daemonized program running status. $ {{.AppName}} install [--systemd] install program as a systemd service. $ {{.AppName}} uninstall remove the installed systemd service. `,
复制代码
你必须按上述格式来提供 Examples
的具体内容。第一行以 $ {{.AppName}}
开头,而后是你的命令,若是是多级下的子命令,请注意补全,例如 $ {{.AppName}} ms tags list
。而后第二行为上一行命令的功能性描述,不建议描述太冗长,也不建议描述被切分到多行。如是重复。
这样作的缘由是为了在 man page 和文档输出时 cmdr
可以重组 examples 部分的格式令其更视觉化。
这是一个 man page 的部分截图,咱们能够令其更视觉化,帮助最终使用者。
若是你不想命令被显示在帮助屏、man page、文档中,使用 Hidden
字段来隐藏它。
若是你计划在下一某个版本废弃某个命令,可使用 Deprecated
字段来标识它,你应该提供一个语义化的版本号到 Deprecated 中,至少在 Markdown 的文档输出中,它会被显示为删除线样式。
在 Terminal 中,deprecated 的命令显示为暗色。
适用于
Flag
,不适用于Command
DefaultValuePlaceholder
字段提供一个字符串 X,X 被链接在长参数以后用于显示目的,例如:--config=FILE
。这是为了让参数的用法更具备表义性,也是为了强调参数为带值的。
注意为了提醒 cmdr
你须要一个带值参数,你必须明确设定 DefaultValue
字段为一个特定数据类型的值。你可使用 string, int, string slice, int slice, duration 做为默认值。
若是是不带值的参数,它们老是具备 bool 类型的隐含值。若是你不指定 DefaultValue
,那么 cmdr
认为你须要的是一个 bool 类型的不带值参数。
若是你在提供命令行参数是使用逗号分隔的字符串,并且为 DefaultValue
设定了 string slice, int slice 的话,那么 cmdr
会识别到并切分字符串转义为 Slice。稍后你在 Action 中可使用 cmdr.GetStringSlice()
等方式直接抽取到数组。
DefaultValue
字段决定了 该参数的值的存储方式。但你能够自由地抽取该参数值到不一样的数据类型,你能够经过 Get()
抽出该参数值的内部存储,而后自行转义为想要的类型。
since 0.2.13,DefaultValuePlaceholder 已被移出 BaseOpt 结构,移入 Flag 中。
since 0.2.13,Flags 已被移出 BaseOpt 结构,移入 Command 中。
命令的参数集被定义于此。
对于命令来讲,多级命令可以构成一个结构化的层次,不只便于用户索引和记忆,也有利于业务逻辑的构建和编写。
嵌套多级的子命令可能会很冗长,所以实际编码过程当中,你能够考虑拆分并独立定义子命令,并在父命令中组合它们。
对于命令来讲,在 Usage 行的显示也须要被 meaningful。若是你有这样的须要,那么 TailPlaceHolder
字段能够在 Usage 行的正常输出以外额外嵌入一段文字。
对于 TailPlaceHolder="<host-fqdn> <ipv4/6>"
来讲,显示的效果是这样的:
应该不须要更多解释了,这个用文字表达我须要首先给出一堆术语释义才行,就不骗字数了。
参数,选项,都是 Flag 的同义语。cmdr
在代码实现时选用了 Flag
这个单词而已。
除了在 Command
中已经描述过的 术语二者都有的字段以外,这一小节描述其它部分,尤为是 Flag 特有的部分:
参考 Command 中有关小节的描述。虽未实现,但这个字段能够干点什么,未来吧。
参考 Command 中有关小节的描述。自 cmdr v0.2.13 起,通过代码 review,这个字段正式移入 Flag 中,由于这才是正确的逻辑归属点。
参考 Command 中有关小节的描述。嗯,它原本就设计在 Flag 中,难怪之前写 demo 时感受怪怪的,DefaultValuePlaceholder 写在一处,DefaultValue 又写在另外一处。从此就是一家人了。
还没有实现。暂时也没考虑。原来的意图是提供枚举文字量。但是你们都是写代码的,不如就 1,2,3 将就了吧先。
未用。实际上 cmdr
没有校验的概念,也没有必须存在这种概念。
由于咱们以为,你不该该要求用户必定要提供一个什么。
好比 consul 集群在哪里呀?consul 集群固然是在 consul.ops.local 那儿啊,要否则大家家云设施架构师设计的不同,那么它就在 registrar.prod.ashiley.org.local 啊。换句话说,你老是应该给参数一个默认值,甚至给它 nil 或者 ”“ 也能够,你的业务逻辑应该处理一下这些临界场景。
尽管咱们设计了 cmdr
以帮助你创建完善的 Command Line UI,但让用户随时随地能省缺就省缺才是正确的。
这个字段的用途,首先是实现 git commit -m
效果。
为了达到效果,你必须在 ExternalTool 中填写 ”EDITOR“ 字符串,又或者使用 cmdr.ExternalToolEditor
常量。
本质上,cmdr
将 ExternalTool
视为环境变量名,试图探查环境变量是否是存在,并取得该值做为执行文件X,而后采用一个临时文件T做为执行文件X的输入参数并就地执行它们,待用户操做完毕并关闭执行文件X以后,临时文件T的内容被当作文本并被做为选项值填入。
因此,git commit -m
就是这么干的,cmdr
复制了这个流程。若是你须要相似的逻辑,那么就能够借助于 ExternalTool
字段。
依据上面各小节的对 RootCommand,Command,Flag的阐述,接下来就是具体的数据集的定义了。
咱们已经提到过嵌套结构的烦恼并作出了建议,至于更好的数据集定义方案,继续改善吧,欢迎给我建议。
那么如今,你已经能够构建出你的 Command Line UI 了。wget-demo 已经实现了三组参数集,不但可以被正确识别,显示的效果也还不错:
若是但愿对命令行参数的解释和操做有更多便利,欢迎 Issue 到: