如何在 Go 中使用标志包

介绍

命令行实用程序很少有用在没有额外配置的情况下。好的默认值很重要,但有用的实用程序需要接受用户的配置。在大多数平台上,命令行实用程序接受旗帜来定制命令执行。

在本教程中,您将探索使用旗帜包来构建不同类型的命令行实用程序的各种方法.您将使用旗帜来控制程序的输出,引入位置论点,在那里您混合旗帜和其他数据,然后实施子命令。

使用旗帜来改变程序的行为

使用flag包包括三个步骤:首先, 定义变量来捕捉旗值,然后定义你的Go应用程序将使用的旗帜,最后,分析执行时给应用程序提供的旗帜。

为了说明,您将创建一个程序,该程序定义一个 Boolean的旗帜,该旗帜将改变将打印到标准输出的消息。

创建一个名为boolean.go的新文件:

1nano boolean.go

将下列代码添加到文件中以创建程序:

 1[label boolean.go]
 2package main
 3
 4import (
 5    "flag"
 6    "fmt"
 7)
 8
 9type Color string
10
11const (
12    ColorBlack Color = "\u001b[30m"
13    ColorRed          = "\u001b[31m"
14    ColorGreen        = "\u001b[32m"
15    ColorYellow       = "\u001b[33m"
16    ColorBlue         = "\u001b[34m"
17    ColorReset        = "\u001b[0m"
18)
19
20func colorize(color Color, message string) {
21    fmt.Println(string(color), message, string(ColorReset))
22}
23
24func main() {
25    useColor := flag.Bool("color", false, "display colorized output")
26    flag.Parse()
27
28    if *useColor {
29    	colorize(ColorBlue, "Hello, DigitalOcean!")
30    	return
31    }
32    fmt.Println("Hello, DigitalOcean!")
33}

此示例使用 ANSI Escape Sequences来指示终端显示彩色输出. 这些是特定的字符序列,所以为它们定义一个新类型是有道理的。 在本示例中,我们将该类型称为颜色,并将类型定义为字符串。 然后我们定义在const块中使用的颜色组合。 定义为const块后定义的colorize函数接受这些颜色常数之一和字符串变量来调色消息。 然后,它指示终端通过先打印所需颜色的escape序列来改变颜色,然后打印消息,并最终要求终端通过打印特殊颜色重置序列来重置颜色。

中,我们使用flag.Bool函数来定义一个名为颜色的布尔旗帜。该函数的第二个参数false设置了这个旗帜的默认值,当它没有提供的时候。

最后的参数是可以作为使用信息打印的文档串。从这个函数返回的值是指向bool的指针。下一行上的flag.Parse函数使用这个指针来根据用户传输的旗帜设置bool变量。我们可以通过指引指针来检查这个bool指针的值。 有关指针变量的更多信息可以在 指针教程中找到。 使用这个 Boolean 值,当设置颜色旗帜时,我们可以调用colorize,并在旗帜不存在时调用fmt.Println变量。

保存文件并运行程序没有任何旗帜:

1go run boolean.go

您将看到以下输出:

1[secondary_label Output]
2Hello, DigitalOcean!

现在用颜色旗再次运行此程序:

1go run boolean.go -color

输出将是相同的文本,但这次是蓝色。

标志不是指令传输的唯一值,您也可以发送文件名或其他数据。

用积极论点工作

通常,命令会采取一系列作为命令焦点的主题的参数。例如,打印文件的第一行的命令通常被称为头例.txt

Parse()函数将继续分析它遇到的旗帜,直到检测到一个非旗帜参数. flag包通过Args()Arg()函数来提供这些参数。

为了说明这一点,您将构建一个简化的命令的重新实现,该命令显示给定文件的头几个行:

创建一个名为head.go的新文件,并添加以下代码:

 1[label head.go]
 2package main
 3
 4import (
 5    "bufio"
 6    "flag"
 7    "fmt"
 8    "io"
 9    "os"
10)
11
12func main() {
13    var count int
14    flag.IntVar(&count, "n", 5, "number of lines to read from the file")
15    flag.Parse()
16
17    var in io.Reader
18    if filename := flag.Arg(0); filename != "" {
19    	f, err := os.Open(filename)
20    	if err != nil {
21    		fmt.Println("error opening file: err:", err)
22    		os.Exit(1)
23    	}
24    	defer f.Close()
25
26    	in = f
27    } else {
28    	in = os.Stdin
29    }
30
31    buf := bufio.NewScanner(in)
32
33    for i := 0; i < count; i++ {
34    	if !buf.Scan() {
35    		break
36    	}
37    	fmt.Println(buf.Text())
38    }
39
40    if err := buf.Err(); err != nil {
41    	fmt.Fprintln(os.Stderr, "error reading: err:", err)
42    }
43}

首先,我们定义了一个变量,以保持该程序应该从文件中读取的行数。然后我们使用flag.IntVar定义了n旗帜,反映了原来的程序的行为。这个函数允许我们将自己的指针(https://www.digitalocean.com/community/conceptual_articles/understanding-pointers-in-go)传递给一个变量,而不是函数,这些函数没有Var补丁。除了这种差异,其他参数的flag.IntVar遵循其flag.Int对应:旗名,默认值和描述。

下一节读取文件. 我们首先定义一个 io.Reader 变量,它将被设置为用户要求的文件,或标准输入传递给程序. 在 if 语句中,我们使用 flag.Arg 函数来访问所有旗帜之后的第一个位置参数。如果用户提供了一个文件名称,它将被设置。否则,它将是空串(")。当一个文件名存在时,我们使用 os.Open 函数来打开该文件,并将我们之前定义的 io.Reader 设置为该文件。否则,我们使用 os.Stdin 从标准输入中读取。

最后的部分使用了与bufio.NewScanner创建的*bufio.Scanner来读取io.Reader变量in的行,我们使用for循环(https://andsky.com/tech/tutorials/how-to-construct-for-loops-in-go)重复到 count的值,如果用buf.Scan扫描行产生false值,表示行数小于用户要求的数目。

运行此程序并使用head.go作为文件参数来显示您刚刚写的文件的内容:

1go run head.go -- head.go

--分离器是由flag包识别的特殊旗帜,表明不会有更多的旗号参数。

1[secondary_label Output]
2package main
3
4import (
5        "bufio"
6        "flag"

使用您定义的n旗来调整输出量:

1go run head.go -n 1 head.go

这只输出包装声明:

1[secondary_label Output]
2package main

最后,当程序检测到没有提供位置参数时,它会读取标准输入的输入,就像

1echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3

你会看到输出:

1[secondary_label Output]
2fish
3lobsters
4sharks

到目前为止,您所看到的函数的行为仅限于检查整个命令召唤,您并不总是希望这种行为,特别是如果您正在编写支持子命令的命令行工具。

使用 FlagSet 执行子命令

现代命令行应用程序经常实施子命令,以便在一个单一命令下组合一套工具.使用这种模式的最著名的工具是git。当检查一个命令如git init,git是命令,initgit的子命令。

Go 应用程序可以使用flag.(*FlagSet)类型支持带有自己的旗帜的子命令,为了说明这一点,创建一个使用两个带有不同旗帜的子命令执行命令的程序。

创建一个名为subcommand.go的新文件,并将下列内容添加到文件中:

 1package main
 2
 3import (
 4    "errors"
 5    "flag"
 6    "fmt"
 7    "os"
 8)
 9
10func NewGreetCommand() *GreetCommand {
11    gc := &GreetCommand{
12    	fs: flag.NewFlagSet("greet", flag.ContinueOnError),
13    }
14
15    gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted")
16
17    return gc
18}
19
20type GreetCommand struct {
21    fs *flag.FlagSet
22
23    name string
24}
25
26func (g *GreetCommand) Name() string {
27    return g.fs.Name()
28}
29
30func (g *GreetCommand) Init(args []string) error {
31    return g.fs.Parse(args)
32}
33
34func (g *GreetCommand) Run() error {
35    fmt.Println("Hello", g.name, "!")
36    return nil
37}
38
39type Runner interface {
40    Init([]string) error
41    Run() error
42    Name() string
43}
44
45func root(args []string) error {
46    if len(args) < 1 {
47    	return errors.New("You must pass a sub-command")
48    }
49
50    cmds := []Runner{
51    	NewGreetCommand(),
52    }
53
54    subcommand := os.Args[1]
55
56    for _, cmd := range cmds {
57    	if cmd.Name() == subcommand {
58    		cmd.Init(os.Args[2:])
59    		return cmd.Run()
60    	}
61    }
62
63    return fmt.Errorf("Unknown subcommand: %s", subcommand)
64}
65
66func main() {
67    if err := root(os.Args[1:]); err != nil {
68    	fmt.Println(err)
69    	os.Exit(1)
70    }
71}

这个程序分为几个部分:‘主’函数、‘根’函数和执行子命令的个别函数。‘主’函数处理从命令中返回的错误。如果任何函数返回了 错误,‘如果’声明将捕捉它,打印错误,并且该程序将以状态代码为‘1’退出,表明操作系统的其他部分发生了错误。在‘主’中,我们将被召唤的所有参数转移到‘根’。

root函数定义了[]Runner,其中将定义所有子命令。Runner是子命令的界面(https://andsky.com/tech/tutorials/how-to-use-interfaces-in-go),允许root使用Name()获取子命令的名称,并将其与内容subcommand变量进行比较。

我们只定义一个子命令,虽然这个框架可以很容易地使我们创建另一个子命令。GreetCommand是使用NewGreetCommand进行实例化,我们使用flag.NewFlagSet创建一个新的*flag.FlagSet命令。flag.NewFlagSet采用两种参数:旗组的名称和报告错误的策略。NewGreetCommand的名称可以使用flag.(*FlagSet).Name方法来访问。我们在(*GreetCommand).Name()方法中使用这种方法,所以子命令的名称与我们给*flag.FlagSet的名称相匹配。GreetCommand也以类似于例子之前的方式定义

如果你构建这个程序,然后运行它,你会更容易看到子命令。

1go build subcommand.go

现在运行程序没有论点:

1./subcommand

你会看到这个输出:

1[secondary_label Output]
2You must pass a sub-command

现在用问候子命令运行程序:

1./subcommand greet

这产生了以下产出:

1[secondary_label Output]
2Hello World !

现在使用-name旗帜与greet来指定一个名字:

1./subcommand greet -name Sammy

你会看到这个节目的输出:

1[secondary_label Output]
2Hello Sammy !

这个例子说明了在Go中如何构建更大的命令行应用程序背后的某些原则。 `FlagSet 旨在为开发人员提供更多的控制权,以便通过旗帜解析逻辑来处理旗帜。

结论

旗帜使您的应用程序在更多情况下变得更有用,因为它们让用户控制程序的执行方式。重要的是给用户有用的默认设置,但你应该给他们机会取代不适合他们的情况的设置。你已经看到旗帜包提供了灵活的选择,为用户提供配置选项。你可以选择几个简单的旗帜,或者构建一个可扩展的子命令套件。在任何情况下,使用旗帜包将帮助你构建实用工具,就像灵活和可编写命令行工具的悠久历史一样。

要了解更多关于 Go 编程语言的信息,请参阅我们的完整 How To Code in Go 系列

Published At
Categories with 技术
Tagged with
comments powered by Disqus