以太坊命令行库 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 ...