介绍
命令行实用程序很少有用在没有额外配置的情况下。好的默认值很重要,但有用的实用程序需要接受用户的配置。在大多数平台上,命令行实用程序接受旗帜来定制命令执行。
在本教程中,您将探索使用旗帜
包来构建不同类型的命令行实用程序的各种方法.您将使用旗帜来控制程序的输出,引入位置论点,在那里您混合旗帜和其他数据,然后实施子命令。
使用旗帜来改变程序的行为
使用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
是命令,init
是git
的子命令。
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 系列。