调查 Go 的 exec.Command
因为我正在编写一个频繁执行命令的库,所以我想要充分理解exec.Command。
我想要从各个方面仔细观察它的行为。
执行“包”的概述。
首先让我们来翻译一下官方 Package exec 的概要。
包执行将执行外部命令。它封装了os.StartProcess,可以轻松地重定向stdin和stdout,通过管道连接I/O,并进行其他调整。
与C和其他语言的系统库调用不同,os/exec包有意不执行系统shell。它不会展开glob模式,也不执行其他扩展、管道和重定向等shell会执行的操作。该包类似于C的exec函数族。为了展开glob模式,直接调用shell并对危险输入进行转义,并使用path/file包的Glob函数。环境变量的展开使用os的ExpandEnv函数。
示例是针对Unix系统设计的。在Windows上不起作用。在Go Playground中也不起作用。
- glob
简单的使用方法
最简单的方法是执行命令,并希望以显示标准输出和错误输出的方式完成任何操作。
exec.Command() 只是用来创建命令并不会执行。只有在调用Output() 方法时才会执行命令,然后返回标准输出。
ls, err := exec.Command("ls").Output()
fmt.Printf("hello ls:\n%s :Error:\n%v\n", ls, err)
结果
hello ls:
go.mod
main.go
返回标准输出和错误输出
然而,如果这样做的话,虽然能知道发生了错误,但错误输出的内容却没有显示出来。因此,使用CombineOutput()函数可以同时返回标准输出和错误输出。如果按照正常方式操作,这个命令是无法执行的,但通过使用sh -c命令可以绕过这个问题。另外,-c之后通常应该用单引号括起来,但是没有括起来的地方很有趣。
ps, err := exec.Command("sh", "-c", "echo stdout; echo stderr 1>&2").CombinedOutput()
fmt.Printf("hello ps -ef :\n%s :Error:\n%v\n", ps, err)
结果
stderr
:Error:
<nil>
获取标准输出和错误输出的读取器。
如果您不仅仅想获取标准输出和错误输出,而且想区分它们,您可以采取以下措施。这个示例实际上没有获取到错误输出,导致调试花费了一些时间 – 在 bash 中是有效的,但在 exec.Command 中却无效。值得一提的是,Windows 的 cmd 也发生了相同的现象。可以想象到是由于引号的转义导致的。因此,我在 StackOverflow 上提问了如何在 Go 语言的 exec.Command 中传递单引号。
cmd := exec.Command("kubectl", "run", "my-release-kafka-client", "--restart='Never'", "--image", "docker.io/bitnami/kafka:2.7.0-debian-10-r68", "--namespace", "default", "--command", "--", "sleep", "infinity")
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
fmt.Printf("Stdout: %s\n", stdout.String())
fmt.Printf("Stderr: %s\n", stderr.String())
} else {
fmt.Printf("Stdout: %s\n", stdout.String())
}
结果
$ go run main.go
Stdout:
Stderr: error: invalid restart policy: 'Never'
See 'kubectl run -h' for help and examples
设置超时时间
如果要编写一个执行命令的程序,并且存在长时间运行的命令,可能希望在一定时间内设置超时。在这种情况下,可以使用exec.CommandContext()。看起来非常方便。
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Microsecond)
defer cancel()
if output, err := exec.CommandContext(ctx, "sleep", "5").CombinedOutput(); err != nil {
fmt.Printf("Exceed the timeout 100ms. %v\n", err)
fmt.Printf("Combine Output: %s\n", output)
}
结果
Exceed the timeout 100ms. signal: killed
Combine Output:
我想要使用管道
当我们想要使用管道时,该怎么办呢?如果随便写,会出现明显的错误。
output, err := exec.Command("ps -ef | grep jvm").CombinedOutput()
fmt.Printf("CombineOutput: %s, Error: %v\n", output, err)
结果
CombineOutput: , Error: exec: "ps -ef | grep jvm": executable file not found in $PATH
这样就完美了
output, err = exec.Command("sh", "-c", "ps -ef | grep jvm").CombinedOutput()
fmt.Printf("CombineOutput: %s, Error: %v\n", output, err)
结果
CombineOutput: ushio 30477 30451 0 01:01 pts/8 00:00:00 sh -c ps -ef | grep jvm
ushio 30479 30477 0 01:01 pts/8 00:00:00 grep jvm
标准错误管道
返回 StdErrorPipe io.ReadCloser。即所谓的流。当然,还有 StdOutPipe 存在。重点在于使用 cmd.Start() 和 cmd.Wait()。Start() 用于执行命令,但不会等待。因此,在 Wait() 中等待。这个 StdErrorPipe 在 Wait() 之前必须执行,因为 Wait() 会关闭 io.ReadCloser。除此之外还有 cmd.Run() 这个命令,它在内部调用了 cmd.Start() 和 cmd.Wait(),是一种始终等待的类型。不过,按照这个逻辑,就不能使用 StdErrorPipe了。
cmd = exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
stdErrorPipe, err := cmd.StderrPipe()
if err != nil {
log.Fatal(err)
}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
slurp, _ := ioutil.ReadAll(stdErrorPipe)
fmt.Printf("stderr: %s\n", slurp)
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
用于调试的 String() 方法
尽管明确写着仅用于调试并不适用于生产环境,但似乎还有一个名为String()的函数。不知为何,它返回了命令的绝对路径。
pwd := exec.Command("ps", "-ef").String()
fmt.Printf("hello ps:\n%s\n", pwd)
结果
hello ps:
/usr/bin/ps -ef
总结
因为基本上覆盖了我想做的事情,所以我应该能够快速编写代码了。