以太坊命令行库 urfave/cli
使用 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
1. 基本结构
导入包以后,通过 cli.NewApp() 创建一个实例,然后调用 Run() 方法就实现了一个最基本的命令行程序了。当然,为了让我们的程序干点事情,可以指定一下入口函数app.Action,具体写法如下:
import ( "fmt" "gopkg.in/urfave/cli.v1" ) func main() { app := cli.NewApp() app.Action = func(c *cli.Context) error { fmt.Println("BOOM!") return nil } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
2. 公共配置
帮助里需要显示的一些基本信息:
app.Name = "GoTest" app.Usage = "hello world" app.Version = "1.2.3"
3. Flag配置
具体对应于帮助中的以下信息:
--lang FILE, -l FILE read from FILE (default: "english") --port value, -p value listening port (default: 8000)
对应代码:
var language string app.Flags = []cli.Flag { cli.IntFlag { Name: "port, p", Value: 8000, Usage: "listening port", }, cli.StringFlag { Name: "lang, l", Value: "english", Usage: "read from `FILE`", Destination: &language, }, }
可以看到,每一个flag都对应一个cli.Flag接口的实例。
- Name:逗号后面的字符表示flag的简写,也就是说"--port"和"-p"是等价的。
- Value:可以指定flag的默认值。
- Usage:flag的描述信息。
- Destination:可以为该flag指定一个接收者,比如上面的language变量。解析完"--lang"这个flag后会自动存储到这个变量里,后面的代码就可以直接使用这个变量的值了。
另外,如果你想给用户增加一些属性值类型的提示,可以通过占位符(placeholder)来实现,比如上面的"--lang FILE"。占位符通过``符号来标识。
我们可以在app.Action中测试一下打印这些flag的值:
app.Action = func(c *cli.Context) error { fmt.Println("BOOM!") fmt.Println(c.String("lang"), c.Int("port")) fmt.Println(language) return nil }
另外,正常来说帮助信息里的flag是按照代码里的声明顺序排列的,如果你想让它们按照字典序排列的话,可以借助于sort:
import "sort" sort.Sort(cli.FlagsByName(app.Flags))
最后,help和version这两个flag有默认实现,也可以自己改:
cli.HelpFlag = cli.BoolFlag { Name: "help, h", Usage: "Help!Help!", } cli.VersionFlag = cli.BoolFlag { Name: "print-version, v", Usage: "print version", }
4. Command配置
命令行程序除了有flag,还有command(比如git log, git commit等等)。
另外,每个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。具体代码如下:
app.Commands = []cli.Command { { Name: "add", Aliases: []string{"a"}, Usage: "calc 1+1", Category: "arithmetic", Action: func(c *cli.Context) error { fmt.Println("1 + 1 = ", 1 + 1) return nil }, }, { Name: "sub", Aliases: []string{"s"}, Usage: "calc 5-3", Category: "arithmetic", Action: func(c *cli.Context) error { fmt.Println("5 - 3 = ", 5 - 3) return nil }, }, { Name: "db", Usage: "database operations", Category: "database", Subcommands: []cli.Command { { Name: "insert", Usage: "insert data", Action: func(c *cli.Context) error { fmt.Println("insert subcommand") return nil }, }, { Name: "delete", Usage: "delete data", Action: func(c *cli.Context) error { fmt.Println("delete subcommand") return nil }, }, }, }, }
如果你想在command执行前后执行后完成一些操作,可以指定app.Before/app.After这两个字段:
app.Before = func(c *cli.Context) error { fmt.Println("app Before") return nil } app.After = func(c *cli.Context) error { fmt.Println("app After") return nil }
测试一下:
$ GoTest add $ GoTest db insert
5. 小结
urfave/cli这个库还是很好用的,完成了很多 routine 的工作,程序员只需要专注于具体业务逻辑的实现。
完整 demo 代码:
package main import ( "fmt" "gopkg.in/urfave/cli.v1" "log" "os" ) func commandAction(ctx *cli.Context) error { fmt.Println("action name:", ctx.Command.Name) for _, v := range ctx.FlagNames() { fmt.Printf("action flag: --%v=%v\n", v, ctx.GlobalString(v)) } return nil } func testAction(ctx *cli.Context) error { fmt.Println("action name:", ctx.Command.Name) for _, v := range ctx.FlagNames() { fmt.Printf("action flag: --%v=%v\n", v, ctx.GlobalString(v)) } return nil } func main() { httpFlags := []cli.Flag{ cli.StringFlag{ Name: "http.api", Usage: "API's offered over the HTTP-RPC interface", Value: "http flag default value", }, } testFlags := []cli.Flag{ cli.StringFlag{ Name: "test.api", Usage: "API's offered over the TEST-RPC interface", Value: "test flag default value", }, } httpCommand := cli.Command{ Action: commandAction, Name: "http", Flags: httpFlags, } testCommand := cli.Command{ Action: testAction, Name: "test", Flags: testFlags, } //实例化一个命令行程序 app := cli.NewApp() app.Flags = append(httpFlags,testFlags...) app.Commands = []cli.Command{ httpCommand, testCommand, } //程序名称 app.Name = "GoTool" //程序的用途描述 app.Usage = "To show urfave/cli usages" //程序的版本号 app.Version = "1.0.0" //该程序执行的代码 app.Action = func(c *cli.Context) error { if args := c.Args(); len(args) > 0 { return fmt.Errorf("invalid command: %q", args[0]) } fmt.Println(c.Args()) return nil } //启动 if err := app.Run(os.Args); err != nil { log.Fatal(err) } }
下一章:以太坊源码分析 RPC
以太坊源码分析 RPC:本文主要分析以太坊 RPC 的完整流程,也就是 API 注册和 API 调用流程。以太坊 RPC 遵循JSON RPC规范,API列表参见以下链接:https://github.com/ethereum/w ...