暂时来讲,这是最后一篇关于 cmdr
的系列介绍文章了。
所有这个系列包括:
- 另一个go命令行参数处理器 - cmdr
- cmdr 02 - 复刻一个 wget
- cmdr 03 - 用流式接口定义命令行参数处理选项
- cmdr 04 - 简单微服务
- cmdr 05 - 扫尾 - Tricks/Walks/Hooks
这一次的内容算是杂烩乱炖。
Tricks
~~debug
已经在前文讲述过了。这里不再凑字数了。
--tree
cmdr
提供了一个内置的选项:--tree
。
虽然这是一个选项,但它和 --version
一样是有着命令一样的效果:如果 cmdr
在命令行参数中检测到了 --tree
,那么它会忽略已经处理的和将要处理的子命令、选项,直接执行 --tree
的 Action
。
要想达到类似的效果并不困难:定义一个选项,重载其 Action 字段到一个响应函数,并且在该响应函数的结尾返回
cmdr.ErrShouldBeStopException
,这样就会在该选项被识别时并执行Action后直接退出应用程序了。
--tree
的功能是打印出全部命令和子命令,以树结构方式呈现出来。
一个样例如下图:
这是我在开发阶段执行 examples/demo 小程序所得到的结果。
Walk for all commands
--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
的实现源代码如下:
func dumpTreeForAllCommands(cmd *Command, args []string) (err error) {
command := &rootCommand.Command
_ = walkFromCommand(command, 0, func(cmd *Command, index int) (e error) {
if cmd.Hidden {
return
}
deep := findDepth(cmd) - 1
if deep == 0 {
fmt.Println("ROOT")
} else {
sp := strings.Repeat(" ", deep)
// fmt.Printf("%s%v - \x1b[%dm\x1b[%dm%s\x1b[0m\n",
// sp, cmd.GetTitleNames(),
// BgNormal, CurrentDescColor, cmd.Description)
if len(cmd.Deprecated) > 0 {
fmt.Printf("%s\x1b[%dm\x1b[%dm%s - %s\x1b[0m [deprecated since %v]\n",
sp, BgNormal, CurrentDescColor, cmd.GetTitleNames(), cmd.Description,
cmd.Deprecated)
} else {
fmt.Printf("%s%s - \x1b[%dm\x1b[%dm%s\x1b[0m\n",
sp, cmd.GetTitleNames(), BgNormal, CurrentDescColor, cmd.Description)
}
}
return
})
return ErrShouldBeStopException
}
比较
可以想象到你能够借助这个遍历器实现某些更强大的特性,在具备遍历能力的基础上,我们其实可以设计更强大的命令行界面结构,而不必担心过分复杂带来的负面效果。
关于如何设计命令行界面的体系结构,保持其清晰性,这个不是我们再这个系列文章中要讨论的话题。
至于 Painter 和 Walker,其区别也很明显。Painter 是被限定在帮助屏构造层面的,且不会递归下去,除非你想自行实现。Walker 是全局层面的递归遍历器,面向的是所有的命令。
Actions
Action for Command
Command
的 Action
字段可以定义你的命令的业务实现逻辑。
func MsTagsList(cmd *cmdr.Command, args []string) (err error) {
return
}
一般来说,你在 impl
package 中定义业务实现逻辑的入口,如同上面的代码示例,并在某个 Command
的数据定义中引用它。
msTagsListCommand = &cmdr.Command {
BaseOpt: cmdr.BaseOpt {
Short: "ls",
Full: "list",
Description: "list all tags of a micro-service",
Action: impl.MsTagsList,
}
}
所以,对于 Command
来说,Action 可能是最重要的 Hook。
PreAction & PostAction for Command
在 Command
的 Action
被执行之前,其 PreAction
会被首先调用,你可以定义自己的逻辑,例如检查特定条件是否满足,如果不满足则返回一个error以通知cmdr错误性结束。如果你认为并没有错误发生,但仍应该结束处理,你可以返回一个 cmdr.ErrShouldBeStopException
,这样的话 cmdr
也会结束处理,但整个 program 会被正常终止:反应到 Shell 层面上时,此时程序是无错的,Shell返回值为0。
在 Command
的 Action
被执行之后,无论处理结果如何,PostAction
将会被调用。它可以用来进行某些退出时逻辑处理。
PreAction & PostAction for RootCommand
在被命中的命令的 PreAction
和 PostAction
被执行之际,RootCommand
的 PreAction
和 PostAction
也会分别被执行。这是一个关键性的特性。它使得你可以妥善地实现你的全局预处理和后处理逻辑,例如注册微服务到注册中心以及撤销注册,连接到数据库和关闭数据库连接,等等。
Action for Flag
对于 Flag 来说,没有 Pre/Post 机制。
Flag 具有 Action 的 Hook,所以你可以在每个 Flag 被命中时做点什么事。例如,反转或复位 Owner 的所有其他 bool flags,或者为应用程序的其他配置设定一整组预设方案,等等。
值得一提的是,在 v0.2.15 之后,我们已经实现了 ToggleGroup
,因此对于想要建立RadioButtonGroup 效果的场景来说,你倒是不必再手写逻辑了。
Listeners
AddOnAfterXrefBuilt(cb HookXrefFunc), AddOnBeforeXrefBuilding(cb HookXrefFunc)
Xref 术语是一个特定节点的表述。
当 cmdr.Exec(rootCmd) 进入状态时,它依次做这些事:
- 初始化相关内务
- 调用所有 beforeXrefBuilding Hooks
- buildXref
- 调用所有 afterXrefBuilt Hooks
- 开始处理命令行的所有参数
- ...
在 buildXref 阶段,cmdr
实际上建立了 rootCmd 及其所有子命令和选项集合 的内部map、索引等等交叉引用。这个过程中,cmdr
处理 rootCmd,也查找配置文件以及子目录 conf.d,也对环境变量进行搜寻。
当 buildXref 完成之后,cmdr.GetXXX()
就是完全可用的状态了。
如此,beforeXrefBuilding 和 afterXrefBuilt 的 hooks 实际上是你想要进行定制化操作的关键节点。
欢迎使用。
AddOnConfigLoadedListener,RemoveOnConfigLoadedListener,SetOnConfigLoadedListener
- AddOnConfigLoadedListener(c ConfigReloaded)
- RemoveOnConfigLoadedListener(c ConfigReloaded)
- SetOnConfigLoadedListener(c ConfigReloaded, enabled bool)
应该不必解释太多。
当外部文件被修改时,cmdr自动载入变化,并合并到已有的选项集合中,然后发出回调通知观察者应该做点什么了。
你可以在任何地方任意地发出观察约定。整体上说这是极为轻量级的。这么做的原因是不同业务模块有必要自行管理自己的相关选项集而不是在全局的某一个集中点处理全部模块的全部选项集合,那太耦合了。
需要特别指出的是,配置文件总是一个主文件,加上可选多个子配置文件(都被放在配置文件所在目录的 conf.d
子目录之下)。而我们内部的文件系统 Watcher 只会监听 conf.d 之中的文件变化,而忽略主文件的变化。
所以你不应该在主配置文件中放置太多具体选项。好的实践是将一切配置都切分到 conf.d 之中。
SetPredefinedLocations(locations string) 允许你指定配置文件的搜索路径。
SetOnConfigLoadedListener
的用途是临时禁用和启用一个观察者。
SetCustomShowBuildInfo(fn func()),SetCustomShowVersion(fn func())
说它们是 Hook 也不算错。
cmdr
自动提供 ShowBuildInfo 和 ShowVersion 实现,用于打印 编译信息屏 和 版本信息屏。
SetCustonShowBuildInfo 和 SetCustomShowVersion 则允许你自行提供你的实现。
小结
暂时结束。继续改进。