Go os/exec 使用实践

os/exec 是 Go 提供的内置包,可以用来执行外部命令或程序。比如,我们的主机上安装了 redis-server 二进制文件,那么就可以使用 os/exec 在 Go 程序中启动 redis-server 提供服务。当然,我们也可以使用 os/exec 执行 lspwd 等操作系统内置命令。本文不求内容多么深入,旨在带大家极速入门 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 结构体表示一个准备或正在执行的外部命令。调用函数 CommandCommandContext 可以构造一个 *Cmd 对象。调用 RunStartOutputCombinedOutput 方法可以运行 *Cmd 对象所代表的命令。 调用 Environ 方法可以获取命令执行时的环境变量。调用 StdinPipeStdoutPipeStderrPipe 方法用于获取管道对象。调用 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 对象的 StdoutStderr 属性,重定向标准输出和标准错误输出到当前进程:

复制代码
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 对象即可。

设置和使用环境变量

*CmdEnviron 方法可以获取环境变量,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 对象提供的 StdinPipeStdoutPipeStderrPipe 三个方法用于获取管道对象。故名思义,三者分别对应标准输入、标准输出、标准错误输出的管道对象。

使用示例如下:

复制代码
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(例如 bashsh),让 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 命令了。

相关推荐
kfepiza7 分钟前
Debian编译安装mysql8.0.41源码包 笔记250401
数据库·笔记·mysql·debian·database
tjfsuxyy9 分钟前
SqlServer整库迁移至Oracle
数据库·oracle·sqlserver
nlog3n12 分钟前
Java外观模式详解
java·开发语言·外观模式
方瑾瑜22 分钟前
Visual Basic语言的物联网
开发语言·后端·golang
老王笔记31 分钟前
MySQL统计信息
数据库·mysql
无名之逆1 小时前
[特殊字符] Hyperlane 框架:高性能、灵活、易用的 Rust 微服务解决方案
运维·服务器·开发语言·数据库·后端·微服务·rust
Vitalia1 小时前
⭐算法OJ⭐寻找最短超串【动态规划 + 状态压缩】(C++ 实现)Find the Shortest Superstring
开发语言·c++·算法·动态规划·动态压缩
爱的叹息1 小时前
MongoDB 的详细解析,涵盖其核心概念、架构、功能、操作及应用场景
数据库·mongodb·架构
最后一个bug1 小时前
PCI与PCIe接口的通信架构是主从模式吗?
linux·开发语言·arm开发·stm32·嵌入式硬件
落落鱼20131 小时前
TP6图片操作 Image::open 调用->save()方法时候报错Type is not supported
开发语言