# urfave cli

参考资料

urfave cli 官网:https://cli.urfave.org/

urfave cli github: https://github.com/urfave/cli

photoprism (cli 用法参考) : https://github.com/photoprism/photoprism

# 简介

cli 是一个用于构建命令行程序的库。

# 快速开始

安装 cli 库,有 v1v2 两个版本。如果没有特殊需求,一般安装 v2 版本:

$ go get -u github.com/urfave/cli/v2

使用

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

运行

# 运行
$go run main.go
# 编译后运行
$go build -o hello
$./hello

cli 会为我们额外生成了帮助信息:

$ ./hello --help
NAME:
   hello - hello world example
USAGE:
   hello [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)

# 帮助信息

var version = "v0.0.1 development"
var log = zerolog.New(os.Stderr).With().Timestamp().Logger()
const appName = "AliyunLogAnalysis"
const appAbout = "Twelveeee"
const appDescription = "阿里云日志分析"
const appCopyright = "(c) 2023 Twelveeee @ Twelveeee"
// Metadata contains build specific information.
var Metadata = map[string]interface{}{
	"Name":        appName,
	"About":       appAbout,
	"Description": appDescription,
	"Version":     version,
}
func main() {
	defer func() {
		if r := recover(); r != nil {
			log.Err(errors.New("hello"))
			os.Exit(1)
		}
	}()
	app := cli.NewApp()
	app.Usage = appAbout
	app.Description = appDescription
	app.Version = version
	app.Copyright = appCopyright
	app.EnableBashCompletion = true
	app.Flags = config.Flags.Cli()
	app.Commands = command.Commands
	app.Metadata = Metadata
	// app.UseShortOptionHandling = true
	zerolog.SetGlobalLevel(-1)
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
	log.Info().Msg("start app")
	if err := app.Run(os.Args); err != nil {
		log.Err(err).Msg("run error")
	}
}
$go run main.go
NAME:
   main - Twelveeee
USAGE:
   main [global options] command [command options] [arguments...]
VERSION:
   v0.0.1 development
DESCRIPTION:
   阿里云日志分析
COMMANDS:
   start, up      Starts the server
   startDay, upd  Starts the server with start day and end day
   help, h        Shows a list of commands or help for one command
GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version
COPYRIGHT:
   (c) 2023 Twelveeee @ Twelveeee

# 参数

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

  • NArg() :返回参数个数;
  • Args() :返回 cli.Args 对象,调用其 Get(i) 获取位置 i 上的参数。

参数是指

$go run main.go  fff --lang spanish
# 其中 fff --lang spanish 均为参数 都可以通过 Args ().Get (i) 进行获取

示例:

func main() {
  app := &cli.App{
    Name:  "arguments",
    Usage: "arguments example",
    Action: func(c *cli.Context) error {
      for i := 0; i < c.NArg(); i++ {
        fmt.Printf("%d: %s\n", i+1, c.Args().Get(i))
      }
      return nil
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
$go run main.go  fff --lang spanish
1: fff
2: --lang
3: spanish

# 选项

Flags 字段是 []cli.Flag 类型, cli.Flag 实际上是接口类型。 cli 为常见类型都实现了对应的 XxxFlag ,如 BoolFlag/DurationFlag/StringFlag 等。它们有一些共用的字段, Name/Value/Usage (名称 / 默认值 / 释义)。

示例:

func main() {
	app := &cli.App{
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:  "lang",
				Value: "english",
				Usage: "language for the greeting",
			},
		},
		Action: func(cCtx *cli.Context) error {
			name := "Nefertiti"
			log.Info().Msg(strconv.Itoa(cCtx.NArg()))
			if cCtx.NArg() > 0 {
				name = cCtx.Args().Get(0)
			}
			if cCtx.String("lang") == "spanish" {
				fmt.Println("Hola", name)
			} else {
				fmt.Println("Hello", name)
			}
			return nil
		},
	}
	if err := app.Run(os.Args); err != nil {
		log.Err(err).Msg("run error")
	}
}
# 参数在前 flag 在后,没有识别 flag
$go run main.go  fff --lang spanish
{"level":"info","time":"2023-11-12T06:31:34Z","message":"3"}
Hello fff
#flag 在前,,参数在后 正常识别
$go run main.go --lang spanish fff
{"level":"info","time":"2023-11-12T06:32:20Z","message":"1"}
Hola fff

# 获取选项的值

获取 flag 可以通过 ctx.Type(name) 也可以绑定 Destination 字段

func main() {
  var language string
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:        "lang",
        Value:       "english",
        Usage:       "language for the greeting",
          // 这的 Destination
        Destination: &language,
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }
      if language == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("你好", name)
      }
      return nil
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

# 占位符

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

func main() {
  app := & cli.App{
    Flags : []cli.Flag {
      &cli.StringFlag{
        Name:"config",
        Usage: "Load configuration from `FILE`",
      },
    },
  }
  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}
# GLOBAL OPTIONS 里 value 变成了 FILE
$go run main.go --help
NAME:
   placeholder - A new cli application
USAGE:
   placeholder [global options] command [command options] [arguments...]
COMMANDS:
   help, h  Shows a list of commands or help for one command
GLOBAL OPTIONS:
   --config    Load configuration from FILE
   --help, -h     show help (default: false)

# 别名

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

func main() {
    app := &cli.App{
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name:    "lang",
                Aliases: []string{"l"},
                Value:   "english",
                Usage:   "language for the greeting",
            },
        },
    }
    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }
}
# GLOBAL OPTIONS 里显示,可以使用 --lang 和 - l 去设置 flag
# 通过两个效果一样的名称指定同一个选项会报错
$go run main.go -h
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:
   --lang value, -l value  language for the greeting (default: "english")
   --help, -h              show help

# 环境变量

读取指定的环境变量作为选项的值。只需要将环境变量的名字设置到选项对象的 EnvVars 字段即可。可以指定多个环境变量名字, cli 会依次查找,第一个有值的环境变量会被使用。

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"},
            },
        },
    }
    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }
}

# 文件变量

可以从文件中获取变量,从文件中获取变量的优先级大于环境变量

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

支持从 YAML、JSON、TOML 中获取变量

官网:https://cli.urfave.org/v2/examples/flags/#values-from-alternate-input-sources-yaml-toml-and-others

参考资料:https://github.com/urfave/cli/issues/1250

这块查阅资料后发现有一系列的坑,官网以及国内的各个博客都没有做出很好的解释

test2: TTAA
test: 1234
key: 
    child-key: value
    child-key2: value2
func main() {
	flags := []cli.Flag{
		altsrc.NewIntFlag(&cli.IntFlag{Name: "test"}),
		altsrc.NewStringFlag(&cli.StringFlag{Name: "test2"}),
		altsrc.NewStringFlag(&cli.StringFlag{Name: "child-key2", Aliases: []string{"key.child-key2"}}),
		&cli.StringFlag{Name: "load", Value: "yaml.yaml"},
	}
	app := &cli.App{
		Action: func(ctx *cli.Context) error {
			fmt.Println("--test value.*default: 0")
			fmt.Println("test2:", ctx.String("test2"))
			fmt.Println("test:", ctx.Int("test"))
			fmt.Println("child-key2:", ctx.String("key.child-key2"))
			fmt.Println("child-key2:", ctx.String("child-key2"))
			return nil
		},
		Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")),
		Flags:  flags,
	}
	err := app.Run(os.Args)
	if err != nil {
		log.Err(err).Msg("run error")
	}
}
$ go run main.go
--test value.*default: 0
test2: TTAA
test: 1234
child-key2: value2
child-key2: value2

如果要读取 yaml 里的变量,需要定义一个 altsrc.NewStringFlag 生成的 flag,里面包含一个 FlagSet

如果 yaml 变量是层级关系,则可以使用点进行间隔,如 key.child-key2 , 也可以通过设置别名,在 Action 中进行获取。

Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")),

这个函数的主要目的是 在程序开始前,初始化 flags 参数

NewYamlSourceFromFlagFunc 中的 load 其实也是 flag 的值,支持通过命令行进行初始化。demo 中使用的是默认的值

# 必要选项

flag 里加 Required

func main() {
    app := &cli.App{
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name:     "lang",
                Value:    "english",
                Usage:    "language for the greeting",
                Required: true,
            },
        },
        Action: func(cCtx *cli.Context) error {
            output := "Hello"
            if cCtx.String("lang") == "spanish" {
                output = "Hola"
            }
            fmt.Println(output)
            return nil
        },
    }
    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }
}

# 帮助文本中的默认值

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

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

上面代码逻辑中,如果 Value 设置为 0 就随机一个端口,这时帮助信息中 default: 0 就容易产生误解了。通过 DefaultText 可以避免这种情况:

$go run main.go --help
NAME:
   default-text - A new cli application
USAGE:
   default-text [global options] command [command options] [arguments...]
COMMANDS:
   help, h  Shows a list of commands or help for one command
GLOBAL OPTIONS:
   --port value  Use a randomized port (default: random)
   --help, -h    show help (default: false)

# 子命令

类似于 git ,git 里就有大量的命令,很多以某个命令下的子命令存在。例如 git remote 命令下有 add/rename/remove 等子命令, git submodule 下有 add/status/init/update 等子命令。

func main() {
    app := &cli.App{
        Commands: []*cli.Command{
            {
                Name:    "add",
                Aliases: []string{"a"},
                Usage:   "add a task to the list",
                Action: func(cCtx *cli.Context) error {
                    fmt.Println("added task: ", cCtx.Args().First())
                    return nil
                },
            },
            {
                Name:    "complete",
                Aliases: []string{"c"},
                Usage:   "complete a task on the list",
                Action: func(cCtx *cli.Context) error {
                    fmt.Println("completed task: ", cCtx.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(cCtx *cli.Context) error {
                            fmt.Println("new task template: ", cCtx.Args().First())
                            return nil
                        },
                    },
                    {
                        Name:  "remove",
                        Usage: "remove an existing template",
                        Action: func(cCtx *cli.Context) error {
                            fmt.Println("removed task template: ", cCtx.Args().First())
                            return nil
                        },
                    },
                },
            },
        },
    }
    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }
}
$go run main.go -h
NAME:
   main - A new cli application
USAGE:
   main [global options] command [command options] [arguments...]
COMMANDS:
   add, a       add a task to the list
   complete, c  complete a task on the list
   template, t  options for task templates
   help, h      Shows a list of commands or help for one command
GLOBAL OPTIONS:
   --help, -h  show help

子命令默认不显示在帮助信息中,需要显式调用子命令所属命令的帮助

$ go run main.go add -h
NAME:
   main add - add a task to the list
USAGE:
   main add [command options] [arguments...]
OPTIONS:
   --help, -h  show help

# 分类

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

func main() {
	app := &cli.App{
		Commands: []*cli.Command{
			{
				Name: "noop",
			},
			{
				Name:     "add",
				Category: "template",
				Usage:    "Usage for add",
			},
			{
				Name:     "remove",
				Category: "template",
				Usage:    "Usage for remove",
			},
		},
	}
	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}
$ go run main.go -h
NAME:
   main - A new cli application
USAGE:
   main [global options] command [command options] [arguments...]
COMMANDS:
   noop     
   help, h  Shows a list of commands or help for one command
   template:
     add     Usage for add
     remove  Usage for remove
GLOBAL OPTIONS:
   --help, -h  show help

# 自定义信息

自定义 help 信息 包括版本 appName 等等

var version = "v0.0.1 development"
const appName = "AliyunLogAnalysis"
const appAbout = "Twelveeee"
const appDescription = "日志分析"
const appCopyright = "(c) 2023 Twelveeee @ Twelveeee"
// Metadata contains build specific information.
var Metadata = map[string]interface{}{
	"Name":        appName,
	"About":       appAbout,
	"Description": appDescription,
	"Version":     version,
}
func main() {
	defer func() {
		if r := recover(); r != nil {
			os.Exit(1)
		}
	}()
	app := cli.NewApp()
	app.Usage = appAbout
	app.Description = appDescription
	app.Version = version
	app.Copyright = appCopyright
	app.EnableBashCompletion = true
	app.Flags = config.Flags.Cli()
	app.Commands = command.Commands
	app.Metadata = Metadata
	// app.UseShortOptionHandling = true
	log.Info().Msg("start app")
	if err := app.Run(os.Args); err != nil {
		log.Err(err).Msg("run error")
	}
}
$ go run main.go 
NAME:
   main - Twelveeee
USAGE:
   main [global options] command [command options] [arguments...]
VERSION:
   v0.0.1 development
DESCRIPTION:
   日志分析
COMMANDS:
   start, up      Starts the server
   startDay, upd  Starts the server with start day and end day
   help, h        Shows a list of commands or help for one command
GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version
COPYRIGHT:
   (c) 2023 Twelveeee @ Twelveeee
更新于
-->