在 Go 里面如何使用 Flag 包 #
简介 #
命令行工具很少在没有额外配置的情况下开箱即用。好的默认值固然很重要,但有用的工具需要接受用户的配置。在大多数平台上,命令行工具通过接收标志来指定命令的执行。标志是以键值分隔的字符串,加在命令的名称后面。Go 让你通过使用标准库中的 flag 包来制作接受标志的命令行工具。
在本教程中,你将探索使用 flag 包来建立不同种类的命令行工具的各种方法。你将使用一个标志来控制程序输出,引入位置参数,在这里你将混合标志和其他数据,然后实现子命令。
用 Flag 来改变程序的行为 #
使用 flag 包包括三个步骤:首先,定义变量以捕获标志值,然后定义你的 Go 应用程序将使用的标志,最后解析执行时提供给应用程序的标志。flag包内的大多数函数都与定义标志和将它们与你定义的变量绑定有关。解析阶段由Parse()函数处理。
为了阐述这一点,你将创建一个程序,定义一个 Boolean标志,改变这个标志将会把信息打印到标准输出上。如果提供一个-color标志,程序会用蓝色来打印消息。如果没有这个标志,则打印消息不会有颜色。
创建一个叫boolean.go的文件:
nano boolean.go
添加如下代码到文件里面来创建程序:
package main
import (
"flag"
"fmt"
)
type Color string
const (
ColorBlack Color = "\u001b[30m"
ColorRed = "\u001b[31m"
ColorGreen = "\u001b[32m"
ColorYellow = "\u001b[33m"
ColorBlue = "\u001b[34m"
ColorReset = "\u001b[0m"
)
func colorize(color Color, message string) {
fmt.Println(string(color), message, string(ColorReset))
}
func main() {
useColor := flag.Bool("color", false, "display colorized output")
flag.Parse()
if *useColor {
colorize(ColorBlue, "Hello, DigitalOcean!")
return
}
fmt.Println("Hello, DigitalOcean!")
}
这个例子使用ANSI 逃逸序列来指示终端显示彩色输出。这些是专门的 character 序列,所以为它们定义一个新的类型是有意义的(L8)。在这个例子中,我们称该类型为color,并将该类型定义为string。然后我们定义了一个调色板,在后面的 const 块中使用。定义在const块之后的colorize函数接受Color常量其中之一和一个string,用于对信息进行着色。然后它指示终端改变颜色,首先打印所要求的颜色的转义序列,然后打印信息,最后要求终端通过打印特殊的颜色重置序列来重置其颜色。
在main中,我们使用flag.Bool函数来定义一个名为color的 Boolean 标志。这个函数的第二个参数,false,在没有提供这个标志的情况下,设置这个标志的默认值。与你可能有的期望相反,将其设置为true并不会颠倒行为,如提供一个标志会导致它变成 false。因此,这个参数的值在布尔标志下几乎总是false。
最后一个参数是一个可以作为使用信息打印出来的文档 string。从这个函数返回的值是一个指向bool的指针。下一行的flag.Parse函数使用这个指针,然后根据用户传入的标志,设置bool变量。 然后我们就可以通过取消引用这个指针来检查这个bool指针的值。更多关于指针变量的信息可以在指针教程找到。使用这个 Boolean,我们就可以在设置-color标志时调用colorize,而在没有这个标志时调用fmt.Println变量。
保存文件,并在未传入没有任何标志的情况下运行该程序:
go run boolean.go
你将会看到如下输出:
Output
Hello, DigitalOcean!
现在带上-color标志再跑一遍程序:
go run boolean.go -color
输出文本会是一样的,只不过这时候颜色时蓝色的。
标志不是传递给命令的唯一参数。你也能发送文件名或其他数据。
使用位置参数 #
通常情况下,命令会接受一些参数,这些参数作为命令的重点对象。例如,打印文件第一行的head命令经常被以head example.txt调用。文件example.txt是调用head命令时的一个位置参数。
Parse()函数将一直解析它所遇到的标志,直到它检测到一个非标志参数。flag包通过Args()和Arg()函数使这些参数可用。
为了阐述这一点,你将重新实现一个简化的head命令,它显示一个给定文件的前几行:
创建一个新的文件称为head.go,然后添加如下代码:
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
)
func main() {
var count int
flag.IntVar(&count, "n", 5, "number of lines to read from the file")
flag.Parse()
var in io.Reader
if filename := flag.Arg(0); filename != "" {
f, err := os.Open(filename)
if err != nil {
fmt.Println("error opening file: err:", err)
os.Exit(1)
}
defer f.Close()
in = f
} else {
in = os.Stdin
}
buf := bufio.NewScanner(in)
for i := 0; i < count; i++ {
if !buf.Scan() {
break
}
fmt.Println(buf.Text())
}
if err := buf.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error reading: err:", err)
}
}
首先,我们定义了一个count变量,用来保存程序应该从文件中读取的行数。然后,我们使用flag.IntVar定义-n标志,模拟原始head程序的行为。 这个函数允许我们将自己的pointer传递给一个变量,与没有Var后缀的标志函数相反。除了这个区别之外,flag.IntVar的其他参数与flag.Int对应的参数相同:标志名称、默认值和描述。 和前面的例子一样,我们随后调用flag.Parse()来处理用户的输入。
下一节读取文件。我们首先定义一个io.Reader变量,该变量将被设置为用户请求的文件,或传递给程序的标准输入。在if语句中,我们使用flag.Arg函数来访问所有标志之后的第一个位置参数。如果用户提供了文件名,这个位置参数会被设置。否则,它将为空 string("")。当文件名提供时,我们使用os.Open函数来打开该文件,并将我们之前定义的io.Reader设置为该文件。否则,我们使用os.stdin来读取标准输入。
最后一节使用一个用bufio.NewScanner创建的*bufio.Scanner从io.Reader变量in中读取行数据。我们使用forloop遍历到 count 的值,如果用buf.Scan扫描该行结果为false,则调用break,表示行数少于用户要求的数量。
运行这个程序,用head.go作为文件参数,显示你刚才写的文件的内容:
go run head.go -- head.go
--分隔符是一个被flag包识别的特殊标志,它表示后面没有更多的 flag 参数。当你运行这个命令时,你会收到以下输出:
Output
package main
import (
"bufio"
"flag"
使用你定义的-n标志来调整输出的数量:
go run head.go -n 1 head.go
这只输出包的声明:
Output
package main
最后,当程序检测到没有提供位置参数时,它从标准输入中读取输入,就像head一样。试着运行这个命令:
echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3
你将会看到如下输出:
Output
fish
lobsters
sharks
到目前为止,你所看到的flag函数的行为仅限于检查整个命令的调用。你并不总是想要这种行为,特别是当你在编写一个支持子命令的命令行工具时。
用 FlagSet 来实现子命令 #
现代的命令行应用程序经常实现 “子命令”,将一套工具捆绑在一个命令之下。使用这种模式的最著名的工具是git。 当检查像git init这样的命令时,git是命令,init是 git 的子命令。子命令的一个显著特点是,每个子命令可以有自己的标志集合。
Go 应用程序可以使用flag.(*FlagSet)类型支持具有自己的标志集的子命令。为了阐述这一点,创建一个程序,使用两个具有不同标志的子命令来实现一个命令。
创建一个名为subcommand.go的新文件,并在该文件中添加以下内容:
package main
import (
"errors"
"flag"
"fmt"
"os"
)
func NewGreetCommand() *GreetCommand {
gc := &GreetCommand{
fs: flag.NewFlagSet("greet", flag.ContinueOnError),
}
gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted")
return gc
}
type GreetCommand struct {
fs *flag.FlagSet
name string
}
func (g *GreetCommand) Name() string {
return g.fs.Name()
}
func (g *GreetCommand) Init(args []string) error {
return g.fs.Parse(args)
}
func (g *GreetCommand) Run() error {
fmt.Println("Hello", g.name, "!")
return nil
}
type Runner interface {
Init([]string) error
Run() error
Name() string
}
func root(args []string) error {
if len(args) < 1 {
return errors.New("You must pass a sub-command")
}
cmds := []Runner{
NewGreetCommand(),
}
subcommand := os.Args[1]
for _, cmd := range cmds {
if cmd.Name() == subcommand {
cmd.Init(os.Args[2:])
return cmd.Run()
}
}
return fmt.Errorf("Unknown subcommand: %s", subcommand)
}
func main() {
if err := root(os.Args[1:]); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
这个程序分为几个部分:main函数,root函数,以及实现子命令的各个函数。main函数处理从命令返回的错误。如果任何函数返回错误,if语句将捕捉到它,打印出错误,程序将以1的状态码退出,向操作系统的其他部分表明发生了错误。在main中,我们将程序被调用的所有参数传递给root。我们通过先将os.Args切片来删除第一个参数,也就是程序的名称(在前面的例子中是./subcommand)。
root函数定义了[]Runner,所有的子命令都会在这里定义。Runner是一个子命令的 interface ,允许root使用Name()获取子命令的名称,并将其与变量subcommand内容进行比较。一旦在遍历cmds变量后找到了正确的子命令,我们就用其余的参数初始化子命令,并调用该命令的Run()方法。
我们只定义了一个子命令,尽管这个框架很容易让我们创建其他子命令。GreetCommand是使用NewGreetCommand实例化的,在这里我们使用flag.NewFlagSet创建一个新的*flag.FlagSet。flag.NewFlagSet需要两个参数:一个标志集的名称,和一个报告解析错误的策略。用flag.(*FlagSet).Name方法获取*flag.FlagSet的名称。我们在(*GreetCommand).Name()方法中使用这个方法,所以子命令的名字与我们给*flag.FlagSet的名字一致。 NewGreetCommand也用了类似于以前的例子的方式定义了一个-name标志,但它改为从*GreetCommand的*flag.FlagSet字段中调用这个方法,gc.fs。当root调用*GreetCommand的Init()方法时,我们将传入的参数传递给*flag.FlagSet字段的Parse方法。
如果你构建这个程序,然后运行它,就会更容易看到子命令。建立该程序:
go build subcommand.go
现在运行该程序,没有参数:
./subcommand
你会看到如下输出:
Output
You must pass a sub-command
现在用greet子命令运行该程序。
./subcommand greet
这会输出如下内容:
Output
Hello World !
现在使用-name标志和greet来指定一个名字:
./subcommand greet -name Sammy
你会看到程序给出的这个输出:
Output
Hello Sammy !
这个例子说明了在 Go 中如何构建大型命令行应用程序的一些原则。 FlagSets的设计是为了给开发者提供更多的控制权,使其能够通过 flag 解析逻辑,分析flag的位置和处理方式。
总结 #
标记使你的应用程序在更多情景下更有用,因为它们让你的用户控制程序的执行方式。给用户提供有用的默认值很重要,但你应该让他们有机会覆盖那些不适合他们情况的设置。你已经看到,flag包提供了灵活的选择,向你的用户展示配置选项。你可以选择一些简单的标志,或者建立一套可扩展的子命令。 无论是哪种情况,在过去长久历史沉淀的风格下,使用flag包都可以帮助你按照灵活的、可编写脚本的命令行工具。