os/exec
是 Go 提供的内置包,可以用来执行外部命令或程序。比如,我们的主机上安装了 redis-server
二进制文件,那么就可以使用 os/exec
在 Go 程序中启动 redis-server
提供服务。当然,我们也可以使用 os/exec
执行 ls
、pwd
等操作系统内置命令。本文不求内容多么深入,旨在带大家极速入门 os/exec
的常规使用。
os/exec 包结构体与方法
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
Cmd
结构体表示一个准备或正在执行的外部命令。调用函数 Command
或 CommandContext
可以构造一个 *Cmd
对象。调用 Run
、Start
、Output
、CombinedOutput
方法可以运行 *Cmd
对象所代表的命令。 调用 Environ
方法可以获取命令执行时的环境变量。调用 StdinPipe
、StdoutPipe
、StderrPipe
方法用于获取管道对象。调用 Wait
方法可以阻塞等待命令执行完成。调用 String
方法返回命令的字符串形式。LookPath
函数用于搜索可执行文件。
使用方法
package main
import (
"log"
"os/exec"
)
func main() {
// 创建一个命令
cmd := exec.Command("echo", "Hello, World!")
// 执行命令并等待命令完成
err := cmd.Run() // 执行后控制台不会有任何输出
if err != nil {
log.Fatalf("Command failed: %v", err)
}
}
exec.Command
函数用于创建一个命令,函数第一个参数是命令的名称,后面跟一个不定常参数作为这个命令的参数,最终会传递给这个命令。
*Cmd.Run
方法会阻塞等待命令执行完成,默认情况下命令执行后控制台不会有任何输出:
# 执行程序
$ go run main.go
# 执行完成后没有任何输出
可以在后台运行一个命令:
func main() {
cmd := exec.Command("sleep", "3")
// 执行命令(非阻塞,不会等待命令执行完成)
if err := cmd.Start(); err != nil {
log.Fatalf("Command start failed: %v", err)
return
}
fmt.Println("Command running in the background...")
// 阻塞等待命令完成
if err := cmd.Wait(); err != nil {
log.Fatalf("Command wait failed: %v", err)
return
}
log.Println("Command finished")
}
实际上 Run
方法就等于 Start
+ Wait
方法,如下是 Run
方法源码的实现:
func (c *Cmd) Run() error {
if err := c.Start(); err != nil {
return err
}
return c.Wait()
}
创建带有 context 的命令
os/exec
还提供了一个 exec.CommandContext
构造函数可以创建一个带有 context
的命令。那么我们就可以利用 context
的特性来控制命令的执行了。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "sleep", "5")
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v\n", err) // signal: killed
}
}
执行示例代码,得到输出如下:
$ go run main.go
2025/01/14 23:54:20 Command failed: signal: killed
exit status 1
当命令执行超时会收到 killed
信号自动取消。
获取命令的输出
无论是调用 *Cmd.Run
还是 *Cmd.Start
方法,默认情况下执行命令后控制台不会得到任何输出。
可以使用 *Cmd.Output
方法来执行命令,以此来获取命令的标准输出:
func main() {
// 创建一个命令
cmd := exec.Command("echo", "Hello, World!")
// 执行命令,并获取命令的输出,Output 内部会调用 Run 方法
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // Hello, World!
}
执行示例代码,得到输出如下:
$ go run main.go
Hello, World!
获取组合的标准输出和错误输出
*Cmd.CombinedOutput
方法能够在运行命令后,返回其组合的标准输出和标准错误输出:
func main() {
// 使用一个命令,既产生标准输出,也产生标准错误输出
cmd := exec.Command("sh", "-c", "echo 'This is stdout'; echo 'This is stderr' >&2")
// 获取 标准输出 + 标准错误输出 组合内容
output, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("Command execution failed: %v", err)
}
// 打印组合输出
fmt.Printf("Combined Output:\n%s", string(output))
}
执行示例代码,得到输出如下:
$ go run main.go
Combined Output:
This is stdout
This is stderr
设置标准输出和错误输出
可以利用 *Cmd
对象的 Stdout
和 Stderr
属性,重定向标准输出和标准错误输出到当前进程:
func main() {
cmd := exec.Command("ls", "-l")
// 设置标准输出和标准错误输出到当前进程,执行后可以在控制台看到命令执行的输出
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
这样,使用 *Cmd.Run
执行命令后控制台就能看到命令执行的输出了。
执行示例代码,得到输出如下:
$ go run main.go
total 4824
-rw-r--r-- 1 jianghushinian staff 12 Jan 4 10:37 demo.log
drwxr-xr-x 3 jianghushinian staff 96 Jan 13 09:41 examples
-rwxr-xr-x 1 jianghushinian staff 2453778 Jan 1 15:09 main
-rw-r--r-- 1 jianghushinian staff 6179 Jan 15 09:13 main.go
使用标准输入传递数据
可以使用 grep
命令接收 stdin
的数据,然后在其中搜索包含指定模式的文本行:
func main() {
cmd := exec.Command("grep", "hello")
// 通过标准输入传递数据给命令
cmd.Stdin = bytes.NewBufferString("hello world!\nhi there\n")
// 获取标准输出
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
return
}
fmt.Println(string(output)) // hello world!
}
可以将一个 io.Reader
对象赋值给 *Cmd.Stdin
属性,来实现将数据通过 stdin
传递给外部命令。
执行示例代码,得到输出如下:
$ go run main.go
hello world!
还可以将打开的文件描述符传给 *Cmd.Stdin
属性:
func main() {
file, err := os.Open("demo.log") // 打开一个文件
if err != nil {
log.Fatalf("Open file failed: %v\n", err)
return
}
defer file.Close()
cmd := exec.Command("cat")
cmd.Stdin = file // 将文件作为 cat 的标准输入
cmd.Stdout = os.Stdout // 获取标准输出
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
只要是 io.Reader
对象即可。
设置和使用环境变量
*Cmd
的 Environ
方法可以获取环境变量,Env
属性则可以设置环境变量:
func main() {
cmd := exec.Command("printenv", "ENV_VAR")
log.Printf("ENV: %+v\n", cmd.Environ())
// 设置环境变量
cmd.Env = append(cmd.Environ(), "ENV_VAR=HelloWorld")
log.Printf("ENV: %+v\n", cmd.Environ())
// 获取输出
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // HelloWorld
}
这段代码输出结果与执行环境相关,此处不演示执行结果了,你可以自行尝试。
不过最终的 output
输出结果一定是 HelloWorld
。
使用管道
os/exec
支持管道功能,*Cmd
对象提供的 StdinPipe
、StdoutPipe
、StderrPipe
三个方法用于获取管道对象。故名思义,三者分别对应标准输入、标准输出、标准错误输出的管道对象。
使用示例如下:
func main() {
// 命令中使用了管道
cmdEcho := exec.Command("echo", "hello world\nhi there")
outPipe, err := cmdEcho.StdoutPipe()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
// 注意,这里不能使用 Run 方法阻塞等待,应该使用非阻塞的 Start 方法
if err := cmdEcho.Start(); err != nil {
log.Fatalf("Command failed: %v", err)
}
cmdGrep := exec.Command("grep", "hello")
cmdGrep.Stdin = outPipe
output, err := cmdGrep.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // hello world
}
首先创建一个用于执行 echo
命令的 *Cmd
对象 cmdEcho
,并调用它的 StdoutPipe
方法获得标准输出管道对象 outPipe
;然后调用 Start
方法非阻塞的方式执行 echo
命令;接着创建一个用于执行 grep
命令的 *Cmd
对象 cmdGrep
,将 cmdEcho
的标准输出管道对象赋值给 cmdGrep.Stdin
作为标准输入,这样,两个命令就通过管道串联起来了;最终通过 cmdGrep.Output
方法拿到 cmdGrep
命令的标准输出。
执行示例代码,得到输出如下:
$ go run main.go
hello world
使用 bash -c
执行复杂命令
如果你不想使用 os/exec
提供的管道功能,那么在命令中直接使用管道符 |
,也可以实现同样功能。
不过此时就需要使用 sh -c
或者 bash -c
等 Shell 命令来解析执行更复杂的命令了:
func main() {
// 命令中使用了管道
cmd := exec.Command("bash", "-c", "echo 'hello world\nhi there' | grep hello")
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // hello world
}
这段代码中的管道功能同样生效。
指定工作目录
可以通过指定 *Cmd
对象的的 Dir
属性来指定工作目录:
func main() {
cmd := exec.Command("cat", "demo.log")
cmd.Stdout = os.Stdout // 获取标准输出
cmd.Stderr = os.Stderr // 获取错误输出
// cmd.Dir = "/tmp" // 指定绝对目录
cmd.Dir = "." // 指定相对目录
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
捕获退出状态
上面讲解了很多执行命令相关操作,但其实还有一个很重要的点没有讲到,就是如何捕获外部命令执行后的退出状态码:
func main() {
// 查看一个不存在的目录
cmd := exec.Command("ls", "/nonexistent")
// 运行命令
err := cmd.Run()
// 检查退出状态
var exitError *exec.ExitError
if errors.As(err, &exitError) {
log.Fatalf("Process PID: %d exit code: %d", exitError.Pid(), exitError.ExitCode()) // 打印 pid 和退出码
}
}
这里执行 ls
命令来查看一个不存在的目录 /nonexistent
,程序退出状态码必然不为 0
。
执行示例代码,得到输出如下:
$ go run main.go
2025/01/15 23:31:44 Process PID: 78328 exit code: 1
exit status 1
搜索可执行文件
最后要介绍的函数就只剩一个 LookPath
了,它用来搜索可执行文件。
搜索一个存在的命令:
func main() {
path, err := exec.LookPath("ls")
if err != nil {
log.Fatal("installing ls is in your future")
}
fmt.Printf("ls is available at %s\n", path)
}
执行示例代码,得到输出如下:
$ go run main.go
ls is available at /bin/ls
搜索一个不存在的命令:
func main() {
path, err := exec.LookPath("lsx")
if err != nil {
log.Fatal(err)
}
fmt.Printf("ls is available at %s\n", path)
}
执行示例代码,得到输出如下:
$ go run main.go
2025/01/15 23:37:45 exec: "lsx": executable file not found in $PATH
exit status 1
功能练习
介绍完了 os/exec
常用的方法和函数,我们现在来做一个小练习,使用 os/exec
来执行外部命令 ls -l /var/log/*.log
。
示例如下:
func main() {
cmd := exec.Command("ls", "-l", "/var/log/*.log")
output, err := cmd.CombinedOutput() // 获取标准输出和错误输出
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output))
}
执行示例代码,得到输出如下:
$ go run main.go
2025/01/16 09:15:52 Command failed: exit status 1
exit status 1
执行报错了,这里的错误码为 1
,但错误信息并不明确。
这个报错其实是因为 os/exec
默认不支持通配符参数导致的,exec.Command
不支持直接在参数中使用 Shell 通配符(如 *
),因为它不会通过 Shell 来解析命令,而是直接调用底层的程序。
要解决这个问题,可以通过显式调用 Shell(例如 bash
或 sh
),让 Shell 来解析通配符。
比如使用 bash -c
执行通配符命令 ls -l /var/log/*.log
:
func main() {
// 使用 bash -c 来解析通配符
cmd := exec.Command("bash", "-c", "ls -l /var/log/*.log")
output, err := cmd.CombinedOutput() // 获取标准输出和错误输出
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output))
}
执行示例代码,得到输出如下:
$ go run main.go
-rw-r--r-- 1 root wheel 0 Oct 7 21:20 /var/log/alf.log
-rw-r--r-- 1 root wheel 11936 Jan 13 11:36 /var/log/fsck_apfs.log
-rw-r--r-- 1 root wheel 334 Jan 13 11:36 /var/log/fsck_apfs_error.log
-rw-r--r-- 1 root wheel 19506 Jan 11 18:04 /var/log/fsck_hfs.log
-rw-r--r--@ 1 root wheel 21015342 Jan 16 09:02 /var/log/install.log
-rw-r--r-- 1 root wheel 1502 Nov 5 09:44 /var/log/shutdown_monitor.log
-rw-r-----@ 1 root admin 3779 Jan 16 08:59 /var/log/system.log
-rw-r----- 1 root admin 187332 Jan 16 09:05 /var/log/wifi.log
此外,我们还可以用 Go 标准库提供的 filepath.Glob
来手动解析通配符:
func main() {
// 匹配通配符路径
files, err := filepath.Glob("/var/log/*.log")
if err != nil {
log.Fatalf("Glob failed: %v", err)
}
if len(files) == 0 {
log.Println("No matching files found")
return
}
// 将匹配到的文件传给 ls 命令
args := append([]string{"-l"}, files...)
cmd := exec.Command("ls", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
filepath.Glob
函数会返回模式匹配的文件名列表,如果不匹配则返回 nil
。这样,我们就可以先解析文件名列表,再交给 exec.Command
来执行 ls
命令了。