侧边栏壁纸
博主头像
汪洋

即使慢,驰而不息,纵会落后,纵会失败,但一定可以达到他所向的目标。 - 鲁迅

  • 累计撰写 191 篇文章
  • 累计创建 74 个标签
  • 累计收到 112 条评论

Golang 库 - urfave/cli

汪洋
2022-04-08 / 0 评论 / 0 点赞 / 253 阅读 / 8,839 字

介绍

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)

如果希望修改 main - A new cli application 这部分输出可以

package main

import (
	"fmt"
	"os"

	"github.com/urfave/cli/v2"
)

func main() {
	app := &cli.App{
		Name:  "greet",
		Usage: "say a greeting",
		Action: func(c *cli.Context) error {
			fmt.Println("Greetings")
			return nil
		},
	}
	// 接受os.Args启动程序
	app.Run(os.Args)
}

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

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/Action。Name 和 Usage 都显示在帮助中,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", c.Args().Get(0))
			fmt.Printf("The number of arguments:%d\n", c.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 main

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 (
	"fmt"
	"log"
	"os"

	"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)
  }
}

顺序

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

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)
	}
}

环境变量

除了通过执行程序时手动指定命令行选项,我们还可以读取指定的环境变量作为选项的值。只需要将环境变量的名字设置到选项对象的 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 (
	"fmt"
	"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: "111aaaa",
			},
		},
	}
	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.App 的 Commands 字段添加命令,设置各个命令的 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/template,template命令定义了 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)
	}
}
0

评论区