HOWTO · Go

如何在 Go 中执行 Shell 命令

了解如何使用 os/exec 从 Go 运行外部程序、捕获输出、处理错误、传递参数以及设置命令的工作目录。Shell 功能需要显式调用 shell。

本页内容

若要从 Go 运行外部程序,请使用标准库的 os/exec 包。它直接启动指定的可执行文件;它不会隐式运行 shell。因此,管道、重定向、变量展开和通配符等 shell 功能不会被应用。如果任务需要这些功能,请显式调用 shell,并将其脚本作为参数传递,同时注意不要将不受信任的文本放入该脚本中。

使用 os/exec 运行命令

使用 exec.Command 创建命令,然后调用 Output 等方法来启动它并收集其标准输出。第一个参数指定可执行文件,其余参数会传递给该程序。这个完整的程序会以一个参数将自己启动为子进程。子进程会打印固定的一行,父进程则捕获该标准输出并将其写入自己的标准输出。程序通过父进程打印子进程的那一行。

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	if len(os.Args) > 1 && os.Args[1] == "child" {
		fmt.Println("hello from child")
		return
	}

	cmd := exec.Command(os.Args[0], "child")
	output, err := cmd.Output()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Print(string(output))
}
hello from child

在这里,os.Args[0] 标识当前的可执行文件,因此本示例不依赖于是否安装了单独的实用程序。在应用程序中,将其替换为您需要的可执行文件,例如 git 或 go,并分别提供该程序的参数。

捕获输出并处理错误

在使用结果之前,请检查 Output 或 CombinedOutput 返回的错误。无法启动程序(例如找不到可执行文件)与程序启动后以非零状态结束是不同的。在后一种情况下,错误通常是 *exec.ExitError;当退出状态重要时请检查它。Cmd 类型还可让您直接连接流:当您希望将输出发送到某处而不是收集在字节切片中时,请将 cmd.Stdout 或 cmd.Stderr 设置为写入器。

当标准输出是您的程序所需的数据时,请使用 Output。当您还需要在返回的字节中包含标准错误时,请使用 CombinedOutput;这两个流会被合并,因此它们的相对顺序并不是重建单独流的可靠方法。当诊断需要保持分开时,请为 cmd.Stdout 和 cmd.Stderr 分配单独的写入器。在调用方可以决定是否重试、返回或继续的位置处理并报告错误;打印错误然后将命令视为成功,可能会隐藏失败的操作。

将参数传递给命令

将每个参数作为自己的字符串传递到可执行文件名称之后,例如 exec.Command("grep", "-n", pattern, filename)。Go 不会将这些值连接成 shell 命令,也不会为您解释空格、引号、管道或重定向字符。包含空格的文件名在作为一个字符串提供时仍是一个参数。这可以避免 shell 解析,但不会为被调用的程序验证输入:参数对该程序仍可能具有特殊意义,因此请根据命令自己的接口验证值。

对于输入和输出流,将 io.Reader 分配给 cmd.Stdin,将写入器分配给 cmd.Stdout 或 cmd.Stderr,然后调用 Run。上面的示例可执行文件使用 Output,因为它只需要捕获一个短的结果。当您想要连接流且不需要将标准输出作为返回值收集时,请使用 Run。可执行文件必须可供进程使用,其选项在不同操作系统上可能有所不同。

在不同的目录中运行命令

在启动命令之前设置 cmd.Dir 以选择其工作目录。例如,在构造命令后,设置 cmd.Dir = projectDir,然后调用 Output、CombinedOutput 或 Run。该目录必须存在且进程可以访问。相对的 cmd.Dir 是相对于 Go 进程的当前工作目录来解析的;当调用方的起始目录可能变化时,请使用绝对路径。此设置会更改子进程的目录,不会更改 Go 进程自己的工作目录。