「每周译Go」在 Go 里面如何使用 Flag 包

我在这儿 

Gophers,今天是《How To Code In Go》系列的最后一篇了!

坚持学习下来的每位 Gopher 都是 NB 的!给自己一个大大赞!

若需要完整版合集可在我们的收录合集 #How To Code In Go (文末有,去点)点击查看 或 至公众号后台回复 [codeingo] 查看 pdf 获取方法哦。


在 Go 里面如何使用 Flag 包

目录

  1. 在 Go 中导入包

  2. 理解 Go 中包的可见性

  3. 如何在 Go 中编写条件语句

  4. 如何在 Go 中编写 Switch 语句

  5. 如何在 Go 中构造 for 循环

  6. 在循环中使用 Break 和 Continue

  7. 如何在 Go 中定义并调用函数

  8. 如何在 Go 中使用可变参数函数

  9. 了解 Go 中的 defer

  10. 了解 Go 中的 init

  11. 用构建标签定制 Go 二进制文件

  12. 了解 Go 中的指针

  13. 在 Go 中定义结构体

  14. 在 Go 中定义方法

  15. 何构建和安装 Go 程序

  16. 如何在 Go 中使用结构体标签

  17. 如何在 Go 使用 interface

  18. 在不同的操作系统和架构编译 Go 应用

  19. 用 ldflags 设置 Go 应用程序的版本信息

  20. 在 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 逃逸序列[1]来指示终端显示彩色输出。这些是专门的 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.Scannerio.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是一个子命令的  nterface (点击跳转查看),允许root使用Name()获取子命令的名称,并将其与变量subcommand内容进行比较。一旦在遍历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

你会看到如下输出:

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包都可以帮助你按照灵活的、可编写脚本的命令行工具。

相关链接:

[1]https://en.wikipedia.org/wiki/ANSI_escape_code



「每周译Go」在 Go 里面如何使用 Flag 包_第1张图片

小 G 向您发出参会邀请

你可能感兴趣的:(golang,java,开发语言,后端)