社区资源这么丰富我们怎么抄作业

社区资源这么丰富我们怎么抄作业

认识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执行。

  • 交互模式代码
func loop() {
	l, err := readline.NewEx(&readline.Config{
		Prompt:            "\033[31m»\033[0m ",
		HistoryFile:       "/tmp/readline.tmp",
		AutoComplete:      readlineCompleter,
		InterruptPrompt:   "^C",
		EOFPrompt:         "^D",
		HistorySearchFold: true,
	})
	if err != nil {
		panic(err)
	}
	defer l.Close()

	for {
		line, err := l.Readline()
		if err != nil {
			if err == readline.ErrInterrupt {
				break
			} else if err == io.EOF {
				break
			}
			continue
		}
		if line == "exit" {
			os.Exit(0)
		}
		args, err := shellwords.Parse(line)
		if err != nil {
			fmt.Printf("parse command err: %v\n", err)
			continue
		}
		Start(args)
	}
}

统一命令行模式与交互模式的执行方法

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 configCmd
func 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 subcommand
func 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是很好的命令行范例,基本包括了一个命令行工具该有的所有基本特性。
同学们,今天的作业就抄到这里。

1赞

感谢贾老师的分享

1赞