原文题目:Executing Shell Commands in Golang](https://www.sohamkamani.com/golang/exec-shell-command/))

作者:Soham Kamani

之前本人也写过 os/exec 包执行 Shell 命令的文章,然而没有这篇讲的具体,感兴趣能够看看,点此处。

在本教程中,咱们将学习如何在 Golang 中执行shell命令(如 lsmkdirgrep )。咱们还将学习如何通过 stdinstdout 传递 I/O 到正在运行的命令,以及治理长时间运行的命令。

如果只是想看代码,能够在 Github 上查看

Exec

咱们能够应用官网的 os/exec 包来运行外部命令。

当咱们执行 shell 命令时,咱们是在 Go 应用程序之外运行代码。为此,咱们须要在子过程中运行这些命令。

每个命令都作为正在运行的 Go 应用程序中的子过程运行,并公开咱们能够用来从过程读取和写入数据的 StdinStdout 属性。

运行根本的 Shell 命令

要运行一个简略的命令并读取其输入,咱们能够创立一个新的 *exec.Cmd 实例并运行它。在此示例中,让咱们应用 ls 列出当前目录中的文件,并打印代码的输入:

// 创立了一个新的 *Cmd 实例// 应用 "ls" 命令和 "./" 参数作为参数cmd := exec.Command("ls", "./")// 应用 `Output` 办法执行该命令并收集其输入out, err := cmd.Output()if err != nil {  // 如果执行命令时呈现谬误,则输入错误信息  fmt.Println("could not run command: ", err)}// 否则,输入运行该命令的输入后果fmt.Println("Output: ", string(out))

因为我在示例仓库中运行此代码,因而它会打印我的项目根目录中的文件:

> go run shellcommands/main.goOutput:  LICENSEREADME.mdgo.modshellcommands

请留神,当咱们运行 exec 时,咱们的应用程序不会生成 shell,而是间接运行给定的命令。这意味着将不会执行任何基于 shell 的解决,例如 glob 模式或扩大。

执行长久运行的命令

后面的示例执行了 ls 命令,该命令立刻返回了它的输入。那些输入是间断的,或者须要很长时间能力检索的命令呢?

例如,当咱们运行 ping 命令时,咱们会定期取得间断输入:

➜  ~ ping google.comPING google.com (142.250.77.110): 56 data bytes64 bytes from 142.250.77.110: icmp_seq=0 ttl=116 time=11.397 ms64 bytes from 142.250.77.110: icmp_seq=1 ttl=116 time=17.646 ms  ## this is received after 1 second64 bytes from 142.250.77.110: icmp_seq=2 ttl=116 time=10.036 ms  ## this is received after 2 seconds64 bytes from 142.250.77.110: icmp_seq=3 ttl=116 time=9.656 ms   ## and so on# ...

如果咱们尝试应用 cmd.Output 执行此类型的命令,咱们将不会失去任何输入,因为 Output 办法期待命令执行,而 ping 命令执行工夫有限。

相同,咱们能够应用自定义的 Stdout 属性来间断读取输入:

cmd := exec.Command("ping", "google.com")// pipe the commands output to the applications// standard outputcmd.Stdout = os.Stdout// Run still runs the command and waits for completion// but the output is instantly piped to Stdoutif err := cmd.Run(); err != nil {  fmt.Println("could not run command: ", err)}
这段代码应用 Go 语言的 exec 包来执行 ping 命令并将输入重定向到规范输入流(os.Stdout)。具体来说,它创立了一个命令对象(cmd),该对象蕴含要执行的命令("ping"和"google.com")。而后将命令的规范输入流(cmd.Stdout)设置为应用程序的规范输入流(os.Stdout)。最初,应用 cmd.Run() 办法运行该命令,并期待其实现。如果运行命令时呈现谬误,将在控制台输入错误信息。

输入后果:

> go run shellcommands/main.goPING google.com (142.250.195.142): 56 data bytes64 bytes from 142.250.195.142: icmp_seq=0 ttl=114 time=9.397 ms64 bytes from 142.250.195.142: icmp_seq=1 ttl=114 time=37.398 ms64 bytes from 142.250.195.142: icmp_seq=2 ttl=114 time=34.050 ms64 bytes from 142.250.195.142: icmp_seq=3 ttl=114 time=33.272 ms# ...# and so on

通过间接调配 Stdout 属性,咱们能够捕捉整个命令生命周期的输入,并在收到后立刻解决。

自定义输入写入程序

与应用 os.Stdout 不同,咱们能够创立实现 io.Writer 接口的本人的编写器。

让咱们创立一个编写器,在每个输入块之前增加一个 "received output:" 前缀:

type customOutput struct{}func (c customOutput) Write(p []byte) (int, error) {    fmt.Println("received output: ", string(p))    return len(p), nil}

当初咱们能够指定一个新的 customWriter 实例作为输入写入器:

cmd.Stdout = customOutput{}

如果咱们当初运行应用程序,咱们将失去以下输入:

received output:  PING google.com (142.250.195.142): 56 data bytes64 bytes from 142.250.195.142: icmp_seq=0 ttl=114 time=187.825 msreceived output:  64 bytes from 142.250.195.142: icmp_seq=1 ttl=114 time=19.489 msreceived output:  64 bytes from 142.250.195.142: icmp_seq=2 ttl=114 time=117.676 msreceived output:  64 bytes from 142.250.195.142: icmp_seq=3 ttl=114 time=57.780 ms

应用 STDIN 将输出传递给命令

在后面的示例中,咱们在不提供任何输出(或提供无限的输出作为参数)的状况下执行命令。在大多数状况下,输出是通过 STDIN 流给出的。

译注:就是内部给命令,而后去执行

一个驰名的例子是 grep 命令,咱们能够通过管道从另一个命令输出:

➜  ~ echo "1. pear\n2. grapes\n3. apple\n4. banana\n" | grep apple3. apple

在这里,输出通过 STDIN 传递给 grep 命令。在本例中,输出是一个水果列表,grep 过滤蕴含 " apple" 的行。

Cmd 实例为咱们提供了一个能够写入的输出流。让咱们应用它向 grep 子过程传递输出:

cmd := exec.Command("grep", "apple")// Create a new pipe, which gives us a reader/writer pairreader, writer := io.Pipe()// assign the reader to Stdin for the commandcmd.Stdin = reader// the output is printed to the consolecmd.Stdout = os.Stdoutgo func() {  defer writer.Close()  // the writer is connected to the reader via the pipe  // so all data written here is passed on to the commands  // standard input  writer.Write([]byte("1. pear\n"))  writer.Write([]byte("2. grapes\n"))  writer.Write([]byte("3. apple\n"))  writer.Write([]byte("4. banana\n"))}()if err := cmd.Run(); err != nil {  fmt.Println("could not run command: ", err)}

输入:

3. apple

Kill 一个子过程

有几个命令会无限期地运行,或者须要明确的信号能力进行。

例如,如果咱们应用 python3 -m http.server 启动 Web 服务器或执行 sleep 10000,则生成的子过程将运行很长时间(或有限运行)。

要进行这些过程,咱们须要从应用程序发送终止信号。咱们能够通过向命令增加一个上下文实例来做到这一点。

如果上下文被勾销,命令也会终止。

ctx := context.Background()// The context now times out after 1 second// alternately, we can call `cancel()` to terminate immediatelyctx, cancel = context.WithTimeout(ctx, 1*time.Second)cmd := exec.CommandContext(ctx, "sleep", "100")out, err := cmd.Output()if err != nil {  fmt.Println("could not run command: ", err)}fmt.Println("Output: ", string(out))

这将在 1 秒后给出以下输入:

could not run command:  signal: killedOutput:  

当您想要限度运行命令所破费的工夫或想要创立回退以防命令未按时返回后果时,终止子过程很有用。

总结

到目前为止,咱们学习了多种执行 unix shell 命令并与之交互的办法。应用 os/exec 包时须要留神以下几点:

  • 当您想要执行通常不会提供太多输入的简略命令时,请应用 cmd.Output
  • 对于具备间断或长时间运行输入的函数,您应该应用 cmd.Run 并应用 cmd.Stdoutcmd.Stdin 与命令交互
  • 在生产应用程序中,如果某个过程在给定的工夫内没有响应,那么放弃超时并终止该过程是十分有用的。咱们能够应用上下文勾销发送终止命令。

如果您想理解更多对于不同性能和配置选项的信息,能够查看官网文档页面。

您能够在 Github 上查看所有示例的工作代码。