Go语言学习笔记(十七)
一、使用命令行程序
在图形用户界面(GUI)面世前,与计算机交互通常是通过命令行进行的。当前,对程序员和系统管理员来说,命令行程序依然是一种流行而实用的与底层操作系统交互的方式。出于如下原因,程序员想创建命令行程序。
- 为创建能够定期自动运行的脚本
- 为创建与系统中的文件交互的脚本
- 为创建能够执行系统维护任务的脚本
- 为避免设计图形用户界面这种无谓的开销
命令行程序通常执行下面的操作:管理目录中的文件或接收一些输入数据并返回一些输出数据。一个这样的典型实例是Windows、Linux和macOS都支持的命令sort,这个命令接受一个美行都包含单词的文件,并返回排序后的版本。- 命令行程序可用任何编程语言来编写,只要脚本是可执行的,就可使用中断来运行(执行)它。
1 操作输入和输出
编写命令行程序前,必须明白一些理论,确保我们编写的脚本能够与操作系统和其他脚本交互。命令行程序操作输入和输出。对于这些输入和输出,Windows、MacOS和Linux使用的术语相同,而Go语言也是用这些术语,鉴于此,熟悉有关输入和输出的术语和代码很有用
:如下表
名称 | 代码 | 描述 |
---|---|---|
标准输入 | 0 | 包含提供给程序的输入 |
标准输出 | 1 | 包含显示到屏幕上的输出 |
标准错误 | 2 | 包含显示到屏幕上的错误消息 |
- 标准输入是提供给命令行程序的数据,他可以是文件,也可以是文本字符串。
- 标准输出是来自程序的输出。标准错误时来自程序的错误
- 长期运行的进程(如Web服务器)常常将数据同时记录到标准输入和标准输出,及通常将数据发送到日志文件。与命令行程序相关的生态系统几乎都是从正确地使用标准流(即标准输入、标准输出和标准错误)的程序衍生而来的,因此对彪顺流有大致的了解将对我们有很大帮助。
2 访问命令行参数
在创建命令行程序方面,Go语言提供了强大的支持。它遵循接收输入并发送输出的理念,且通常会自动确保输出被发送到正确的输出流。在命令行中传递给命令行程序的数据被称为参数。在Go语言中,要读取传递给命令行的参数,可使用标准库中的os包,如下面程序所示:
package main
import (
"fmt"
"os"
)
func main() {
for i, arg := range os.Args {
fmt.Println("argument", i, "is", arg)
}
}
方法Args返回一个结果切片,其中包含程序的名称以及传递给程序的所有参数。在这个实例中使用range来遍历参数并将其打印到终端。
go build之后,在Windows下将生成一个.exe的文件,在Linux和macOS系统中,执行go build将生成一个没有拓展名的可执行文件。请注意,在Windows和Linux/macOS系统中执行文件的方式存在细微差别。在Linux/或macOS中,要执行当前工作目录下的可执行文件,必须在指定文件时加上前缀./:而在Windows系统中,不需要添加这样的前缀。
Windows下结果如图:
3 分析命令行标志
虽然可使用os包来获取命令行参数,但Go语言还在标准库中提供了flag包。除了os.Args的功能外,这个包还提供了众多其他的功能,其中包括以下几点。
- 指定作为参数传递的值的类型
- 设置标志的默认值
- 自动生成帮助文本
演示程序如下:
package main
import (
"flag"
"fmt"
)
func main() {
s := flag.String("s", "Hello world", "String help text")
flag.Parse()
fmt.Println("value of s:", *s)
}
程序解读如下。
- 声明变量s并将其设置为flag.String返回的值
- flag.String能够让您声明命令行标志,并指定其名称、默认值和帮助文本。
- 调用flag.Parse,让程序能够传递声明的参数
- 最后打印变量s的值。请注意,flag.String返回的是一个指针,因此使用运算符*对其解除引用,以便显示底层的值,
执行这个程序,会发现flag包给标志-s设置了默认值。
我们也可以给-s赋值
我们可以使用-h/–h/-help/–help来查看flag自动创建的一些帮助文本,如下结果完全一样,毕竟是一个很小的程序:
PS:在运行第一个例子的时候,可以传递多个参数。为此,可先指定这个可执行文件的名称,在指定用空格分割的参数。程序将收到这些参数并将它们打印到终端:
4 指定标志的类型
flag包根据声明分析标志的类型,这对应于Go语言的类型系统。编写命令程序时,必须考虑程序将接收的数据,并将其映射到正确的类型。下面的实例程序演示了如何分析String、Int和Boolean标志并将它们的值打印到终端。
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("value of s:", *s)
fmt.Println("value of i:", *i)
fmt.Println("value of b:", *b)
}
如果运行这个程序,将发现正确地设置了这些标志的值
同样我们可以修改这些参数,而且,程序会自动将我们传入的字符串(char(42))指定为对应参数的类型(int(42)),对于Boolean标志,指定即设为true,如下图:
当然如果输入的字符串不能转换为对应的类型将报错,
5 自定义帮助文本
虽然flag包会自动生成帮助文本,但完全可以覆盖默认的帮助格式并提供自定义的帮助文本。为此可将变量Usage设置为一个函数,这样每当在分析标志的过程中发生错误时,都将调用这个函数。这是关于这个函数的简单实现:
flag.Usage = func(){
fmt.Fprintln(os.Stderr,"hello world")
}
这里使用了标准库中的os包来讲喜爱西打印到标准误差(standard Error),因为这条消息将在分析错误的时候显示,但输出是完全可定制的。
详细实例如下
package main
import (
"flag"
"fmt"
"os"
)
func main() {
flag.Usage = func() {
usageText := `Usage example04 [OPTION]
An example of customising usage output
-s, --s example string argument, default: String help text
-i, --i example integer argument, default: Int help text
-b, --b example boolean argument, default: Bool help text`
fmt.Fprintf(os.Stderr, "%s\n", usageText)
}
s := flag.String("s", "Hello world", "String help text")
i := flag.Int("i", 1, "Int help text")
b := flag.Bool("b", true, "Bool help text")
flag.Parse()
fmt.Println("value of s:", *s)
fmt.Println("value of i:", *i)
fmt.Println("value of b:", *b)
}
运行结果如下
6 创建子命令
很多命令行都支持子命令,一个简单的实例是git,它包含顶级命令git和多个子命令,而这些子命令都有独立的选项和帮助文本。下面是git的一些子命令
git clone
fit branch
如果运行这些子命令的时候指定表示–help,我们将发现他们有独立的选项。flag包通过FlagSets提供了子命令支持,让我们能够创建子命令,并制定独立的标志集。要创建子命令并指定标志可像下面这样做:
cloneCmd :=flagNewFlagSet(“clone”,flag.ExitError)
其中第一个参数为子命令名,而第二个参数则制定了错误处理行为1.flag.ContinueOneError:如果没有分析错误,就继续执行
2.flag.ExitOnError:如果有分析错误,就退出并将状态码设置为2
3.flag.PanicOnError:如果发生分析错误,就引发panic
使用NewFlagSet可创建独立的标志集。要根据参数作相应的处理,可使用switch语句。在switch语句中使用os.Arg来处理标志集。因为要处理的是标志集,而索引是从0开始的,所以这里要使用索引1:
switch os.Arg[1]{
case "clone":
//这里处理clone子命令
case "branch":
//这里处理branch子命令
default:
//处理其他操作
}
在下面的实例中,创建了一个命令行工具,它提供了两个命令:uppercase和lowercase。这些命令接受标志-s或者–s指定的字符串,并返回处理后的文本。
uppercaseCmd := flag.NewFlagSet("uppercase", flag.ExitOnError)
lowercaseCmd := flag.NewFlagSet("lowercase", flag.ExitOnError)
switch os.Args[1] {
case "uppercase":
s := uppercaseCmd.String("s", "", "A string of text to be uppercased")
uppercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToUpper(*s))
case "lowercase":
s := lowercaseCmd.String("s", "", "A string of text to be lowercased")
lowercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToLower(*s))
default:
flag.Usage()
}
解释如下:
- 创建了两个FlagSet,一个表示命令uppercase,另一个表示命令lowercase。
- 使用switch语句读取命令的第一个参数。
- 如果这个参数为uppercase,就在FlagSet uppercase中初始化一个字符串标志,再讲其他参数传递给FlagSet uppercase,并对他们进行分析。
- 将s的值传递给strings包中的方法ToUpper,以便将其转换为大写。如果用户没有给s指定值,将传递默认的空字符串
- 如果第一个参数既不是uppercase也不是lowercase,将执行switch语句的default部分,单在这里啥都没有做
如果没有参数,我们可以用Usage()函数设置输出,当没有第二个参数。即os.Arg的长度为1时调用,如下:
package main
import (
"flag"
"fmt"
"os"
"strings"
)
func flagUsage() {
usageText := `example05 is an example cli tool.
Usage:
example05 command [arguments]
The commands are:
uppercase uppercase a string
lowercase lowercase a string
Use "example05 [command] --help" for more information about a command.`
fmt.Fprintf(os.Stderr, "%s\n\n", usageText)
}
func main() {
flag.Usage = flagUsage
uppercaseCmd := flag.NewFlagSet("uppercase", flag.ExitOnError)
lowercaseCmd := flag.NewFlagSet("lowercase", flag.ExitOnError)
if len(os.Args) == 1 {
flag.Usage()
return
}
switch os.Args[1] {
case "uppercase":
s := uppercaseCmd.String("s", "", "A string of text to be uppercased")
uppercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToUpper(*s))
case "lowercase":
s := lowercaseCmd.String("s", "", "A string of text to be lowercased")
lowercaseCmd.Parse(os.Args[2:])
fmt.Println(strings.ToLower(*s))
default:
flag.Usage()
}
}
运行结果如下:
7 POSIX兼容性
在Linux和macOS系统中,大多数命令行工具都要求以推荐标准POSIX指定的方式传递参数。POSIX是一系列标准,旨在确保操作系统之间彼此兼容。很多开发人员都希望采用这种方式,因此关于Go语言的flag包的话题中,经常会引发对POSIX兼容性的讨论。
虽然Go语言的eflag包没有醉熏这些推荐标准,但有多个第三方替代品遵循了这些推荐标准
8 安装和分享命令行程序
如果我们想要自己的命令行程序能够在任何地方使用可以使用go install命令
安装它
- 将自己写的命令行程序放在
%GOPATH%/src/github.com/[your github username]/[package name]
下- 执行
go install %GOPATH%/src/github.com/[your github username]/[package name]
命令即可
9 相关问题
1 如何查看命令的退出状态?
要查看命令的退出状态,在Windows系统中可使用
echo%errorlevel%
,而在macOS和Linux中可使用echo $?
2 Go语言为何将-option
和--option
视为同一个选项?
虽然使用一个连字符和两个连字符的选项通常是不同的选项,但Go设计者将它们视为相同的选项。
3 使用go install
安装他人提供的命令行程序安全么?
安装并运行包之前,务必检查其内容。在访问操作系统时,Go程序的权限很大,因此务必谨慎地运行他们。
4 命令行程序出现错误时,如果想将其显示给用户,应该将其发送到哪种输出?
要在命令行脚本中显示错误,应使用标准错误。要将文本发送到标准错误,可像下面这样使用fmt和os包。
参考书籍
[1]: 【Go语言入门经典】[英] 乔治·奥尔波 著 张海燕 译