golang urfave/cli包

本文是阅读urfave/cli包官方文档的一些笔记,原文在这里!!

介绍

cli是一个简单、快速、有趣的包,用于在Go中构建命令行应用程序。其目标是使开发人员能够以表达的方式编写快速且可分发的命令行应用程序。

Getting Started

使用非常简单,理论上创建一个cli.App结构的对象,然后调用其Run()方法,传入命令行的参数即可。一个空白的cli应用程序如下:

package main
import (
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  (&cli.App{}).Run(os.Args)
}

但是这个空白程序没有什么用处,只会输出一些帮助信息

NAME:
   main - A new cli application
USAGE:
   main [global options] command [command options] [arguments...]
COMMANDS:
   help, h  Shows a list of commands or help for one command
GLOBAL OPTIONS:
   --help, -h  show help (default: false)

在上面空白程序基础上加上执行特定操作的动作

package main
import (
  "fmt"
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Name: "boom",
    Usage: "make an explosive entrance",
    Action: func(c *cli.Context) error {
      fmt.Println("boom! I say!")
      return nil
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

运行上面的程序会执行Action定义的函数。上面的代码中设置了Name/Usage/ActionNameUsage都显示在帮助中,Action是调用该命令行程序时实际执行的函数,需要的信息可以从参数cli.Context获取。

参数

通过cli.Context的相关方法我们可以获取传给命令行的参数信息:

  • NArg():返回参数个数;
  • Args():返回cli.Args对象,调用其Get(i)获取位置i上的参数。
package main
import (
  "fmt"
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Action: func(c *cli.Context) error {
      fmt.Printf("Hello %q\n",context.Args().Get(0))
	  fmt.Printf("The number of arguments:%d\n",context.NArg())
      return nil
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
} 

选项(Flags)

一个好用的命令行程序怎么会少了选项呢?cli设置和获取选项非常简单。在cli.App{}结构初始化时,设置字段Flags即可添加选项。Flags字段是[]cli.Flag类型,cli.Flag实际上是接口类型。cli为常见类型都实现了对应的XxxFlag,如BoolFlag/DurationFlag/StringFlag等。它们有一些共用的字段,Name/Value/Usage(名称/默认值/释义)。看示例:

package ma in
import (
  "fmt"
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Flags: []cli.Flag {
      &cli.StringFlag{
        Name: "lang",
        Value: "english",
        Usage: "language for the greeting",
      },
    },
    Action: func(c *cli.Context) error {
      name := "Nefertiti"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }
      if c.String("lang") == "spanish" {
        fmt.Println("Hola", name)
      } else {
        fmt.Println("Hello", name)
      }
      return nil
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

上面是一个打招呼的命令行程序,可通过选项lang指定语言,默认为英语。设置选项为非english的值,使用西班牙语。如果有参数,使用第一个参数作为人名,否则使用Nefertiti。注意选项是通过c.Type(name)来获取的,Type为选项类型,name为选项名。编译、运行:

$ go build
# 默认调用
$ ./main
hello Nefertiti
# 设置非英语
$ ./main  --lang spanish
Hola Nefertiti
# 传入参数作为人名
$ ./mian  --lang spanish alan
Hola alan

除了通过c.Type(name)来获取选项的值,我们还可以将选项存到某个预先定义好的变量中。只需要设置Destination字段为变量的地址即可:

package main
import (
  "log"
  "os"
  "fmt"
  "github.com/urfave/cli/v2"
)
func main() {
  var language string
  app := &cli.App{
    Flags: []cli.Flag {
      &cli.StringFlag{
        Name:        "lang",
        Value:       "english",
        Usage:       "language for the greeting",
        Destination: &language,
      },
    },
    Action: func(c *cli.Context) error {
      name := "someone"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }
      if language == "spanish" {
        fmt.Println("Hola", name)
      } else {
        fmt.Println("Hello", name)
      }
      return nil
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

与上面的程序效果是一样的。

占位符

cli可以在Usage字段中为选项设置占位值,占位值通过反引号**`**包围。只有第一个生效,其他的维持不变。占位值有助于生成易于理解的帮助信息:

package main
import (
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "config",
        Aliases: []string{"c"},
        Usage:   "Load configuration from `FILE`",
      },
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

设置占位值之后,帮助信息中,该占位值会显示在对应的选项后面,对短选项也是有效的

--config FILE, -c FILE   Load configuration from FILE

别名

选项可以设置多个别名,设置对应选项的Aliases字段即可:

package main
import (
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Flags: []cli.Flag {
      &cli.StringFlag{
        Name:    "lang",
        Aliases: []string{"l"},
        Value:   "english",
        Usage:   "language for the greeting",
      },
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

顺序

应用程序和命令的选项一般按其定义的顺序显示,但是,可以使用FlagsByNameCommandsByName对其进行排序。

package main
import (
  "log"
  "os"
  "sort"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Flags: []cli.Flag{
        &cli.StringFlag{
            Name:    "lang",
            Aliases: []string{"l"},
            Value:   "english",
            Usage:   "Language for the greeting",
        },
        &cli.StringFlag{
            Name:    "config",
            Aliases: []string{"c"},
            Usage:   "Load configuration from `FILE`",
        },
    },
    Commands: []*cli.Command{
      {
        Name:    "complete",
        Aliases: []string{"c"},
        Usage:   "complete a task on the list",
        Action:  func(c *cli.Context) error {
          return nil
        },
      },
      {
        Name:    "add",
        Aliases: []string{"a"},
        Usage:   "add a task to the list",
        Action:  func(c *cli.Context) error {
          return nil
        },
      },
    },
  }
  sort.Sort(cli.FlagsByName(app.Flags))
  sort.Sort(cli.CommandsByName(app.Commands))
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

帮助文档输出将如下所示

--config FILE, -c FILE  Load configuration from FILE
--lang value, -l value  Language for the greeting (default: "english")

环境变量

除了通过执行程序时手动指定命令行选项,我们还可以读取指定的环境变量作为选项的值。只需要将环境变量的名字设置到选项对象的EnvVars字段即可。可以指定多个环境变量名字,cli会依次查找,第一个有值的环境变量会被使用。

package main
import (
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "lang",
        Aliases: []string{"l"},
        Value:   "english",
        Usage:   "language for the greeting",
        EnvVars: []string{"LEGACY_COMPAT_LANG", "APP_LANG", "LANG"},
      },
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

编译、运行

$ go buildmain.go
$ APP_LANG=spanish  ./main
Hola

文件

cli还支持从文件中读取选项的值,设置选项对象的FilePath字段为文件路径

package main
import (
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := cli.NewApp()
  app.Flags = []cli.Flag {
    &cli.StringFlag{
      Name: "password",
      Aliases: []string{"p"},
      Usage: "password for the mysql database",
      FilePath: "/etc/mysql/password",
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

cli还支持从YAML/JSON/TOML等配置文件中读取选项值。

必要选项

如果将选项的Required字段设置为true,那么该选项就是必要选项。必要选项必须指定,否则会报错

package main
import (
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := cli.NewApp()
  app.Flags = []cli.Flag {
    &cli.StringFlag{
      Name: "lang",
      Value: "english",
      Usage: "language for the greeting",
      Required: true,
    },
  }
  app.Action = func(c *cli.Context) error {
    var output string
    if c.String("lang") == "spanish" {
      output = "Hola"
    } else {
      output = "Hello"
    }
    fmt.Println(output)
    return nil
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

帮助文档的默认值

默认情况下,帮助文本中选项的默认值显示为Value字段值。有些时候,Value并不是实际的默认值。这时,我们可以通过DefaultText设置

package main
import (
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.IntFlag{
        Name:    "port",
        Usage:   "Use a randomized port",
        Value: 0,
        DefaultText: "random",
      },
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

在帮助文档中将看到下面信息

--port value  Use a randomized port (default: random)

优先级

上面介绍了几种设置选项值的方式,如果同时有多个方式生效,按照下面的优先级从高到低设置:

  • 用户指定的命令行选项值;
  • 环境变量;
  • 配置文件;
  • 选项的默认值。

子命令

子命令使命令行程序有更好的组织性。git有大量的命令,很多以某个命令下的子命令存在。例如git remote命令下有add/rename/remove等子命令,git submodule下有add/status/init/update等子命令。

cli通过设置cli.AppCommands字段添加命令,设置各个命令的SubCommands字段,即可添加子命令。非常方便!

package main
import (
  "fmt"
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name:    "add",
        Aliases: []string{"a"},
        Usage:   "add a task to the list",
        Action:  func(c *cli.Context) error {
          fmt.Println("added task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:    "complete",
        Aliases: []string{"c"},
        Usage:   "complete a task on the list",
        Action:  func(c *cli.Context) error {
          fmt.Println("completed task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:        "template",
        Aliases:     []string{"t"},
        Usage:       "options for task templates",
        Subcommands: []*cli.Command{
          {
            Name:  "add",
            Usage: "add a new template",
            Action: func(c *cli.Context) error {
              fmt.Println("new task template: ", c.Args().First())
              return nil
            },
          },
          {
            Name:  "remove",
            Usage: "remove an existing template",
            Action: func(c *cli.Context) error {
              fmt.Println("removed task template: ", c.Args().First())
              return nil
            },
          },
        },
      },
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

上面定义了 3 个命令add/complete/templatetemplate命令定义了 2 个子命令add/remove。注意一点,子命令默认不显示在帮助信息中,需要显式调用子命令所属命令的帮助,例如

./main template --help

分类

在子命令数量很多的时候,可以设置Category字段为它们分类,在帮助信息中会将相同分类的命令放在一起展示

package main
import (
  "log"
  "os"
  "github.com/urfave/cli/v2"
)
func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name: "noop",
      },
      {
        Name:     "add",
        Category: "template",
      },
      {
        Name:     "remove",
        Category: "template",
      },
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

版权声明:本文为gbh18682030862原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。