阅读 284

Golang实践录:命令行cobra库实例

本文使用 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 中的执行函数。

package main

import (
    _ "fmt"
    "os"
    rootCmd "github.com/latelee/cmdtool/cmd"
)

func main() {
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}复制代码

命令行入口

rootCmd.go 源码:

package cmd

import (
    "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 = `  database test tool.
  命令终端测试示例工具。
`
    example = `  comming soon...
`
)

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   filepath.Base(os.Args[0]),
    Short: "database tool",
    Long: longDescription,
    Example: example,
    Version: "1.0",
}

func Execute() error {
    rootCmd.AddCommand(test.NewCmdTest())
    rootCmd.AddCommand(misc.NewCmdMisc())
    rootCmd.AddCommand(db.NewCmdDb())

    return rootCmd.Execute()
}

func init() {
    cobra.OnInitialize(initConfig)

    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (config.yaml)")

    rootCmd.PersistentFlags().BoolVar(&conf.FlagPrint, "print", false, "will print sth")

}

var yamlExample = []byte(
`dbserver:
  dbstr: helloooooo
  timeout:
    connect: 67s
    singleblock: 2s
  name:
    name: firstblood
`)

func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        viper.AddConfigPath("./")
        viper.SetConfigName("config")
        viper.SetConfigType("yaml")
    }

    viper.AutomaticEnv()

    err := viper.ReadInConfig();
    if  err != nil {
        klog.Println("not found config file. using default")
        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(e fsnotify.Event) {
        conf.FlagTimeout = viper.GetString("dbserver.timeout.connect")
    })

    viper.WatchConfig()

}复制代码

其中 initConfig 函数作用是读取配置文件字段,如果没有文件则自动生成默认的配置。注意,该函数的 yamlExample 需要保持实际配置文件的格式(从 viper.GetString 函数参数可以看出 dbserver 为顶层字段)。 最后利用 viper 监听配置文件的变化。实际测试发现会触发2次,利用循环定时判断变量值可以解决。

子命令实现

子命令的实现形式大同小异,以 test 为例,源码如下:

package cmd

import (
    "github.com/spf13/cobra"
    _ "github.com/spf13/pflag"
    
    "k8s.io/klog"
)

var (
    name = `test`
    shortDescription = `  test command`
    longDescription  = `  test...
`
    example = `  example comming up...
`
)

type UserCmdFunc struct {
    name string
    fn func(args []string)
}

func NewCmdTest() *cobra.Command{
    
    var cmd = &cobra.Command{
        Use:     name,
        Short:   shortDescription,
        Long:    longDescription,
        Example: example,
        RunE: func(cmd *cobra.Command, args []string) error {
            if (len(args) == 0) {
                klog.Warning("no args found")
                return nil
            }

            if (args[0] == "foo"){
                foo(args)
            } else if (args[0] == "watch"){
                testWatch(args)
            } else {
                klog.Printf("cmd '%v' not support", args[0])
                return nil
            } 
            return nil
        },
    }

    return cmd
}复制代码

在 NewCmdTest 函数中创建 cobra.Command 并返回,在 RunE 中判断参数并真正执行业务函数。本例实现了参数监听功能,源码:

// 监听配置参数变化
func testWatch(args []string) {
    timeout := conf.FlagTimeout
    for {
        if timeout != conf.FlagTimeout {
            klog.Printf("param changed: %v\n", conf.FlagTimeout)
            timeout = conf.FlagTimeout
        }
        com.Sleep(1000)
    }
}复制代码

当配置文件相应字段变化时,将其打印出来。

测试

默认输出帮助信息:

$ ./cmdtool.exe
  database test tool.
  命令终端测试示例工具。

Usage:
  cmdtool.exe [command]

Examples:
  comming soon...


Available Commands:
  db            db command
  help        Help about any command
  misc          misc command
  test          test command

Flags:
  -h, --help      help for cmdtool.exe
      --print     will print sth
      --version   version for cmdtool.exe

Use "cmdtool.exe [command] --help" for more information about a command.复制代码

执行子命令:

$ ./cmdtool.exe test foo
[2020-10-20 21:46:39.304 rootCmd.go:113] helloooooo 61s firstblood
[2020-10-20 21:46:39.305 busy.go:12] test foo.....复制代码

监听配置文件:

$ ./cmdtool.exe test watch
[2020-10-20 21:47:14.408 rootCmd.go:113] helloooooo 61s firstblood
[2020-10-20 21:47:29.411 busy.go:20] param changed: 100s复制代码

源码

源码在此。

其它事项

利用viper.SafeWriteConfig()写配置文件时,发现 yamlExample 添加的注释会被删除,所以可以考虑直接将字符串通过ioutil.WriteFile写到文件。

viper 获取 yaml 参数的接口:

获取数值、字符串、字符串数组、数值数组
GetInt GetInt32 GetInt64 GetUint GetUint32 GetUint64 GetString GetStringSlice GetIntSlice


作者:李迟
链接:https://juejin.cn/post/7026508637149331487


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐