2022.11.22 19:44:01
很多用Go写的命令行程序都用了urfave/cli这个库,包括geth,有必要简单了解一下。
用C写过命令行程序的人应该都不陌生,我们需要根据argc/argv一个个地解析命令行参数,调用不同的函数,最后还要写一个usage()函数用于打印帮助信息。urfave/cli把这个过程做了一下封装,抽象出flag/command/subcommand这些模块,用户只需要提供一些模块的配置,参数的解析和关联在库内部完成,帮助信息也可以自动生成。
举个例子,我们想要实现下面这个命令行程序:
NAME: GoTest - hello world
USAGE: GoTest [global options] command [command options] [arguments...]
VERSION: 1.2.3
COMMANDS: help, h Shows a list of commands or help for one command
arithmetic:
add, a calc 1+1
sub, s calc 5-3
database:
db database operations
GLOBAL OPTIONS:
--lang FILE, -l FILE read from FILE (default: "english")
--port value, -p value listening port (default: 8000)
--help, -h Help!Help!
--print-version, -v print version
当然,为了让我们的程序干点事情,可以指定一下入口函数app.Action,具体写法如下:
import (
"fmt"
"gopkg.in/urfave/cli.v1"
)
func main() {
:= cli.NewApp()
app .Action = func(c *cli.Context) error {
app.Println("BOOM!")
fmtreturn nil
}
:= app.Run(os.Args)
err if err != nil {
.Fatal(err)
log}
}
.Name = "GoTest"
app.Usage = "hello world"
app.Version = "1.2.3" app
--lang FILE, -l FILE read from FILE (default: "english")
--port value, -p value listening port (default: 8000)
对应代码:
var language string
.Flags = []cli.Flag {
app.IntFlag {
cli: "port, p",
Name: 8000,
Value: "listening port",
Usage},
.StringFlag {
cli: "lang, l",
Name: "english",
Value: "read from `FILE`",
Usage: &language,
Destination},
}
可以看到,每一个flag都对应一个cli.Flag接口的实例。
Name字段中逗号后面的字符表示flag的简写,也就是说”–port”和”-p”是等价的。
Value字段可以指定flag的默认值。
Usage字段是flag的描述信息。
Destination字段可以为该flag指定一个接收者,比如上面的language变量。解析完”–lang”这个flag后会自动存储到这个变量里,后面的代码就可以直接使用这个变量的值了。
另外,如果你想给用户增加一些属性值类型的提示,可以通过占位符(placeholder)来实现,比如上面的”–lang FILE”。占位符通过``符号来标识。
我们可以在app.Action中测试一下打印这些flag的值:
.Action = func(c *cli.Context) error {
app.Println("BOOM!")
fmt.Println(c.String("lang"), c.Int("port"))
fmt.Println(language)
fmtreturn nil
}
另外,正常来说帮助信息里的flag是按照代码里的声明顺序排列的,如果你想让它们按照字典序排列的话,可以借助于sort:
import "sort"
.Sort(cli.FlagsByName(app.Flags)) sort
最后,help和version这两个flag有默认实现,也可以自己改:
.HelpFlag = cli.BoolFlag {
cli: "help, h",
Name: "Help!Help!",
Usage}
.VersionFlag = cli.BoolFlag {
cli: "print-version, v",
Name: "print version",
Usage}
另外每个command可能还有subcommand,也就必须要通过添加两个命令行参数才能完成相应的操作。比如我们的db命令包含2个子命令,如果输入GoTest db -h会显示下面的信息:
NAME: GoTest db - database operations
USAGE: GoTest db command [command options] [arguments...]
COMMANDS: insert insert data
delete delete data
OPTIONS: --help, -h Help!Help!
每个command都对应于一个cli.Command接口的实例,入口函数通过Action指定。如果你想像在帮助信息里实现分组显示,可以为每个command指定一个Category。具体代码如下:
.Commands = []cli.Command {
app{
: "add",
Name: []string{"a"},
Aliases: "calc 1+1",
Usage: "arithmetic",
Category: func(c *cli.Context) error {
Action.Println("1 + 1 = ", 1 + 1)
fmtreturn nil
},
},
{
: "sub",
Name: []string{"s"},
Aliases: "calc 5-3",
Usage: "arithmetic",
Category: func(c *cli.Context) error {
Action.Println("5 - 3 = ", 5 - 3)
fmtreturn nil
},
},
{
: "db",
Name: "database operations",
Usage: "database",
Category: []cli.Command {
Subcommands{
: "insert",
Name: "insert data",
Usage: func(c *cli.Context) error {
Action.Println("insert subcommand")
fmtreturn nil
},
},
{
: "delete",
Name: "delete data",
Usage: func(c *cli.Context) error {
Action.Println("delete subcommand")
fmtreturn nil
},
},
},
},
}
如果你想在command执行前后执行后完成一些操作,可以指定app.Before/app.After这两个字段:
.Before = func(c *cli.Context) error {
app.Println("app Before")
fmtreturn nil
}
.After = func(c *cli.Context) error {
app.Println("app After")
fmtreturn nil
}
具体测试一下:
$ GoTest add
$ GoTest db insert
附完整demo代码:
package cli
import (
"fmt"
"log"
"os"
"sort"
"gopkg.in/urfave/cli.v1"
)
func Run() {
var language string
:= cli.NewApp()
app .Name = "GoTest"
app.Usage = "hello world"
app.Version = "1.2.3"
app.Flags = []cli.Flag{
app.IntFlag{
cli: "port, p",
Name: 8000,
Value: "listening port",
Usage},
.StringFlag{
cli: "lang, l",
Name: "english",
Value: "read from `FILE`",
Usage: &language,
Destination},
}
.Commands = []cli.Command{
app{
: "add",
Name: []string{"a"},
Aliases: "calc 1+1",
Usage: "arithmetic",
Category: func(c *cli.Context) error {
Action.Println("1 + 1 = ", 1+1)
fmtreturn nil
},
},
{
: "sub",
Name: []string{"s"},
Aliases: "calc 5-3",
Usage: "arithmetic",
Category: func(c *cli.Context) error {
Action.Println("5 - 3 = ", 5-3)
fmtreturn nil
},
},
{
: "db",
Name: "database operations",
Usage: "database",
Category: []cli.Command{
Subcommands{
: "insert",
Name: "insert data",
Usage: func(c *cli.Context) error {
Action.Println("insert subcommand")
fmtreturn nil
},
},
{
: "delete",
Name: "delete data",
Usage: func(c *cli.Context) error {
Action.Println("delete subcommand")
fmtreturn nil
},
},
},
},
}
.Action = func(c *cli.Context) error {
app.Println("BOOM!")
fmt.Println(c.String("lang"), c.Int("port"))
fmt.Println(language)
fmtif c.Int("port") == 8000 {
return cli.NewExitError("invalid port", 88)
}
return nil
}
.Before = func(c *cli.Context) error {
app.Println("app Before")
fmtreturn nil
}
.After = func(c *cli.Context) error {
app.Println("app After")
fmtreturn nil
}
.Sort(cli.FlagsByName(app.Flags))
sort.HelpFlag = cli.BoolFlag{
cli: "help, h",
Name: "Help!Help!",
Usage}
.VersionFlag = cli.BoolFlag{
cli: "print-version, v",
Name: "print version",
Usage}
:= app.Run(os.Args)
err if err != nil {
.Fatal(err)
log}
}