如何在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.Scannerio.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是命令,initgit的子命令。 子命令的一个显着特征是每个子命令都可以有自己的标志集合。

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 函数处理从命令返回的错误。 如果任何函数返回 errorif 语句将捕获它,打印错误,程序将退出并返回状态码 1,表示出现错误发生在操作系统的其余部分。 在 main 中,我们将调用程序的所有参数传递给 root。 我们通过首先切片 os.Args 来删除第一个参数,它是程序的名称(在前面的示例中为 ./subcommand)。

root 函数定义了 []Runner,其中所有子命令都将被定义。 Runner 是子命令的 接口 ,它允许 root 使用 Name() 检索子命令的名称,并将其与内容 [ X166X] 变量。 一旦在遍历 cmds 变量后找到正确的子命令,我们将使用其余参数初始化子命令并调用该命令的 Run() 方法。

我们只定义了一个子命令,尽管这个框架很容易让我们创建其他的。 GreetCommand 使用 NewGreetCommand 实例化,我们使用 flag.NewFlagSet 创建一个新的 *flag.FlagSetflag.NewFlagSet 有两个参数:标志集的名称和报告解析错误的策略。 使用 flag.(*FlagSet).Name 方法可以访问 *flag.FlagSet 的名称。 我们在 (*GreetCommand).Name() 方法中使用它,因此子命令的名称与我们给 *flag.FlagSet 的名称匹配。 NewGreetCommand 还定义了一个 -name 标志,其方式与前面的示例类似,但它改为将其称为 *GreetCommand*flag.FlagSet 字段之外的方法, gc.fs。 当 root 调用 *GreetCommandInit() 方法时,我们将提供的参数传递给 *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 系列中编码