乐趣区

Golang-聊一聊Go的那些处理命令行参数和配置文件的库

前言

最近应为一直在写 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()

完整代码
https://play.golang.org/p/mjgZ6SJMeAm

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

https://github.com/alecthomas…

一些主要的特点:

  • 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 社区最大的魅力。

退出移动版