Goos/exec使用方式实践

Written by

in

文章目录
  • 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() }
  • 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: %vn”, 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 theren”) // 获取标准输出 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: %vn”, 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: %+vn”, cmd.Environ()) // 设置环境变量 cmd.Env = append(cmd.Environ(), “ENV_VAR=HelloWorld”) log.Printf(“ENV: %+vn”, 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 worldnhi 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
  • 如果你不想使用 os/exec 提供的管道功能,那么在命令中直接使用管道符 |,也可以实现同样功能。 不过此时就需要使用 sh -c 或者 bash -c 等 Shell 命令来解析执行更复杂的命令了: func main() { // 命令中使用了管道 cmd := exec.Command(“bash”, “-c”, “echo ‘hello worldnhi 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 %sn”, 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 %sn”, 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 命令了。
  • 以上为个人经验,希望能给大家一个参考,也希望大家多多支持风君子博客。 您可能感兴趣的文章: Go的os/exec执行超时导致程序死机的解决方案 Golang标准库os/exec执行外部命令并获取其输出包代码示例 详解Go语言中用 os/exec 执行命令的五种方法
  • 目录
    • os/exec 包结构体与方法
    • 使用方法
    • 创建带有 context 的命令
    • 获取命令的输出
    • 获取组合的标准输出和错误输出
    • 设置标准输出和错误输出
    • 使用标准输入传递数据
    • 设置和使用环境变量
    • 使用管道
    • 使用bash -c执行复杂命令
    • 指定工作目录
    • 捕获退出状态
    • 搜索可执行文件
    • 功能练习
    • 总结

    os/exec 是 Go 提供的内置包,可以用来执行外部命令或程序。比如,我们的主机上安装了 redis-server 二进制文件,那么就可以使用 os/exec 在 Go 程序中启动 redis-server 提供服务。当然,我们也可以使用 os/exec 执行 lspwd 等操作系统内置命令。本文不求内容多么深入,旨在带大家极速入门 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 对象。
    • 调用 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()
    }

    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: %vn", 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 theren")
    
    // 获取标准输出
     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: %vn", 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: %+vn", cmd.Environ())
    
    // 设置环境变量
     cmd.Env = append(cmd.Environ(), "ENV_VAR=HelloWorld")
    
     log.Printf("ENV: %+vn", 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 worldnhi 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

    如果你不想使用 os/exec 提供的管道功能,那么在命令中直接使用管道符 |,也可以实现同样功能。

    不过此时就需要使用 sh -c 或者 bash -c 等 Shell 命令来解析执行更复杂的命令了:

    func main() {
     // 命令中使用了管道
     cmd := exec.Command("bash", "-c", "echo 'hello worldnhi 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 %sn", 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 %sn", 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 命令了。

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持风君子博客。

    您可能感兴趣的文章:

    • Go的os/exec执行超时导致程序死机的解决方案
    • Golang标准库os/exec执行外部命令并获取其输出包代码示例
    • 详解Go语言中用 os/exec 执行命令的五种方法

    站内搜索