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
- Variables
- func LookPath(file string) (string, error)
-
type Cmd
- func Command(name string, arg ...string) *Cmd
- func CommandContext(ctx context.Context, name string, arg ...string) *Cmd
- func (c *Cmd) CombinedOutput() ([]byte, error)
- func (c *Cmd) Environ() []string
- func (c *Cmd) Output() ([]byte, error)
- func (c *Cmd) Run() error
- func (c *Cmd) Start() error
- func (c *Cmd) StderrPipe() (io.ReadCloser, error)
- func (c *Cmd) StdinPipe() (io.WriteCloser, error)
- func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
- func (c *Cmd) String() string
- func (c *Cmd) Wait() error
- type Error
- type ExitError
Examples
- Cmd.CombinedOutput
- Cmd.Environ
- Cmd.Output
- Cmd.Run
- Cmd.Start
- Cmd.StderrPipe
- Cmd.StdinPipe
- Cmd.StdoutPipe
- Command
- Command (Environment)
- CommandContext
- LookPath
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 的可执行文件, 但以下所述情况除外。
- 在 Windows 上,文件必须具有 PATHEXT 环境变量所指定的扩展名。 当 PATHEXT 未设置时,文件必须具有 ".com"、".exe"、".bat" 或 ".cmd" 扩展名。
- 在 Plan 9 上,LookPath 查询 path 环境变量。 如果 file 以 "/"、"#"、"./" 或 "../" 开头,则直接尝试它, 不查询 path。
- 在 Wasm 上,LookPath 始终返回错误。
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