暂时来说,这是最后一篇关于 cmdr
的系列介绍文章了。git
全部这个系列包括:github
这一次的内容算是杂烩乱炖。数据库
~~debug
已经在前文讲述过了。这里再也不凑字数了。小程序
--tree
cmdr
提供了一个内置的选项:--tree
。segmentfault
虽然这是一个选项,但它和 --version
同样是有着命令同样的效果:若是 cmdr
在命令行参数中检测到了 --tree
,那么它会忽略已经处理的和将要处理的子命令、选项,直接执行 --tree
的 Action
。markdown
要想达到相似的效果并不困难:定义一个选项,重载其 Action 字段到一个响应函数,而且在该响应函数的结尾返回
cmdr.ErrShouldBeStopException
,这样就会在该选项被识别时并执行Action后直接退出应用程序了。函数
--tree
的功能是打印出所有命令和子命令,以树结构方式呈现出来。微服务
一个样例以下图:ui
这是我在开发阶段执行 examples/demo 小程序所获得的结果。
--tree
其实是利用了 cmdr
内建的 WalkAllCommands()
所提供的遍历方式。spa
对全部命令及其选项进行遍历,实际上有两种方式:一是利用 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 是全局层面的递归遍历器,面向的是全部的命令。
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。
Command
在 Command
的 Action
被执行以前,其 PreAction
会被首先调用,你能够定义本身的逻辑,例如检查特定条件是否知足,若是不知足则返回一个error以通知cmdr错误性结束。若是你认为并无错误发生,但仍应该结束处理,你能够返回一个 cmdr.ErrShouldBeStopException
,这样的话 cmdr
也会结束处理,但整个 program 会被正常终止:反应到 Shell 层面上时,此时程序是无错的,Shell返回值为0。
在 Command
的 Action
被执行以后,不管处理结果如何,PostAction
将会被调用。它能够用来进行某些退出时逻辑处理。
RootCommand
在被命中的命令的 PreAction
和 PostAction
被执行之际,RootCommand
的 PreAction
和 PostAction
也会分别被执行。这是一个关键性的特性。它使得你能够妥善地实现你的全局预处理和后处理逻辑,例如注册微服务到注册中心以及撤销注册,链接到数据库和关闭数据库链接,等等。
Flag
对于 Flag 来讲,没有 Pre/Post 机制。
Flag 具备 Action 的 Hook,因此你能够在每一个 Flag 被命中时作点什么事。例如,反转或复位 Owner 的全部其余 bool flags,或者为应用程序的其余配置设定一整组预设方案,等等。
值得一提的是,在 v0.2.15 以后,咱们已经实现了 ToggleGroup
,所以对于想要创建RadioButtonGroup 效果的场景来讲,你却是没必要再手写逻辑了。
Xref 术语是一个特定节点的表述。
当 cmdr.Exec(rootCmd) 进入状态时,它依次作这些事:
在 buildXref 阶段,cmdr
实际上创建了 rootCmd 及其全部子命令和选项集合 的内部map、索引等等交叉引用。这个过程当中,cmdr
处理 rootCmd,也查找配置文件以及子目录 conf.d,也对环境变量进行搜寻。
当 buildXref 完成以后,cmdr.GetXXX()
就是彻底可用的状态了。
如此,beforeXrefBuilding 和 afterXrefBuilt 的 hooks 其实是你想要进行定制化操做的关键节点。
欢迎使用。
应该没必要解释太多。
当外部文件被修改时,cmdr自动载入变化,并合并到已有的选项集合中,而后发出回调通知观察者应该作点什么了。
你能够在任何地方任意地发出观察约定。总体上说这是极为轻量级的。这么作的缘由是不一样业务模块有必要自行管理本身的相关选项集而不是在全局的某一个集中点处理所有模块的所有选项集合,那太耦合了。
须要特别指出的是,配置文件老是一个主文件,加上可选多个子配置文件(都被放在配置文件所在目录的 conf.d
子目录之下)。而咱们内部的文件系统 Watcher 只会监听 conf.d 之中的文件变化,而忽略主文件的变化。
因此你不该该在主配置文件中放置太多具体选项。好的实践是将一切配置都切分到 conf.d 之中。
SetPredefinedLocations(locations string) 容许你指定配置文件的搜索路径。
SetOnConfigLoadedListener
的用途是临时禁用和启用一个观察者。
说它们是 Hook 也不算错。
cmdr
自动提供 ShowBuildInfo 和 ShowVersion 实现,用于打印 编译信息屏 和 版本信息屏。
SetCustonShowBuildInfo 和 SetCustomShowVersion 则容许你自行提供你的实现。
暂时结束。继续改进。