[Go 入门] 第十六章 使用命令行程序

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

命令行程序也叫命令行使用程序或工具, 它被设计到终端运行, 本文内容:

理解命令行

在图形界面(GUI)面世前,与计算机交互通常都是通过命令行进行的。当前,对程序员和系统管理员来说,命令行依然是一种流行且使用的与底层操作系统交互的方式

  • 为创建能够定期自动运行的脚本
  • 为创建于系统中的文件交互的脚本
  • 为创建能够执行系统维护任务的脚本
  • 为避免设计图形用户界面这种无谓的开销

操作输入和输出

编写命令行程序前,必须明白一些理论,确保编写的脚本能够与操作系统和其他脚本交互,命令行程序操作输入和输出。对于这些输入和输出,Windows、macOS 和 Linux 使用的术语相同,而 Go 语言也使用这些术语。

名称 代码 描述
标准输入 0 包含提供给程序的输入
标准输出 1 包含显示到屏幕上的输出
标准错误 2 包含显示到屏幕上的错误消息

标准输入是提供命令行程序的数据,它可以是文件,也可以是文本字符串,

标准输出是来自程序的输出,标准错误是来自程序的错误

长期运行的进程(如 Web 服务器)常常将数据同时记录到标准输入和标准输出,即通常将数据发送到日志文件。与命令行程序相关的生态系统几乎都是从正确地使用标准流(标准输入、标准输出、标准错误)的程序衍生来的。

访问命令行参数

在创建命令行程序方面,Go 语言提供了强大的支持。它遵循接受输入并发送输出的理念,且通常会自动确保输出被发送到正确的输出流。在命令行中传递给命令行程序的数据被称为参数。在Go语言中,要读取传递给命令行程序的参数,可使用标准库的os

1
2
3
4
5
func main() {
for i, arg := range os.Args {
fmt.Println("argument", i, "is", arg)
}
}

分析命令行标志

虽然可使用 os 包来获取命令行参数,但Go语言还在标准库中提供了flag包。flag提供了众多其他功能,包括以下几点:

  • 指定作为参数传递的值的类型
  • 设置标志的默认值
  • 自动生成帮助文本
1
2
3
4
5
func main() {
s := flag.String("s", "Hello world", "String help text")
flag.Parse()
fmt.Println(*s)
}

解读如下:

  • 声明变量s并将其设置为 flag.String 返回的值
  • flag.String 能够让您声明命令行标志,并制定其名称、默认值和帮助文本
  • 调用 flag.Parse, 让程序能够传递声明的参数
  • 最后,打印变量 s 的值,flag.String 返回的是一个指针,因此使用运算符*对其解除引用,以便显示底层的值

如果运行,则 flag 包为 s 设置了默认的值

script
1
2
F:\Go\src\awesomeProject>go run main.go
Hello world

也可以为使用 -s 给标志指定值

script
1
2
F:\Go\src\awesomeProject>go run main.go -s test
test

flag 包会自动创建一些帮助文本,要显示它们,可使用如下任何标志:

  • -h
  • —h
  • -help
  • —help
script
1
2
3
4
5
F:\Go\src\awesomeProject>go run main.go -h
Usage of C:\Users\Admin\AppData\Local\Temp\go-build115081486\b001\exe\main.exe:
-s string
String help text (default "Hello world")
exit status 2

指定标志的类型

flag 包根据声明分析标志的类型,这对应于 Go 语言的类型系统。编写命令行程序时,必须考虑程序将接收的数据,并将其映射到正确的类型,这一点很重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"flag"
"fmt"
)

func main() {
s := flag.String("s", "Hello world", "String help text")
i := flag.Int("i", 1, "Int help text")
b := flag.Bool("b", false, "Bool help text")
flag.Parse()
fmt.Println("s is ", *s)
fmt.Println("i is ", *i)
fmt.Println("b is ", *b)
}

执行这个示例时,可通过传入值来修改标志的值。请注意,对于 Bool 标志,如果没有指定,则默认为 true

其次,flag 包有三种语法格式,请注意:

1
2
3
-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型
script
1
2
3
4
F:\Go\src\awesomeProject>go run main.go -s=test -i 100 -b
s is test
i is 100
b is true

自定义帮助文本

虽然 flag 包会自动生成帮助文本,但完全可以覆盖默认的帮助格式并提供自定义的帮助文本,为此可将变量 Usage 设置为一个函数

1
2
3
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "hello world")
}

这里使用了标准库中的 os 包来将消息打印到标准误差 (standard Error),因为这条消息将在发现分析错误时显示,但输出是完全可定制的

创建子命令

很多命令行程序都支持子命令,一个典型的示例是 git,它包含顶级命令 git 和多个子命令,而这些子命令都有独立的选项和帮助文本,下面是 git 的一堆子命令

script
1
2
git clone
git branch

如果运行这些子命令时指定标志 —help,它们将会有独立的选型。flag包通过FlagSets提供了子命令支持,让您能够创建子命令,并指定独立的标志集。要创建子命令并指定标志,可像下面这样做:

cloneCmd := flag.NewFlagSet("clone", flag.ExitOnError)

其中第一个参数为子命令名,而第二个参数指定了错误处理行为

  • flag.ContinueOnError: 如果没有分析错误,就继续执行
  • flag.ExitOnError: 如果有错误分析,就退出并将状态码,设置为2
  • flag.PanicOnError: 如果发生分析错误,就引发 Panic

使用 NewFlagSet 可创建独立的标志集。要根据参数做相应的处理,可使用 switch 语句。os.Args 包含原始参数,因此可在 swtich 语句中使用它来处理标志集。请注意,由于索引从0开始,因此这里需要使用索引1

1
2
3
4
5
6
7
8
swich os.Args[1] {
case "clone":
//处理clone子命令
case "branch":
//处理branch子命令
default:
// 这里处理其他条件
}

在下面的示例中,创建了一个命令行工具,它提供了两个命令: uppercase 和 lowercase,这些命令接受标志 -s 或 —s 指定的字符串,并返回处理后的文本。当然,这个实例很简单,这里只是演示如何创建子命令,而不是复杂的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uppercaseCmd := flag.NewFlagSet("uppercase", flag.ExitOnError)
lowercaseCmd := flag.NewFlagSet("lowercase", flag.ExitOnError)

switch os.Args[1] {
case "uppercase":
s := uppercaseCmd.String("s", "", "uppercase")
uppercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToUpper(*s))
case "lowercase":
s := lowercaseCmd.String("s", "", "lowercase")
lowercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToLower(*s))
default:
// 处理其他条件
}

解读如下:

  • 创建了两个 FlagSet, 一个表示命令 uppercase,另一个表示命令lowercase
  • 使用 switch 语句读取命令的第一个参数
  • 如果这个参数为 uppercase,就在FlagSet uppercase中初始化一个字符串标志,再将其他参数传递给 FlagSet uppercase,并对它们进行分析
  • 将 s 的值传递给 strings 包中的方法 ToUpper,以便将其转换为大写。如果用户没有给 s 指定值,将传递默认的空字符串
  • 对于 FlagSet lowercase,逻辑与此相同
  • 如果参数既不是 uppercase 也不是 lowercase,执行 default

当前,这个程序的顶层什么都没有做,例如,如果用户执行这个程序时,没有提供任何参数,则什么都不会做。

如果想在什么都没有传入的情况下,输出帮助文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"flag"
"fmt"
"os"
"strings"
)

func flagUsage() {
fmt.Fprintln(os.Stderr, "hello world")
}

func main() {

uppercaseCmd := flag.NewFlagSet("uppercase", flag.ExitOnError)
lowercaseCmd := flag.NewFlagSet("lowercase", flag.ExitOnError)

flag.Usage = flagUsage

if len(os.Args) == 1 {
flag.Usage()
return
}

switch os.Args[1] {
case "uppercase":
s := uppercaseCmd.String("s", "", "uppercase")
uppercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToUpper(*s))
case "lowercase":
s := lowercaseCmd.String("s", "", "lowercase")
lowercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToLower(*s))
default:
// 处理其他条件
}
}

如果用户没有输入任何参数,则会输出帮助文本 hello world

POSIX 兼容性

在 Linux 和 macOS 中,大多数命令行工具都要求以推荐标准 POSIX 指定的方式传递参数。POSIX 是一系列标准,旨在确保操作系统之间彼此兼容。很多程序员都希望采用这种方式,虽然 flag 包没有遵循这些推荐标准,但有多个第三方替代品遵循了这些推荐标准。

安装和分享命令行程序

开发好命令行程序后,可以在系统中安装它,以便在任何地方,而不只是在命令 go build 生成的二进制文件所在的文件夹中才能访问它,为了遵循 Go 语言的约定,请设置好 $GOPATH ,使用标准的目录布局

新建一个 dirname.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"flag"
"fmt"
"os"
"strings"
)

func flagUsage() {
fmt.Fprintln(os.Stderr, "hello world")
}

func main() {

uppercaseCmd := flag.NewFlagSet("uppercase", flag.ExitOnError)
lowercaseCmd := flag.NewFlagSet("lowercase", flag.ExitOnError)

flag.Usage = flagUsage

if len(os.Args) == 1 {
flag.Usage()
return
}

switch os.Args[1] {
case "uppercase":
s := uppercaseCmd.String("s", "", "uppercase")
uppercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToUpper(*s))
case "lowercase":
s := lowercaseCmd.String("s", "", "lowercase")
lowercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToLower(*s))
default:
// 处理其他条件
}
}

随后命令行执行

script
1
go install dirname.go

完成后,打开新的终端,执行

script
1
2
> dirname uppercase -s abc
ABC

问题列表

  • 如何查看命令退出的状态

    要查看命令的退出状态,可在 Windows 系统中使用 echo %errorlevel%,在 Linux 或 macOS 系统中,使用 echo $?

  • Go 为啥将 -option 和 —option 视为同一选项

    虽然二者连字符不是一样的,但 Go 设计者将它们视为同一选项

  • 使用 go install 安装他人提供的命令行安装吗

    在安装时,最好检查一下包的内容,在访问操作系统时, Go 程序的权限很大