如何在Go中使用flag包
介绍
如果没有额外的配置,命令行实用程序很少开箱即用。 好的默认值很重要,但有用的实用程序需要接受用户的配置。 在大多数平台上,命令行实用程序接受标志来自定义命令的执行。 标志是在命令名称之后添加的键值分隔字符串。 Go 允许您使用标准库中的 flag
包来制作接受标志的命令行实用程序。
在本教程中,您将探索使用 flag
包构建不同类型的命令行实用程序的各种方法。 您将使用标志来控制程序输出,在混合标志和其他数据时引入位置参数,然后实现子命令。
使用标志来改变程序的行为
使用 flag
包涉及三个步骤:首先,定义变量 以捕获标志值,然后定义您的 Go 应用程序将使用的标志,最后,解析在执行时提供给应用程序的标志. flag
包中的大多数函数都涉及定义标志并将它们绑定到您定义的变量。 解析阶段由 Parse()
函数处理。
为了说明,您将创建一个程序,该程序定义一个 Boolean 标志,该标志更改将打印到标准输出的消息。 如果提供了 -color
标志,程序将以蓝色打印一条消息。 如果没有提供标志,则消息将不带任何颜色打印。
创建一个名为 boolean.go
的新文件:
nano boolean.go
将以下代码添加到文件中以创建程序:
布尔.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 Escape Sequences 来指示终端显示彩色输出。 这些是特殊的字符序列,因此为它们定义新类型是有意义的。 在此示例中,我们将该类型称为 Color
,并将该类型定义为 string
。 然后,我们定义一个调色板以在后面的 const
块中使用。 在 const
块之后定义的 colorize
函数接受这些 Color
常量之一和用于消息着色的 string
变量。 然后它通过首先打印请求颜色的转义序列来指示终端更改颜色,然后打印消息,最后通过打印特殊颜色重置序列请求终端重置其颜色。
在 main
中,我们使用 flag.Bool
函数来定义一个名为 color
的布尔标志。 此函数的第二个参数 false
在未提供此标志时设置它的默认值。 与您的预期相反,将其设置为 true
不会反转行为,因此提供标志会导致其变为假。 因此,这个参数的值几乎总是带有布尔标志的 false
。
最后一个参数是可以打印为使用消息的文档字符串。 从这个函数返回的值是一个指向 bool
的指针。 下一行的 flag.Parse
函数使用此指针根据用户传入的标志设置 bool
变量。 然后我们可以通过取消引用指针来检查这个 bool
指针的值。 更多关于指针变量的信息可以在指针教程中找到。 使用这个布尔值,我们可以在设置 -color
标志时调用 colorize
,并在标志不存在时调用 fmt.Println
变量。
保存文件并在没有任何标志的情况下运行程序:
go run boolean.go
您将看到以下输出:
OutputHello, 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
程序的行为。 与没有 Var
后缀的 flag
函数相比,此函数允许我们将自己的 指针 传递给变量。 除了这个区别之外,flag.IntVar
的其余参数遵循其 flag.Int
对应项:标志名称、默认值和描述。 和前面的例子一样,我们然后调用 flag.Parse()
来处理用户的输入。
下一节将读取该文件。 我们首先定义一个 io.Reader
变量,它要么设置为用户请求的文件,要么设置为传递给程序的标准输入。 在 if
语句中,我们使用 flag.Arg
函数来访问所有标志之后的第一个位置参数。 如果用户提供了一个文件名,这将被设置。 否则,它将是空字符串 (""
)。 当存在文件名时,我们使用 os.Open
函数打开该文件并将我们之前定义的 io.Reader
设置为该文件。 否则,我们使用 os.Stdin
从标准输入读取。
最后一部分使用由 bufio.NewScanner
创建的 *bufio.Scanner
从 io.Reader
变量 in
中读取行。 我们使用 for 循环 迭代到 count
的值,如果使用 buf.Scan
扫描行产生 false
,则调用 break
value,表示行数小于用户请求的行数。
运行此程序并使用 head.go
作为文件参数显示您刚刚编写的文件的内容:
go run head.go -- head.go
--
分隔符是 flag
包识别的特殊标志,表示后面没有更多标志参数。 运行此命令时,您会收到以下输出:
Outputpackage main import ( "bufio" "flag"
使用您定义的 -n
标志来调整输出量:
go run head.go -n 1 head.go
这仅输出包语句:
Outputpackage main
最后,当程序检测到没有提供位置参数时,它会从标准输入读取输入,就像 head
。 尝试运行以下命令:
echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3
你会看到输出:
Outputfish 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
函数处理从命令返回的错误。 如果任何函数返回 error,if
语句将捕获它,打印错误,程序将退出并返回状态码 1
,表示出现错误发生在操作系统的其余部分。 在 main
中,我们将调用程序的所有参数传递给 root
。 我们通过首先切片 os.Args
来删除第一个参数,它是程序的名称(在前面的示例中为 ./subcommand
)。
root
函数定义了 []Runner
,其中所有子命令都将被定义。 Runner
是子命令的 接口 ,它允许 root
使用 Name()
检索子命令的名称,并将其与内容 [ X166X] 变量。 一旦在遍历 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
你会看到这个输出:
OutputYou must pass a sub-command
现在使用 greet
子命令运行程序:
./subcommand greet
这会产生以下输出:
OutputHello World !
现在使用 -name
标志和 greet
来指定名称:
./subcommand greet -name Sammy
您将看到程序的以下输出:
OutputHello Sammy !
这个例子说明了如何在 Go 中构建更大的命令行应用程序背后的一些原则。 FlagSet
旨在让开发人员更好地控制标志解析逻辑在何处以及如何处理标志。
结论
标志使您的应用程序在更多上下文中更有用,因为它们使您的用户可以控制程序的执行方式。 为用户提供有用的默认值很重要,但您应该让他们有机会覆盖对他们的情况不起作用的设置。 您已经看到 flag
包提供了灵活的选择来向您的用户呈现配置选项。 您可以选择一些简单的标志,或者构建一个可扩展的子命令套件。 在任何一种情况下,使用 flag
包都将帮助您以灵活且可编写脚本的命令行工具的悠久历史的风格构建实用程序。
要了解有关 Go 编程语言的更多信息,请查看我们完整的 如何在 Go 系列中编码 。