作者: jiashiwen 原文来源:https://tidb.net/blog/ef9c5414
社区资源这么丰富我们怎么抄作业
认识 tidb 已经有 4 年了,以前一直在架构、部署、调忧等应用层打转。最近在开发一个客户端 cli 程序,情不自禁的想起了社区中的各种 ctl。于是借鉴一番不仅完成了自己的项目,同时为 pd-ctl 贡献了一个 feature。目前已被官方 merge,估计下个版本能和大家见面了。整个过程蛮有意思,所以决定记录下来和大家分享一下。
代码那么多从哪儿抄起
tidb 系列的工程可以用浩瀚来形容,而且专业性很强。优化器、sql parse、存储引擎这些专业行比较强的东西没有相关的知识背景想读懂代码难如登天。所谓天下难事必作于易,我们可以先从最简单的客户端程序入手。pd-ctl 的 cli 机制和结构很值得借鉴。
pd-ctl 主要依赖 cobra(github.com/spf13/cobra) 和 readline(github.com/chzyer/readline) 实现命令行以及交互模式。cobra 用于解析命令并返回命令执行结果;readline 用于交互模式下读取行,并通过 shellwords 解析成一系列参数反送给 cobra 执行。
统一命令行模式与交互模式的执行方法
pd-ctl 应用 cobra 还是很巧妙的。通常我们编写命令行程序会利用 cobra 的 init 命令生成一组命令行框架,通常的目录树大概会是这样
C:.│ LICENSE│ main.go│ └─cmd root.go
复制代码
子命令通过 cobra add 命令在 cmd 目录下生成模板然后就可以愉快的实现命令行功能了。这对于单纯的命令模式的开发很便利,但是对于交互模式就比较麻烦。命令不好与 readline 相结合。
pd-ctl 的巧妙之处在于通过 cobra 的子命令模式把相关子命令的一系列动作形成一棵命令树,这样在交互模式下可以被复用
命令树代码
func NewConfigCommand() *cobra.Command { conf := &cobra.Command{ Use: "config <subcommand>", Short: "tune pd configs", } conf.AddCommand(NewShowConfigCommand()) conf.AddCommand(NewSetConfigCommand()) conf.AddCommand(NewDeleteConfigCommand()) conf.AddCommand(NewPlacementRulesCommand()) return conf}
// NewShowConfigCommand return a show subcommand of configCmdfunc NewShowConfigCommand() *cobra.Command { sc := &cobra.Command{ Use: "show [replication|label-property|all]", Short: "show replication and schedule config of PD", Run: showConfigCommandFunc, } sc.AddCommand(NewShowAllConfigCommand()) sc.AddCommand(NewShowScheduleConfigCommand()) sc.AddCommand(NewShowReplicationConfigCommand()) sc.AddCommand(NewShowLabelPropertyCommand()) sc.AddCommand(NewShowClusterVersionCommand()) sc.AddCommand(newShowReplicationModeCommand()) return sc}
// NewShowAllConfigCommand return a show all subcommand of show subcommandfunc NewShowAllConfigCommand() *cobra.Command { sc := &cobra.Command{ Use: "all", Short: "show all config of PD", Run: showAllConfigCommandFunc, } return sc}
func showAllConfigCommandFunc(cmd *cobra.Command, args []string) { r, err := doRequest(cmd, configPrefix, http.MethodGet) if err != nil { cmd.Printf("Failed to get config: %s\"n", err) return } cmd.Println(r)}
复制代码
观察一下每个子命令的 ”Use” 属性,以及 AddCommand 函数不难发现命令与子命令间的关系。以 ”config show all” 为例,
NewConfigCommand 函数添加子命令 NewShowConfig;NewShowConfig 添加 NewShowAllConfigCommand 子命令;showAllConfigCommandFunc 函数负责执行并输出结果。
那么 pdctl 是如何实现命令行与交互模式融合的呢?
我们看看 pd/tools/pd-ctl/pdctl/ctl.go。
getBasicCmd 函数负责收集所有子命令并生成 rootCmd,startCmd 函数用于执行命令。这样无论是交互模式还是非交互模式都可以通过 startCmd 执行命令并得到返回结果。
MainStart 函数其实是执行命令的入口,通过执行 getMainCmd 获取是否启用交互模式的 flag “interact”
func getMainCmd(args []string) *cobra.Command { rootCmd := getBasicCmd()
rootCmd.Flags().BoolVarP(&detach, "detach", "d", true, "Run pdctl without readline.") rootCmd.Flags().BoolVarP(&interact, "interact", "i", false, "Run pdctl with readline.") rootCmd.Flags().BoolVarP(&version, "version", "V", false, "Print version information and exit.") rootCmd.Run = pdctlRun
rootCmd.SetArgs(args) rootCmd.ParseFlags(args) rootCmd.SetOutput(os.Stdout)
readlineCompleter = readline.NewPrefixCompleter(genCompleter(rootCmd)...) return rootCmd}
复制代码
command complete 如何实现
对于交互模式如果有命令自动填充功能将会大大改善用户体验。
readline(github.com/chzyer/readline) 通过 *readline.PrefixCompleter 实现命令自动填充。一般情况需要 readline.PcItem() 手动构建 completer 的提示树
var completer = readline.NewPrefixCompleter( readline.PcItem("mode", readline.PcItem("vi"), readline.PcItem("emacs"), ), readline.PcItem("login"), readline.PcItem("say", readline.PcItemDynamic(listFiles("./"), readline.PcItem("with", readline.PcItem("following"), readline.PcItem("items"), ), ), readline.PcItem("hello"), readline.PcItem("bye"), ), readline.PcItem("setprompt"), readline.PcItem("setpassword"), readline.PcItem("bye"), readline.PcItem("help"), readline.PcItem("go", readline.PcItem("build", readline.PcItem("-o"), readline.PcItem("-v")), readline.PcItem("install", readline.PcItem("-v"), readline.PcItem("-vv"), readline.PcItem("-vvv"), ), readline.PcItem("test"), ), readline.PcItem("sleep"),)
复制代码
pd-cli 已经通过 rootCmd 形成了完整的命令数,只需要通过递归方法遍历即可,函数 GenCompleter 提供了构建方法
func GenCompleter(cmd *cobra.Command) []readline.PrefixCompleterInterface { pc := []readline.PrefixCompleterInterface{} if len(cmd.Commands()) != 0 { for _, v := range cmd.Commands() { if v.HasFlags() { flagsPc := []readline.PrefixCompleterInterface{} flagUsages := strings.Split(strings.Trim(v.Flags().FlagUsages(), " "), "\"n") for i := 0; i < len(flagUsages)-1; i++ { flagsPc = append(flagsPc, readline.PcItem(strings.Split(strings.Trim(flagUsages[i], " "), " ")[0])) } flagsPc = append(flagsPc, GenCompleter(v)...) pc = append(pc, readline.PcItem(strings.Split(v.Use, " ")[0], flagsPc...))
} else { pc = append(pc, readline.PcItem(strings.Split(v.Use, " ")[0], GenCompleter(v)...)) } } } return pc}
复制代码
pd-ctl 是很好的命令行范例,基本包括了一个命令行工具该有的所有基本特性。
同学们,今天的作业就抄到这里。
评论