本文使用 cobra 库实现一个命令行工具,类似 git、docker、kubectl 这类的工具。 本文仅为一个初具模型的示例,但有实践参考意义。
起因
在编程中,很多时候,程序都会处理多个参数,特别是一些工具类的函数,需要整合较多功能,即使同一功能,也会有不同参数,利用配置文件或命令选项方式,可使程序具备通用性,也具扩展性。
简单介绍
cobra 功能较强大,在 golang 生态中有很多应用,如大名鼎鼎的 docker。其支持子命令执行,配置文件读写等,本文以实战为目的,不过多介绍。
整体结构
工程名为 cmdtool,见名知义。 工程目录及对应介绍如下:
.├──cmd##子命令总目录│├──db##子命令1实现目录│├──misc##子命令2实现目录│├──rootCmd.go##子命令入口│└──test##子命令3实现目录├──common##共用函数、变量│├──conf│├──constants│└──globalfunc.go├──config.yaml##配置文件├──go.mod├──go.sum├──main.go##入口函数├──mybuild.sh##编译脚本├──pkg##库│├──com│└──wait├──README└──vendor##依赖库├──github.com├──golang.org├──gopkg.in├──k8s.io└──xorm.io
其中 cmd 是所有子命令的入口目录,不同子命令,以不同子目录形式存在。common 目录存在共用的变量或初始化函数,等等。pkg 为个人总结积累的一些有用的库。 main.go 为主函数,调用了 cmd/rootCmd.go 的创建命令函数,由此进入 cobra 的处理框架中。 一般情况下,只需要扩展 cmd 目录下子命令,并补充 rootCmd.go 函数即可,其它即为业务程序的处理。 注:原本设计的思路是,在子命令包的 init 函数中自动注册到 rootCmd 中,但发现不一定符合逻辑,故舍弃,需手动在 rootCmd 添加。
工程分解
入口函数
主入口函数非常简单,实际调用了 rootCmd.go 中的执行函数。
packagemainimport(_"fmt""os"rootCmd"github.com/latelee/cmdtool/cmd")funcmain(){iferr:=rootCmd.Execute();err!=nil{os.Exit(1)}}
命令行入口
rootCmd.go 源码:
packagecmdimport("os""bytes""path/filepath""github.com/spf13/cobra""github.com/spf13/viper""github.com/fsnotify/fsnotify""k8s.io/klog"test"github.com/latelee/cmdtool/cmd/test"misc"github.com/latelee/cmdtool/cmd/misc"db"github.com/latelee/cmdtool/cmd/db"conf"github.com/latelee/cmdtool/common/conf")var(longDescription=`databasetesttool.命令终端测试示例工具。`example=`commingsoon...`)varcfgFilestringvarrootCmd=&cobra.Command{Use:filepath.Base(os.Args[0]),Short:"databasetool",Long:longDescription,Example:example,Version:"1.0",}funcExecute()error{rootCmd.AddCommand(test.NewCmdTest())rootCmd.AddCommand(misc.NewCmdMisc())rootCmd.AddCommand(db.NewCmdDb())returnrootCmd.Execute()}funcinit(){cobra.OnInitialize(initConfig)rootCmd.PersistentFlags().StringVar(&cfgFile,"config","","configfile(config.yaml)")rootCmd.PersistentFlags().BoolVar(&conf.FlagPrint,"print",false,"willprintsth")}varyamlExample=[]byte(`dbserver:dbstr:hellooooootimeout:connect:67ssingleblock:2sname:name:firstblood`)funcinitConfig(){ifcfgFile!=""{viper.SetConfigFile(cfgFile)}else{viper.AddConfigPath("./")viper.SetConfigName("config")viper.SetConfigType("yaml")}viper.AutomaticEnv()err:=viper.ReadInConfig();iferr!=nil{klog.Println("notfoundconfigfile.usingdefault")viper.ReadConfig(bytes.NewBuffer(yamlExample))viper.SafeWriteConfig()}conf.FlagDBServer=viper.GetString("dbserver.dbstr")conf.FlagTimeout=viper.GetString("dbserver.timeout.connect")conf.FlagName=viper.GetString("dbserver.name.name")klog.Println(conf.FlagDBServer,conf.FlagTimeout,conf.FlagName)//设置监听回调函数viper.OnConfigChange(func(efsnotify.Event){conf.FlagTimeout=viper.GetString("dbserver.timeout.connect")})viper.WatchConfig()}
其中 initConfig 函数作用是读取配置文件字段,如果没有文件则自动生成默认的配置。注意,该函数的 yamlExample 需要保持实际配置文件的格式(从 viper.GetString 函数参数可以看出 dbserver 为顶层字段)。 最后利用 viper 监听配置文件的变化。实际测试发现会触发2次,利用循环定时判断变量值可以解决。
子命令实现
子命令的实现形式大同小异,以 test 为例,源码如下:
packagecmdimport("github.com/spf13/cobra"_"github.com/spf13/pflag""k8s.io/klog")var(name=`test`shortDescription=`testcommand`longDescription=`test...`example=`examplecommingup...`)typeUserCmdFuncstruct{namestringfnfunc(args[]string)}funcNewCmdTest()*cobra.Command{varcmd=&cobra.Command{Use:name,Short:shortDescription,Long:longDescription,Example:example,RunE:func(cmd*cobra.Command,args[]string)error{if(len(args)==0){klog.Warning("noargsfound")returnnil}if(args[0]=="foo"){foo(args)}elseif(args[0]=="watch"){testWatch(args)}else{klog.Printf("cmd'%v'notsupport",args[0])returnnil}returnnil},}returncmd}
在 NewCmdTest 函数中创建 cobra.Command 并返回,在 RunE 中判断参数并真正执行业务函数。本例实现了参数监听功能,源码:
//监听配置参数变化functestWatch(args[]string){timeout:=conf.FlagTimeoutfor{iftimeout!=conf.FlagTimeout{klog.Printf("paramchanged:%v\n",conf.FlagTimeout)timeout=conf.FlagTimeout}com.Sleep(1000)}}
当配置文件相应字段变化时,将其打印出来。
测试
默认输出帮助信息:
$./cmdtool.exedatabasetesttool.命令终端测试示例工具。Usage:cmdtool.exe[command]Examples:commingsoon...AvailableCommands:dbdbcommandhelpHelpaboutanycommandmiscmisccommandtesttestcommandFlags:-h,--helphelpforcmdtool.exe--printwillprintsth--versionversionforcmdtool.exeUse"cmdtool.exe[command]--help"formoreinformationaboutacommand.
执行子命令:
$./cmdtool.exetestfoo[2020-10-2021:46:39.304rootCmd.go:113]helloooooo61sfirstblood[2020-10-2021:46:39.305busy.go:12]testfoo.....
监听配置文件:
$./cmdtool.exetestwatch[2020-10-2021:47:14.408rootCmd.go:113]helloooooo61sfirstblood[2020-10-2021:47:29.411busy.go:20]paramchanged:100s
源码
源码在此。
其它事项
利用viper.SafeWriteConfig()
写配置文件时,发现 yamlExample 添加的注释会被删除,所以可以考虑直接将字符串通过ioutil.WriteFile
写到文件。
viper 获取 yaml 参数的接口:
获取数值、字符串、字符串数组、数值数组GetIntGetInt32GetInt64GetUintGetUint32GetUint64GetStringGetStringSliceGetIntSlice