高级特性 (Advanced)
Handlers
(TODO)
Such as: AfterArgParsed, AutomaticEnvHooks, ConfigLoadedListener, CustomShowBuildInfo, CustomShowVersions, ...
智能建议
在用户输入了错误的命令(Command)、标志(Flag)时,cmdr 会停止进一步的解析,报错、并提出智能推断和建议。借助 subcommand 示例程序我们可以观察一下这个特性:
可以见到,对于未知命令 “pan” 而言,cmdr 给出了两个意见,这或许能有助于用户解决自己的问题。
对于未知标志来说也是相似的。
算法内幕
cmdr 早期采用 soundex 算法进行相似度判定。
自从 cmdr v1.1.3 开始,我们使用更强大的 Jaro-Winkler distance 算法来提供更精准的建议。所以你会看到现在的建议值更符合期待。
定制智能建议相似度
Jaro-Winkler distance 算法对单词计算出一个浮点数来表示相似程度,这个值越接近 1.0 则表示相似度越高。cmdr 则使用一个阈值来决定两个单词是不是“相似的”。这个阈值默认为 0.6666666666666666。
可能从实际使用效果来看,这个阈值是相当合理的。但我们总是知道,这无法令所有人满意。所以你可以通过 WithSimilarThreshold(similarThreshold)
来调整这个阈值。
效果当然是见仁见智的。
定制智能建议行为
通过 WithUnknownOptionHandler
你可以指定一个错误选项捕获函数,然后自行决定应该如何向用户反馈该错误:
你可以编写一个 UnknownOptionHandler
函数然后返回 false 来拒绝默认的智能建议内容,这样你的反馈就是唯一的给用户的信息。
Panics
See Also: hedzr/cmdr-examples
panic 的相关测试可以这样:
onUnhandledErrorHandler
cmdr 内置了 recover 机制并将错误打包后返回到 Exec() 的调用者,但对于 onUnhandledError 可以单独处理:
这意味着当业务逻辑代码产生了异常时,你可以选择自己喜欢的提示方式。
和 hedzr/log
的整合
最小的 Logger 接口
hedzr/log
是一个小型库,它提供了一个 logger 所必需的通用接口:
Logger
LoggerConfig
此外,一个专用的配置结构为:
hedzr/log
所提供的 Logger 的最小化接口,有利于你采用自己喜欢的 Logger 后端。
log.Debugf
在 hedzr/log
中,提供了一组 package-level 的函数如 Debugf, Errorf,Fatalf,Infof, Warnf 等等,它们是被固定封装到 go "log" 的函数。
这组函数允许你通过全局方式进行日志输出,如果你不介意后端总是 “log” 的话。
内置 Loggers
hedzr/log
事实上,hedzr/log
内置了两个基本的 Logger:
std
是一个 go log
的包装。
dummy
是一个特殊的 logger,向它发出的日志全数都被抛弃。所以它被用于实现 log.OffLevel
特性。
cmdr.Logger
在 cmdr
系统中,现在已经内置了一个 package 级别的 Logger 变量。
当你使用 cmdr.WithLogx(logger)
时,你实际上是在向这个包级别的变量赋值。
该变量的默认值是 dummyLogger,什么也不打印出来。
所以在全局,你可以借助它进行日志输出:
和 hedzr/logex
相整合
通过 hedzr/logex
我们也提供了面向 logrus / zap 的 log.Logger 封装,你可以直接使用:
借助于 build.New(logConfig)
,你不必关心应该如何与某个 logger 后端完成整合,直接使用就好。
直接建立 log.Logger 实例
通过 hedzr/logex
提供的 logx
特性,你可以透明无感知地创建 logrus, zap, zap sugar 后端。这些后端都经过了必须的配置,不但能够打印出上下文函数名、所在文件位置,也向你提供了真正开箱即用的后端日志记录器。
打印出文件位置,对于使用 GoLand 的小伙伴们将会是很重要的特性。因为无论是在 console 窗口还是 inner Terminal 窗口,你都可以点击文件位置的超链接直达代码相关处以寻找问题的根源。
借助于配置文件
LoggerConfig 在 cmdr 中可以通过配置文件映射的方式被构建:
See Also: hedzr/cmdr-examples
运行该示例需要切换工作目录:
扩展你自己的 Logger
由于 cmdr
最初的动因是源于微服务开发的实际需求,因此我们在 Logging 方面不太重视日志文件输出(但我们确实支持标准的 /var/log/<appname>/<appname>.log
)、日志文件切割等场景,而是重视 stdout,stderr 输出能力。这里有两(、三)个理由:
- 在 K8s, Docker 场景中,日志应该被送至标准输出设备,基础设施负责对其重定向
- 在其他类似的场景中,同样是由基础设施来处理日志收集、集中问题,你没有理由自行开发日志旋转等等
高级
特性 - 如果你真的需要例如 fluentd 的输出能力,好吧,借助
log.Logger
实现一个外包装应该也很容易的。
Debug 增强
~~tree
~~
会引入一个不包含 Options 前缀的内部命名空间。
~~tree
是一个内置的特殊的命令行参数。它能够以层级缩进的方式打印出你的app的全部子命令列表:
这不仅有利于查找错误编码的关联关系,也有助于最终用户快速掌握 CLI 工具的全貌。
~~debug
通过 ~~debug
可以打印出 Option Store 的键值对,这通常有助于帮助你确定配置文件加载问题,命令行参数赋值和识别问题等等。
这个输出内容的一个片段截取如下:
一般来说,输出内容可能都是很长的,请自行尝试。
为了确保 ~~debug
能够被输出并停止 app 运行,通常需要你添加 --help
与其配套。使用与否,由你根据自己的需要实际确定。
有时候你也可能通过一个 timer proc 来打印 OptionStore 的内容,你可以通过 cmdr.DumpXXX
函数来取得一个 yaml 表示的字符串,又或者直接通过 cmdr.GetXXX
抽出具体值。
Strict 模式
Strict Mode
是一种针对错误和警告的模式,缺省值为未启用(false)。当严格模式被启用时,未知命令和标志会被当作错误(error)而终止进一步的处理。否则在默认状态下,未知命令和标志仅仅会报告一条警告信息,但 cmdr 会尝试继续执行剩余的解析和处理已经解析到的子命令。
除此之外,如果在抽取一个标志的设定值部分出现了错误(例如数据类型不符合切无法自动转换,期待数值但正在抽取一个字母序列等等),也会收到 Strict Mode 的节制。
以下的方式可以启用 Strict 严格模式:
-
通过指定环境变量
APP_STRICT_MODE=true
来运行程序 -
通过内建但隐藏的标志
--strict-mode
可以从命令行参数上启用严格模式 -
可以通过配置文件中直接指定:
-
编程方式在 Exec(root, opts) 中指定:
Builtin commands and flags
Predefined commands and flags:
- Help:
-h
,-?
,--help
,--info
,--usage
,--helpme
, ... - Version & Build Info:
--version
/--ver
/-V
,--build-info
/-#
- Simulating version at runtime with
—version-sim 1.9.1
- generally,
conf.AppName
andconf.Version
are originally. ~~tree
: list all commands and sub-commands.--config <location>
: specify the location of the root config file.- 提供一个顶级的
versions
子命令,别名version
/ver
也可用。
- Simulating version at runtime with
- Verbose & Debug:
—verbose
/-v
,—debug
/-D
,—quiet
/-q
- Generate Commands:
generate shell
:—bash
/—zsh
(todo)/--auto
generate manual
: man 1 ready.generate doc
: markdown ready.
cmdr
Specials:--no-env-overrides
, and--strict-mode
--no-color
: print the plain text to console without ANSI colors.
Predefined external config file locations
-
Predefined external config file locations:
-
/etc/<appname>/<appname>.yml
andconf.d
sub-directory. -
/usr/local/etc/<appname>/<appname>.yml
andconf.d
sub-directory. -
$HOME/.config/<appname>/<appname>.yml
andconf.d
sub-directory. -
$HOME/.<appname>/<appname>.yml
andconf.d
sub-directory. -
all predefined locations are:
-
since v1.5.0, uses
cmdr.WithPredefinedLocations("a","b",...),
-
Watch conf.d
directory
-
Watch
conf.d
directory:-
cmdr.WithConfigLoadedListener(listener)
AddOnConfigLoadedListener(c)
RemoveOnConfigLoadedListener(c)
SetOnConfigLoadedListener(c, enabled)
-
As a feature, do NOT watch the changes on
<appname>.yml
.- since v1.6.9,
WithWatchMainConfigFileToo(true)
allows the main config file<appname>.yml
to be watched.
- since v1.6.9,
-
on command-line:
-
supports muiltiple file formats:
- Yaml
- JSON
- TOML
-
cmdr.Exec(root, cmdr.WithNoLoadConfigFiles(false))
: disable loading external config files.
-
定制子文件夹
如果不喜欢使用 conf.d
作为将被自动搜索的子文件夹名,你可以替换它:
.d
后缀是 linux etc 的传统约定,这意味着某个服务、某个功能可以有一组文件被放在这个 .d
的文件夹中,并且将会被自动载入作为配置的一部分。
不过取决于你的系统工作环境以及相应的不同的惯例,你也可以使用别的名字。
辅助文件夹
正如你所知的,cmdr
会在一系列的约定位置寻找和主文件相关的配置文件,通常它的名字必须是 <appname>.yml
,后缀名可以是其它我们可以识别的名字,例如 .yaml
, .json
, .toml
等等。一旦找到了主配置文件,我们会检查 conf.d
文件夹中的其它配置文件并依次载入。
除了这个预定义的逻辑之外,cmdr 也允许你激活辅助配置文件(夹)功能:
当此功能被激活时,cmdr 在完成了标准的配置文件搜索逻辑之后,也会检查当前文件夹中的 .<appname>.yml
文件并加载它;此外,cmdr 还检查当前文件夹中的 .<appname>/
文件夹并尝试加载其中的配置文件,这些配置文件应该有我们可识别的后缀名。
Help Screen 定制
可以定制你自己的帮助屏幕输出,你可以通过自行实现 cmdr.Painter
接口例如 MyPainter
,并将其赋给 ExecWorker.currentHelpPainter
(借助 cmdr.WithHelpPainter(painter)
)即可实现帮助屏的自定义。
这种方式接管了内建的帮助屏幕构造逻辑,灵活度足够大,但工作量也不小。
除此而外,你可以少量地微调现有的帮助屏幕的部分元素。
为每个子命令定制尾部参数格式文本
一条子命令的帮助屏通常看起来是这样的:
在 Usages 段中,命令行组成部分被显示为 app current-command-series tails
格式。默认时 tails
的文本为 [Sub-Commands] [tail args...] [Options] [Parent/Global Options]
。
你可以使用定制这一部分文字,令其符合你的子命令的需要。
关闭 Help Screen 中的结束段落
在帮助屏幕的末尾,我们打印一段文字来做用法提示,默认时它是这样的:
通过 WithHelpTailLine(line)
你可以取代这段文字。
其原型如下:
定制命令行参数解析细节
WithOnSwitchCharHit
在命令行参数解析过程中,如果遇到单独的 "-" 字符,cmdr 会尝试进行回调。你可以通过
WithOnSwitchCharHit(fn)
指定一个相应的回调函数。其原型为:
WithOnPassThruCharHit
在命令行参数解析过程中,如果遇到 “--” 字符序列,cmdr 认为是遇到了 passthrough 字符,并且将会停止剩余命令行的解析。在此时,cmdr 同样地会尝试回调。你可以通过 WithOnPassThruCharHit(fn)
指定一个相应的回调函数。其原型为:
WithAfterArgsParsed
当整个命令行参数全部解析之后,cmdr 会尝试回调,该回调函数可以由 WithAfterArgsParsed(hookProc)
来指定。其原型为:
在你的这个回调函数正常返回之后,cmdr 将会尝试执行已经命中的子命令。
Walking on cmdr
Commands Hierarchy Tree
在 cmdr.Exec(...)
的初始阶段,我们会发现全部的命令、标志,并对这些命令标志进行有效性校验,最后为它们建立起完整的关联关系。
在这之后,我们会处理用户输入的命令行,将其映射为 Option Store 中的配置值,最后调用已经识别到的子命令(如果没有子命令,那就是Root命令)的 Action 处理程序。
因此,在你定义的 Action 处理程序中,借助传入的 cmd *Command
参数,你可以回溯所有上级命令直至 Root,同时,也包括每个命令所拥有的标志集合。反过来,从 Root 命令开始,你也能够遍历一切子命令及其标志。
我们已经知道 ~~tree
标志能够以树状结构打印出全部子命令:
--tree
实际上是利用了 cmdr
内建的 WalkAllCommands()
所提供的遍历方式。
对所有命令及其选项进行遍历,实际上有两种方式:一是利用 Painter
以及相应的内部机制,二是通过 WalkAllCommands
明确地遍历。
Painter
Painter
是一个接口。它被用在输出帮助屏这个方面。尽管输出帮助屏只是一个小小的功能,但你还是可以自定义它的行为。你可以自行实现 Painter
接口并通过 SetCurrentHelpPainter(painter)
来更改帮助屏的显示内容。
如果你真的想这么做,可以查阅 Painter 的定义,也可以 issue 到我,或许说不定我能够有所建议。
Walker
WalAllCommands(cmd, index, walker)
是一个更为强大的遍历器,实际上 manpage,markdown 的输出就是通过这个机制来实现的。利用这个遍历器,你可以便利整个命令集的树状结构。一般来说,你应该给它传递 cmd=nil, index=0
的参数值来开始你的遍历,这表示将会从顶级命令开始遍历,而且将其视作第 0 层。index
这个参数将会在遍历器递归时自动修正到符合层级计数,然后会被传递给 walker。我只是懒得将它改成 level
名字了,它就是那个用途。
例如 --tree
的实现源代码如下:
比较
可以想象到你能够借助这个遍历器实现某些更强大的特性,在具备遍历能力的基础上,我们其实可以设计更强大的命令行界面结构,而不必担心过分复杂带来的负面效果。
关于如何设计命令行界面的体系结构,保持其清晰性,这个不是我们再这个系列文章中要讨论的话题。
至于 Painter 和 Walker,其区别也很明显。Painter 是被限定在帮助屏构造层面的,且不会递归下去,除非你想自行实现。Walker 是全局层面的递归遍历器,面向的是所有的命令。
动态扩充命令
_1. Extensions 子命令分组
可参考:子命令 - Extentsions 子命令分组 有关章节
since v1.7.21
_2. 通过 Golang 插件机制进行扩充
since v1.7.23
现在 cmdr
已经支持所谓的 cmdr-addons
了,你可以通过 cmdr 提供的插件接口 cmdr-base/plugin.go 来编写一个 Golang Plugin,从而允许在主程序的基础上动态地扩充命令。
请勿与 cmdr-addons
项目相混淆,cmdr-addons 项目是一个 GitHub repository,提供一些对 cmdr 进行增强的特性,例如 dex
守护进程支持,等等。
由于 cmdr-addons 的相关接口具备更精细的定义能力,因此这种方式允许你扩充完全支持 cmdr 各种特性的子命令层级以及附带的任意标志集合。
Golang 插件接口
在 cmdr-base 项目中,插件接口具有如下的定义:
编写你的 cmdr-addon
请参考 plugin/demo/demo_addon.go:
编译为二进制动态库
Golang 插件使用这样的命令完成编译:
对于你的项目而言,我们约定一个 cmdr-addon
的 so 文件可以被放在 ./ci/local/share/fluent/addons/
之中,cmdr 将会检测到该插件的存在并装入它。这个文件夹适用于开发于调试期间。
对于你的最终发行二进制包来说,这些位置将会被 cmdr 的检测以求寻找符合 cmdr-addon 接口的 *.so
文件:
你总是可以通过 cmdr.WithPluginLocations(locations ...)
来指定想要的搜索路径。
运行主程序及 cmdr-addons 扩展
所有被找到的 cmdr-addons
所定义的命令及其标志集合,将被添加到 Addons 命令分组中,如同这样:
而执行这些扩充的命令,和执行以前主程序中的子命令并无任何不同:
_3. 通过配置文件定义别名段
since v1.7.25
你当然知道 git 可以通过全局配置文件 .gitconfig 来定义命令别名,这些别名和 git 的内置命令的调用方式是同样的:
cmdr
也支持通过配置文件的方式来定义新的命令,并且具有更强大的能力。
在 ./ci/etc/fluent/conf.d/91.cmd-aliases.yml
中,我们为 ./examples/fluent
定义了这样的一组扩充命令:
在这个示例样本中,我们展示了 cmdr 所支持的三种命令执行方式:
- 命令别名
- 可执行文件
- shell 脚本
至于命令相关的标志集合应该怎样定义,请参考 Flag
的 Struct Tags 声明。
这个样本所定义的扩展别名均被列在 Aliases
子命令分组之下,除非你在 app.aliases.group 中另行指定了分组名。
实际运行之后,第一级命令包括:
services
子命令还包括嵌套命令:
命令别名
命令别名由 invoke
字段定义,如同这样:
invoke
字段指明的是一个 子命令的路径,"/" 代表根 Root 命令,路径字符串依次定义了每一级子命令的名称(Command.Full
)。子命令路径字符串也支持 “." 和 “..”,至于 "//" 所分隔的空命令被忽略不计。
可选地,你可以在子命令路径之后附带一些标志定义。请注意,我们建议你谨慎地在这里使用目标命令的所属标志集,尽管原则上我们并没有任何附加的限制。
例如 fluent 样本示例支持一个顶级命令 “kb” 且允许 --size xxx
标志作为其选项。因此,kx3
这个命令别名将会展开为 "kb" 并且自带 --size 2kb
作为其附加参数。于是别名的执行结果如同这样:
可执行文件
你也可以通过 invoke-proc
字段指定执行一个 elf/exe 二进制可执行文件:
其执行效果如同这样:
shell 脚本
通过 invoke-sh
字段指定执行一个 Shell 脚本:
这个方式定义的扩充命令,和通过在 Extensions 文件夹放入 Shell 脚本的效果是等价的,你可以自行决定如何应用。
🔚
How is this guide?
Last updated on