package exec

import "os/exec"

Package exec 运行外部命令。它封装了 os.StartProcess,使重新映射 stdin 和 stdout、用管道连接 I/O 以及进行其他调整变得更加容易。

与 C 及其他语言中的 "system" 库调用不同,os/exec 包有意不调用 系统 shell,也不展开任何 glob 模式,或处理通常由 shell 完成的 其他扩展、管道或重定向。该包的行为更像 C 的 "exec" 系列函数。 要展开 glob 模式,可以直接调用 shell(注意转义任何危险输入), 或者使用 path/filepath 包的 Glob 函数。要展开环境变量, 请使用包 os 的 ExpandEnv。

注意,本包中的示例假定使用 Unix 系统。它们可能无法在 Windows 上 运行,也无法在 go.dev 和 pkg.go.dev 所使用的 Go Playground 中运行。

当前目录中的可执行文件

函数 Command 和 LookPath 会按照宿主操作系统的约定,在当前路径 所列出的目录中查找程序。数十年来,操作系统一直将当前目录包含在 这一搜索中,有时是隐式包含,有时则是通过默认配置显式地如此设置。 现代实践认为,包含当前目录通常是出乎意料的,并且往往会导致安全问题。

为了避免这些安全问题,自 Go 1.19 起,本包不会再使用相对于当前目录的 隐式或显式路径条目来解析程序。也就是说,如果你运行 LookPath("go"), 无论在 Unix 上返回 ./go 还是在 Windows 上返回 .\go.exe,都不会成功, 无论路径如何配置。相反,如果通常的路径算法会得出那样的结果,这些函数 会返回一个满足 errors.Is(err, ErrDot) 的错误 err。

例如,考虑以下两个程序片段:

path, err := exec.LookPath("prog")
if err != nil {
	log.Fatal(err)
}
use(path)

以及

cmd := exec.Command("prog")
if err := cmd.Run(); err != nil {
	log.Fatal(err)
}

无论当前路径如何配置,它们都不会找到并运行 ./prog 或 .\prog.exe。

如果代码总是想从当前目录运行程序,可以将其改写为 使用 "./prog" 而不是 "prog"。

如果代码坚持要包含来自相对路径条目的结果, 则可以用 errors.Is 检查来覆盖该错误:

path, err := exec.LookPath("prog")
if errors.Is(err, exec.ErrDot) {
	err = nil
}
if err != nil {
	log.Fatal(err)
}
use(path)

以及

cmd := exec.Command("prog")
if errors.Is(cmd.Err, exec.ErrDot) {
	cmd.Err = nil
}
if err := cmd.Run(); err != nil {
	log.Fatal(err)
}

设置环境变量 GODEBUG=execerrdot=0 可以完全禁用 ErrDot 的生成, 为无法应用更有针对性修复的程序临时恢复 Go 1.19 之前的行为。 Go 的未来版本可能会移除对该变量的支持。

在添加此类覆盖之前,请务必理解这样做的安全影响。 更多信息参见 https://go.dev/blog/path-security。

Index

Examples

Variables

var ErrDot = errors.New("cannot run executable found relative to current directory")

ErrDot 表示由于路径中存在 ‘.’(无论是隐式还是显式),路径查找解析到了 当前目录中的可执行文件。详情参见包文档。

注意,本包中的函数不会直接返回 ErrDot。 代码应使用 errors.Is(err, ErrDot) 而不是 err == ErrDot, 来测试返回的错误 err 是否由该情况引起。

var ErrNotFound = errors.New("executable file not found in $PATH")

ErrNotFound 是路径搜索未能找到可执行文件时产生的错误。

var ErrWaitDelay = errors.New("exec: WaitDelay expired before I/O complete")

如果进程以成功的状态码退出,但其输出管道在命令的 WaitDelay 到期之前 未关闭,Cmd.Wait 会返回 ErrWaitDelay。

Functions

func LookPath

func LookPath(file string) (string, error)

LookPath 按照宿主操作系统的约定,在当前路径中搜索名为 file 的可执行文件。 如果 file 包含斜杠,则直接尝试它,不查询默认路径。 否则,成功时结果为绝对路径。

如果解析出的路径相对于当前目录,LookPath 会返回一个满足 errors.Is(err, ErrDot) 的错误。详情参见包文档。

LookPath 在 PATH 环境变量所指定的目录中查找名为 file 的可执行文件, 但以下所述情况除外。

Example
package main

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

func main() {
	path, err := exec.LookPath("fortune")
	if err != nil {
		log.Fatal("installing fortune is in your future")
	}
	fmt.Printf("fortune is available at %s\n", path)
}

Types

type Cmd

type Cmd struct {
	// Path 是要运行的命令的路径。
	//
	// 这是唯一必须设置为非零值的字段。如果 Path 是相对路径,
	// 则相对于 Dir 求值。
	Path string

	// Args 保存命令行参数,其中 Args[0] 是命令本身。
	// 如果 Args 字段为空或为 nil,Run 使用 {Path}。
	//
	// 在典型用法中,Path 和 Args 都通过调用 Command 来设置。
	Args []string

	// Env 指定进程的环境。每一项的形式为 "key=value"。
	// 如果 Env 为 nil,新进程使用当前进程的环境。
	// 如果 Env 包含重复的环境键,则每个重复键只使用切片中的最后一个值。
	// 在 Windows 上有一个特殊情况:如果缺少 SYSTEMROOT,且它没有被显式
	// 设置为空字符串,则会始终添加 SYSTEMROOT。
	//
	// 另请参见 Dir 字段,它可能会设置环境中的 PWD。
	Env []string

	// Dir 指定命令的工作目录。
	// 如果 Dir 为空字符串,Run 在调用进程的当前目录中运行命令。
	//
	// 在 Unix 系统上,如果未另行指定,Dir 的值还会决定子进程的 PWD
	// 环境变量。Unix 进程并不以名称来表示其工作目录,而是以对文件树中
	// 某个节点的隐式引用来表示。因此,如果子进程通过调用诸如 C 的 getcwd
	// 之类的函数(该函数通过向上遍历文件树来计算规范名称)来获取其工作
	// 目录,那么当 Dir 的值是涉及符号链接的别名时,它将无法还原 Dir 的
	// 原始值。然而,如果子进程调用 Go 的 [os.Getwd] 或 GNU C 的
	// get_current_dir_name,并且 PWD 的值是当前目录的别名,那么这些函数
	// 将返回 PWD 的值,该值与 Dir 的值相匹配。
	Dir string

	// Stdin 指定进程的标准输入。
	//
	// 如果 Stdin 为 nil,进程从空设备(os.DevNull)读取。
	//
	// 如果 Stdin 是一个 *os.File,进程的标准输入会直接连接到该文件。
	//
	// 否则,在命令执行期间,会有一个单独的 goroutine 从 Stdin 读取数据,
	// 并通过管道将其传递给命令。在这种情况下,Wait 要等到该 goroutine
	// 停止复制才会完成,这可能是因为它已到达 Stdin 的末尾(EOF 或读取
	// 错误),或者因为向管道写入返回了错误,或者因为设置了非零的 WaitDelay
	// 并已到期。
	Stdin io.Reader

	// Stdout 和 Stderr 指定进程的标准输出和标准错误。
	//
	// 如果其中任何一个为 nil,Run 会将对应的文件描述符连接到空设备
	// (os.DevNull)。
	//
	// 如果其中任何一个是 *os.File,进程对应的输出会直接连接到该文件。
	//
	// 否则,在命令执行期间,会有一个单独的 goroutine 通过管道从进程读取数据,
	// 并将其传递给对应的 Writer。在这种情况下,Wait 要等到该 goroutine
	// 到达 EOF、遇到错误或非零的 WaitDelay 到期才会完成。
	//
	// 如果 Stdout 和 Stderr 是同一个 writer,并且其类型可以用 == 比较,
	// 那么同一时刻至多只有一个 goroutine 会调用 Write。
	Stdout io.Writer
	Stderr io.Writer

	// ExtraFiles 指定由新进程继承的额外打开文件。它不包括标准输入、
	// 标准输出或标准错误。如果非 nil,第 i 项会成为文件描述符 3+i。
	//
	// ExtraFiles 在 Windows 上不受支持。
	ExtraFiles []*os.File

	// SysProcAttr 保存可选的、特定于操作系统的属性。
	// Run 会将其作为 os.ProcAttr 的 Sys 字段传递给 os.StartProcess。
	SysProcAttr *syscall.SysProcAttr

	// Process 是底层的进程,在启动之后才存在。
	Process *os.Process

	// ProcessState 包含已退出进程的信息。
	// 如果进程启动成功,Wait 或 Run 会在命令完成时填充其 ProcessState。
	ProcessState *os.ProcessState

	Err error // LookPath 错误(如果有)。

	// 如果 Cancel 非 nil,则命令必须是通过 CommandContext 创建的,
	// 并且当命令的 Context 完成时,会调用 Cancel。默认情况下,
	// CommandContext 将 Cancel 设置为调用命令 Process 的 Kill 方法。
	//
	// 通常,自定义的 Cancel 会向命令的 Process 发送信号,但它也可以
	// 采取其他操作来发起取消,例如关闭 stdin 或 stdout 管道,或在网络
	// 套接字上发送关闭请求。
	//
	// 如果在调用 Cancel 之后命令以成功状态退出,并且 Cancel 返回的错误
	// 不等价于 os.ErrProcessDone,则 Wait 及类似方法会返回一个非 nil
	// 错误:要么是包装了 Cancel 所返回错误的错误,要么是来自 Context 的错误。
	// (如果命令以非成功状态退出,或者 Cancel 返回的错误包装了
	// os.ErrProcessDone,则 Wait 及类似方法会继续返回命令通常的退出状态。)
	//
	// 如果 Cancel 设置为 nil,当命令的 Context 完成时不会立即发生任何事情,
	// 但非零的 WaitDelay 仍会生效。例如,这对于绕过那些不支持关闭信号
	// 但预期总能快速完成的命令中的死锁可能会很有用。
	//
	// 如果 Start 返回非 nil 错误,则不会调用 Cancel。
	Cancel func() error

	// 如果 WaitDelay 非零,它会限制等待 Wait 中两个意外延迟来源所花费的
	// 时间:在关联的 Context 被取消后未能退出的子进程,以及已退出但未关闭
	// 其 I/O 管道的子进程。
	//
	// WaitDelay 计时器在关联的 Context 完成时启动,或者在调用 Wait 观察到
	// 子进程已退出时启动,以先发生者为准。当延迟耗尽时,命令会关闭子进程
	// 和/或其 I/O 管道。
	//
	// 如果子进程未能退出 —— 也许是因为它忽略了或未能收到来自 Cancel 函数的
	// 关闭信号,或者因为没有设置 Cancel 函数 —— 那么它将使用
	// os.Process.Kill 被终止。
	//
	// 然后,如果与子进程通信的 I/O 管道仍然打开,则会关闭这些管道,
	// 以解除当前阻塞在 Read 或 Write 调用上的任何 goroutine。
	//
	// 如果管道因 WaitDelay 而被关闭,没有发生 Cancel 调用,并且命令以其他
	// 方式以成功状态退出,则 Wait 及类似方法会返回 ErrWaitDelay 而不是 nil。
	//
	// 如果 WaitDelay 为零(默认值),I/O 管道会被读取直到 EOF,而在命令
	// 成为孤儿的子进程也关闭其管道描述符之前,EOF 可能不会出现。
	WaitDelay time.Duration
	// contains filtered or unexported fields
}

Cmd 表示一个正在准备或运行的外部命令。

在调用 Cmd 的 Cmd.Start、Cmd.Run、Cmd.Output 或 Cmd.CombinedOutput 方法之后,Cmd 不能被重用。

func Command
func Command(name string, arg ...string) *Cmd

Command 返回 Cmd 结构体,用于以给定的参数执行指定的程序。

它在返回的结构体中只设置 Path 和 Args。

如果 name 不包含路径分隔符,Command 会使用 LookPath 在可能的情况下 将 name 解析为完整路径。否则,它直接使用 name 作为 Path。

返回的 Cmd 的 Args 字段由命令名后跟 arg 的元素构成,因此 arg 不应包含 命令名本身。例如,Command("echo", "hello")。 Args[0] 始终是 name,而不是可能解析后的 Path。

在 Windows 上,进程将整条命令行作为单个字符串接收,并自行进行解析。 Command 会将 Args 组合并加引号,形成一条命令行字符串,其算法与使用 CommandLineToArgvW 的应用程序兼容(这是最常见的方式)。值得注意的例外 是 msiexec.exe 和 cmd.exe(以及由此而来的所有批处理文件),它们使用了 不同的去引号算法。在这些或其他类似情况下,你可以自行加引号,并在 SysProcAttr.CmdLine 中提供完整的命令行,同时让 Args 保持为空。

Example
package main

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

func main() {
	cmd := exec.Command("tr", "a-z", "A-Z")
	cmd.Stdin = strings.NewReader("some input")
	var out strings.Builder
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("in all caps: %q\n", out.String())
}
Example (Environment)
package main

import (
	"log"
	"os"
	"os/exec"
)

func main() {
	cmd := exec.Command("prog")
	cmd.Env = append(os.Environ(),
		"FOO=duplicate_value", // ignored
		"FOO=actual_value",    // this value is used
	)
	if err := cmd.Run(); err != nil {
		log.Fatal(err)
	}
}
func CommandContext
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd

CommandContext 类似 Command,但包含一个 context。

如果所提供的 context 在命令自行完成之前变为已完成状态, 则该 context 会被用于中断进程(通过调用 cmd.Cancel 或 os.Process.Kill)。

CommandContext 将命令的 Cancel 函数设置为调用其 Process 的 Kill 方法, 并让其 WaitDelay 保持未设置。调用者可以在启动命令之前修改这些字段, 以更改取消行为。

Example
package main

import (
	"context"
	"os/exec"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil {
		// This will fail after 100 milliseconds. The 5 second sleep
		// will be interrupted.
	}
}
func (*Cmd) CombinedOutput
func (c *Cmd) CombinedOutput() ([]byte, error)

CombinedOutput 运行命令并返回其合并后的标准输出和标准错误。

Example
package main

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

func main() {
	cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
	stdoutStderr, err := cmd.CombinedOutput()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", stdoutStderr)
}
func (*Cmd) Environ
func (c *Cmd) Environ() []string

Environ 返回命令在当前配置下运行时所处环境的副本。

Example
package main

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

func main() {
	cmd := exec.Command("pwd")

	// Set Dir before calling cmd.Environ so that it will include an
	// updated PWD variable (on platforms where that is used).
	cmd.Dir = ".."
	cmd.Env = append(cmd.Environ(), "POSIXLY_CORRECT=1")

	out, err := cmd.Output()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", out)
}
func (*Cmd) Output
func (c *Cmd) Output() ([]byte, error)

Output 运行命令并返回其标准输出。 返回的任何错误通常都是 *ExitError 类型。 如果 c.Stderr 为 nil 且返回的错误是 *ExitError 类型, Output 会填充所返回错误的 Stderr 字段。

Example
package main

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

func main() {
	out, err := exec.Command("date").Output()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("The date is %s\n", out)
}
func (*Cmd) Run
func (c *Cmd) Run() error

Run 启动指定的命令并等待其完成。

如果命令能够运行,复制 stdin、stdout 和 stderr 没有问题, 并且以零退出状态退出,则返回的错误为 nil。

如果命令启动了但未能成功完成,则错误类型为 *ExitError。 其他情况下可能返回其他错误类型。

如果调用 goroutine 已使用 runtime.LockOSThread 锁定了操作系统线程, 并修改了任何可继承的操作系统级线程状态(例如 Linux 或 Plan 9 的 命名空间),则新进程将继承调用者的线程状态。

Example
package main

import (
	"log"
	"os/exec"
)

func main() {
	cmd := exec.Command("sleep", "1")
	log.Printf("Running command and waiting for it to finish...")
	err := cmd.Run()
	log.Printf("Command finished with error: %v", err)
}
func (*Cmd) Start
func (c *Cmd) Start() error

Start 启动指定的命令,但不等待其完成。

如果 Start 成功返回,则 c.Process 字段会被设置。

成功调用 Start 之后,必须调用 Cmd.Wait 方法以释放相关的系统资源。

Example
package main

import (
	"log"
	"os/exec"
)

func main() {
	cmd := exec.Command("sleep", "5")
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Waiting for command to finish...")
	err = cmd.Wait()
	log.Printf("Command finished with error: %v", err)
}
func (*Cmd) StderrPipe
func (c *Cmd) StderrPipe() (io.ReadCloser, error)

StderrPipe 返回一个管道,命令启动时该管道会连接到命令的标准错误。

Cmd.Wait 会在看到命令退出后关闭该管道,因此大多数调用者无需自行关闭它。 因此,在从管道读取的所有操作完成之前调用 Wait 是不正确的。 同理,在使用 StderrPipe 时使用 Cmd.Run 也是不正确的。 惯用法参见 StdoutPipe 的示例。

Example
package main

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

func main() {
	cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
	stderr, err := cmd.StderrPipe()
	if err != nil {
		log.Fatal(err)
	}

	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}

	slurp, _ := io.ReadAll(stderr)
	fmt.Printf("%s\n", slurp)

	if err := cmd.Wait(); err != nil {
		log.Fatal(err)
	}
}
func (*Cmd) StdinPipe
func (c *Cmd) StdinPipe() (io.WriteCloser, error)

StdinPipe 返回一个管道,命令启动时该管道会连接到命令的标准输入。 在 Cmd.Wait 看到命令退出后,该管道会自动关闭。 调用者只需调用 Close 即可让管道更早关闭。 例如,如果要运行的命令在标准输入关闭之前不会退出, 调用者必须关闭该管道。

Example
package main

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

func main() {
	cmd := exec.Command("cat")
	stdin, err := cmd.StdinPipe()
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		defer stdin.Close()
		io.WriteString(stdin, "values written to stdin are passed to cmd's standard input")
	}()

	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s\n", out)
}
func (*Cmd) StdoutPipe
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)

StdoutPipe 返回一个管道,命令启动时该管道会连接到命令的标准输出。

Cmd.Wait 会在看到命令退出后关闭该管道,因此大多数调用者无需自行关闭它。 因此,在从管道读取的所有操作完成之前调用 Wait 是不正确的。 同理,在使用 StdoutPipe 时调用 Cmd.Run 也是不正确的。 惯用法参见示例。

Example
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os/exec"
)

func main() {
	cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}
	var person struct {
		Name string
		Age  int
	}
	if err := json.NewDecoder(stdout).Decode(&person); err != nil {
		log.Fatal(err)
	}
	if err := cmd.Wait(); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s is %d years old\n", person.Name, person.Age)
}
func (*Cmd) String
func (c *Cmd) String() string

String 返回 c 的便于阅读的描述。 它仅用于调试。 特别地,它不适合用作 shell 的输入。 String 的输出可能因 Go 版本而异。

func (*Cmd) Wait
func (c *Cmd) Wait() error

Wait 等待命令退出,并等待向 stdin 复制或从 stdout 或 stderr 复制的 任何操作完成。

命令必须已由 Cmd.Start 启动。

如果命令能够运行,复制 stdin、stdout 和 stderr 没有问题, 并且以零退出状态退出,则返回的错误为 nil。

如果命令无法运行或未能成功完成,则错误类型为 *ExitError。 对于 I/O 问题,可能返回其他错误类型。

如果 c.Stdin、c.Stdout 或 c.Stderr 中的任何一个不是 *os.File, Wait 还会等待向进程复制或从进程复制的相应 I/O 循环完成。

Wait 会释放与 Cmd 关联的任何资源。

type Error

type Error struct {
	// Name 是发生错误的文件名。
	Name string
	// Err 是底层错误。
	Err error
}

Error 在 LookPath 无法将某个文件归类为可执行文件时返回。

func (*Error) Error
func (e *Error) Error() string
func (*Error) Unwrap
func (e *Error) Unwrap() error

type ExitError

type ExitError struct {
	*os.ProcessState

	// Stderr 保存来自 Cmd.Output 方法的标准错误输出的一个子集,
	// 前提是标准错误没有以其他方式被收集。
	//
	// 如果错误输出很长,Stderr 可能只包含输出的前缀和后缀,
	// 中间部分会被替换为关于省略字节数的文本。
	//
	// Stderr 供调试使用,用于包含在错误消息中。
	// 有其他需求的用户应按需重定向 Cmd.Stderr。
	Stderr []byte
}

ExitError 报告命令未成功退出。

func (*ExitError) Error
func (e *ExitError) Error() string