调查 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

总结

因为基本上覆盖了我想做的事情,所以我应该能够快速编写代码了。

广告
将在 10 秒后关闭
bannerAds