前言

最近应为一直在写Go,避免不了要处理一些命令行参数和配置文件。虽然Go原生的flag库比起其他语言,在处理命令行参数上已经做的很易用了,Go的社区也有很多好用的库。这篇文章主要介绍一下自己这段时间接触使用过库,为有同样需求的朋友也提供一些参考。

flag

首先还是有必要简单介绍一下Go的原生库flag, 直接上代码

基本用法

var id = flag.Int("id", 1, "user id")
var mail = flag.String("mail", "test@gmail.com", "mail")
var help = flag.Bool("h", false, "this help")

也可以用指针变量去接收flag

var name string
flag.StringVar(&name, "name", "leeif", "your name")

变量也可以是一个实现flag.Value接口的结构体

type Address struct {
    s string
}

func (a *Address) String() string {
    return a.s
}

func (a *Address) Set(s string) error {
    if s == "" {
        return errors.New("address can't be empty")
    }
    a.s = s
    return nil
}

ad := Address{}
flag.Var(&ad, "address", "address of the server")

解析

flag.Parse()

flagSet可以用来处理subcommand

upload := flag.NewFlagSet("upload", flag.ContinueOnError)
localFile := upload.Bool("localFile", false, "")
download := flag.NewFlagSet("download", flag.ContinueOnError)
remoteFile := download.Bool("remoteFile", false, "")

switch os.Args[1] {
  case "upload":
    if err := upload.Parse(os.Args[2:]); err == nil {
      fmt.Println("upload", *localFile)
    }
  case "download":
    if err := download.Parse(os.Args[2:]); err == nil {
      fmt.Println("download", *remoteFile)
    }
}

命令行的指定形式。

-flag (也可以是--flag)
-flag=x
-flag x  // non-boolean flags only

原生的flag在简单的需求下,已经够用了,但是想构建一些复杂的应用的时候还是有些不方便。然而flag的可扩展性也衍生了许多各具特色的第三方库。

kingpin

一些主要的特点:

  • fluent-style的编程风格
  • 不仅可以解析flag, 也可以解析非flag参数
  • 支持短参数的形式
  • sub command

一般的使用方法

debug   = kingpin.Flag("debug", "Enable debug mode.").Bool()
// 可被环境变量覆盖的flag
// Short方法可以指定短参数
timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration()
// IP类型的参数
// Required参数为必须指定的参数
ip      = kingpin.Arg("ip", "IP address to ping.").Required().IP()
count   = kingpin.Arg("count", "Number of packets to send").Int()

用指针类型接收flag

var test string
kingpin.Flag("test", "test flag").StringVar(&test)

实现kingpin.Value接口的参数类型

type Address struct {
    s string
}

func (a *Address) String() string {
    return a.s
}

func (a *Address) Set(s string) error {
    if s == "" {
        return errors.New("address can't be empty")
    }
    a.s = s
    return nil
}

ad := Address{}
kingpin.Flag("address", "address of the server").SetValue

解析

kingpin.Parse()

使用sub command

var (
  deleteCommand     = kingpin.Command("delete", "Delete an object.")
  deleteUserCommand = deleteCommand.Command("user", "Delete a user.")
  deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.")
  deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.")
  deletePostCommand = deleteCommand.Command("post", "Delete a post.")
)

func main() {
  switch kingpin.Parse() {
  case deleteUserCommand.FullCommand():
  case deletePostCommand.FullCommand():
  }
}

kingpin会自动生成help文案。不用做任何设置用--help即可查看。-h则需要手动配置。

kingpin.HelpFlag.Short('h')

cobra

https://github.com/spf13/cobra
cobra是go程序员必须要知道的一款命令行参数库。很多大的项目都是用cobra搭建的。
cobra是为应用级的命令行工具而生的项目, 不仅提供了基本的命令行处理功能外, 而提供了一套搭建命令行工具的架构。

cobra的核型架构。

▾ appName/
    ▾ cmd/
        root.go
        sub.go
      main.go

所有的命令行配置分散写在各个文件中, 例如 root.go

package cmd

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
)

func init() {
  rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
}
var rootCmd = &cobra.Command{
  Use:   "hugo",
  Short: "Hugo is a very fast static site generator",
  Long: `A Fast and Flexible Static Site Generator built with
                love by spf13 and friends in Go.
                Complete documentation is available at http://hugo.spf13.com`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

sub.go

package cmd

import (
  "fmt"

  "github.com/spf13/cobra"
)

func init() {
  rootCmd.AddCommand(subCmd)
}

var subCmd = &cobra.Command{
  Use:   "sub command",
  Short: "short description",
  Long:  `long description`,
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("sub command")
  },
}

在最外面的main.go里,只用写一句话。

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

用cobra的架构来搭建命令行工具会使架构更清晰。

viper

https://github.com/spf13/viper
viper使用来专门处理配置文件的工具, 因为作者和cobra的作者是同一个人, 所以经常和cobra一起配合着使用。就连cobra的官方说明里也
提到了viper。

viper.SetConfigName("config") // name of config file (without extension)
viper.AddConfigPath("/etc/appname/")   // path to look for the config file in
viper.AddConfigPath("$HOME/.appname")  // call multiple times to add many search paths
viper.AddConfigPath(".")               // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
    panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

获取读取到的参数, 为map[string]interface{}类型。

c := viper.AllSettings()

viper也提供处理flag的功能,但是个人感觉没有上面两个库好用,这里也就不做介绍了。

kiper

往往我们要同时处理命令行参数和配置文件, 并且我们想合并这两种参数。

虽然可以用cobra+viper可以实现, 但是个人喜欢kingpin, 因为kingpin可以检查参数的正确性(通过实现kingpin.Value接口的数据类型)。

于是自己写了一个kingpin+viper的wrapper工具, kiper。
https://github.com/leeif/kiper

主要特点:

  • 通过tag配置flag设定(kingpin)
  • 通过viper读取配置文件
  • 自动合并flag和配置文件参数

具体用法

package main

import (
    "errors"
    "fmt"
    "os"
    "strconv"

    "github.com/leeif/kiper"
)

type Server struct {
    Address *Address `kiper_value:"name:address"`
    Port    *Port    `kiper_value:"name:port"`
}

type Address struct {
    s string
}

func (address *Address) Set(s string) error {
    if s == "" {
        return errors.New("address can't be empty")
    }
    address.s = s
    return nil
}

func (address *Address) String() string {
    return address.s
}

type Port struct {
    p string
}

func (port *Port) Set(p string) error {
    if _, err := strconv.Atoi(p); err != nil {
        return errors.New("not a valid port value")
    }
    port.p = p
    return nil
}

func (port *Port) String() string {
    return port.p
}

type Config struct {
    ID     *int   `kiper_value:"name:id;required;default:1"`
    Server Server `kiper_config:"name:server"`
}

func main() {
    // initialize config struct
    c := &Config{
        Server: Server{
            Address: &Address{},
            Port:    &Port{},
        },
    }

    // new kiper
    k := kiper.NewKiper("example", "example of kiper")
    k.SetConfigFileFlag("config", "config file", "./config.json")
    k.Kingpin.HelpFlag.Short('h')

    // parse command line and config file
    if err := k.Parse(c, os.Args[1:]); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    fmt.Println(c.Server.Port)
    fmt.Println(*c.ID)
}

配置文件需要和Config结构体保持一致。

config.json

{
    "server": {
        "address": "192.0.0.1",
        "port": "8080"
    },
    "id": 2
}

有待改善的地方

  • 现在还没有做sub command的功能。
  • 合并的时候配置文件总会覆盖命令行参数(合并的优先顺序)

总结

Go社区给开发着提供了多种处理命令行参数和配置文件的工具。每种工具都有各自的特点和应用场景。 例如flag是原生支持,扩展性高。kingpin可以检查参数的正确性。cobra适合构建复杂的命令行工具。开发者可以根据自己搭需求选择使用的工具, 这样的可选性和自由度也正是Go社区最大的魅力。