Go os/exec 使用实践

打印 上一主题 下一主题

主题 1020|帖子 1020|积分 3060

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
os/exec 是 Go 提供的内置包,可以用来实行外部下令或程序。比如,我们的主机上安装了 redis-server 二进制文件,那么就可以使用 os/exec 在 Go 程序中启动 redis-server 提供服务。固然,我们也可以使用 os/exec 实行 ls、pwd 等操纵体系内置下令。本文不求内容多么深入,旨在带大家极速入门 os/exec 的通例使用。
os/exec 包结构体与方法

  1. func LookPath(file string) (string, error)
  2. type Cmd
  3.     func Command(name string, arg ...string) *Cmd
  4.     func CommandContext(ctx context.Context, name string, arg ...string) *Cmd
  5.     func (c *Cmd) CombinedOutput() ([]byte, error)
  6.     func (c *Cmd) Environ() []string
  7.     func (c *Cmd) Output() ([]byte, error)
  8.     func (c *Cmd) Run() error
  9.     func (c *Cmd) Start() error
  10.     func (c *Cmd) StderrPipe() (io.ReadCloser, error)
  11.     func (c *Cmd) StdinPipe() (io.WriteCloser, error)
  12.     func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
  13.     func (c *Cmd) String() string
  14.     func (c *Cmd) Wait() error
复制代码
Cmd 结构体表现一个预备或正在实行的外部下令。调用函数 Command 或 CommandContext 可以构造一个 *Cmd 对象。调用 Run、Start、Output、CombinedOutput 方法可以运行 *Cmd 对象所代表的下令。 调用 Environ 方法可以获取下令实行时的情况变量。调用 StdinPipe、StdoutPipe、StderrPipe 方法用于获取管道对象。调用 Wait 方法可以阻塞等候下令实行完成。调用 String 方法返回下令的字符串形式。LookPath 函数用于搜索可实行文件。
使用方法

  1. package main
  2. import (
  3. "log"
  4. "os/exec"
  5. )
  6. func main() {
  7. // 创建一个命令
  8. cmd := exec.Command("echo", "Hello, World!")
  9. // 执行命令并等待命令完成
  10. err := cmd.Run() // 执行后控制台不会有任何输出
  11. if err != nil {
  12.   log.Fatalf("Command failed: %v", err)
  13. }
  14. }
复制代码
exec.Command 函数用于创建一个下令,函数第一个参数是下令的名称,后面跟一个不定常参数作为这个下令的参数,最终会通报给这个下令。
*Cmd.Run 方法会阻塞等候下令实行完成,默认情况下下令实行后控制台不会有任何输出:
  1. # 执行程序
  2. $ go run main.go
  3. # 执行完成后没有任何输出
复制代码
可以在背景运行一个下令:
  1. func main() {
  2.  cmd := exec.Command("sleep", "3")
  3. // 执行命令(非阻塞,不会等待命令执行完成)
  4. if err := cmd.Start(); err != nil {
  5.   log.Fatalf("Command start failed: %v", err)
  6.   return
  7.  }
  8.  fmt.Println("Command running in the background...")
  9. // 阻塞等待命令完成
  10. if err := cmd.Wait(); err != nil {
  11.   log.Fatalf("Command wait failed: %v", err)
  12.   return
  13.  }
  14.  log.Println("Command finished")
  15. }
复制代码
实际上 Run 方法就等于 Start + Wait 方法,如下是 Run 方法源码的实现:
  1. func (c *Cmd) Run() error {
  2.  if err := c.Start(); err != nil {
  3.   return err
  4.  }
  5.  return c.Wait()
  6. }
复制代码
创建带有 context 的下令

os/exec 还提供了一个 exec.CommandContext 构造函数可以创建一个带有 context 的下令。那么我们就可以使用 context 的特性来控制下令的实行了。
  1. func main() {
  2.  ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
  3.  defer cancel()
  4.  cmd := exec.CommandContext(ctx, "sleep", "5")
  5.  if err := cmd.Run(); err != nil {
  6.   log.Fatalf("Command failed: %v\n", err) // signal: killed
  7.  }
  8. }
复制代码
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. 2025/01/14 23:54:20 Command failed: signal: killed
  3. exit status 1
复制代码
当下令实行超时会收到 killed 信号自动取消。
获取下令的输出

无论是调用 *Cmd.Run 照旧 *Cmd.Start 方法,默认情况下实行下令后控制台不会得到任何输出。
可以使用 *Cmd.Output 方法来实行下令,以此来获取下令的尺度输出:
  1. func main() {
  2.  // 创建一个命令
  3.  cmd := exec.Command("echo", "Hello, World!")
  4.  // 执行命令,并获取命令的输出,Output 内部会调用 Run 方法
  5.  output, err := cmd.Output()
  6.  if err != nil {
  7.   log.Fatalf("Command failed: %v", err)
  8.  }
  9.  fmt.Println(string(output)) // Hello, World!
  10. }
复制代码
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. Hello, World!
复制代码
获取组合的尺度输出和错误输出

*Cmd.CombinedOutput 方法可以或许在运行下令后,返回其组合的尺度输出和尺度错误输出:
  1. func main() {
  2. // 使用一个命令,既产生标准输出,也产生标准错误输出
  3.  cmd := exec.Command("sh", "-c", "echo 'This is stdout'; echo 'This is stderr' >&2")
  4. // 获取 标准输出 + 标准错误输出 组合内容
  5.  output, err := cmd.CombinedOutput()
  6.  if err != nil {
  7.   log.Fatalf("Command execution failed: %v", err)
  8.  }
  9. // 打印组合输出
  10.  fmt.Printf("Combined Output:\n%s", string(output))
  11. }
复制代码
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. Combined Output:
  3. This is stdout
  4. This is stderr
复制代码
设置尺度输出和错误输出

可以使用 *Cmd 对象的 Stdout 和 Stderr 属性,重定向尺度输出和尺度错误输出到当进步程:
  1. func main() {
  2.  cmd := exec.Command("ls", "-l")
  3.  // 设置标准输出和标准错误输出到当前进程,执行后可以在控制台看到命令执行的输出
  4.  cmd.Stdout = os.Stdout
  5.  cmd.Stderr = os.Stderr
  6.  if err := cmd.Run(); err != nil {
  7.   log.Fatalf("Command failed: %v", err)
  8.  }
  9. }
复制代码
如许,使用 *Cmd.Run 实行下令后控制台就能看到下令实行的输出了。
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. total 4824
  3. -rw-r--r--  1 jianghushinian  staff       12 Jan  4 10:37 demo.log
  4. drwxr-xr-x  3 jianghushinian  staff       96 Jan 13 09:41 examples
  5. -rwxr-xr-x  1 jianghushinian  staff  2453778 Jan  1 15:09 main
  6. -rw-r--r--  1 jianghushinian  staff     6179 Jan 15 09:13 main.go
复制代码
使用尺度输入通报数据 

可以使用 grep 下令接收 stdin 的数据,然后在其中搜索包罗指定模式的文本行:

  1. func main() {
  2.  cmd := exec.Command("grep", "hello")
  3. // 通过标准输入传递数据给命令
  4.  cmd.Stdin = bytes.NewBufferString("hello world!\nhi there\n")
  5. // 获取标准输出
  6.  output, err := cmd.Output()
  7.  if err != nil {
  8.   log.Fatalf("Command failed: %v", err)
  9.   return
  10.  }
  11.  fmt.Println(string(output)) // hello world!
  12. }
复制代码
可以将一个 io.Reader 对象赋值给 *Cmd.Stdin 属性,来实现将数据通过 stdin 通报给外部下令。

实行示例代码,得到输出如下:

  1. $ go run main.go
  2. hello world!
复制代码
 还可以将打开的文件描述符传给 *Cmd.Stdin 属性:
  1. func main() {
  2.  file, err := os.Open("demo.log") // 打开一个文件
  3.  if err != nil {
  4.   log.Fatalf("Open file failed: %v\n", err)
  5.   return
  6.  }
  7.  defer file.Close()
  8.  cmd := exec.Command("cat")
  9.  cmd.Stdin = file       // 将文件作为 cat 的标准输入
  10.  cmd.Stdout = os.Stdout // 获取标准输出
  11.  if err := cmd.Run(); err != nil {
  12.   log.Fatalf("Command failed: %v", err)
  13.  }
  14. }
复制代码
只要是 io.Reader 对象即可。
设置和使用情况变量

*Cmd 的 Environ 方法可以获取情况变量,Env 属性则可以设置情况变量:
  1. func main() {
  2.  cmd := exec.Command("printenv", "ENV_VAR")
  3.  log.Printf("ENV: %+v\n", cmd.Environ())
  4. // 设置环境变量
  5.  cmd.Env = append(cmd.Environ(), "ENV_VAR=HelloWorld")
  6.  log.Printf("ENV: %+v\n", cmd.Environ())
  7. // 获取输出
  8.  output, err := cmd.Output()
  9.  if err != nil {
  10.   log.Fatalf("Command failed: %v", err)
  11.  }
  12.  fmt.Println(string(output)) // HelloWorld
  13. }
复制代码
这段代码输出结果与实行情况相关,此处不演示实行结果了,你可以自行尝试。
不外最终的 output 输出结果一定是 HelloWorld。
使用管道

os/exec 支持管道功能,*Cmd 对象提供的 StdinPipe、StdoutPipe、StderrPipe 三个方法用于获取管道对象。故名思义,三者分别对应尺度输入、尺度输出、尺度错误输出的管道对象。

使用示比方下:

  1. func main() {
  2. // 命令中使用了管道
  3.  cmdEcho := exec.Command("echo", "hello world\nhi there")
  4.  outPipe, err := cmdEcho.StdoutPipe()
  5.  if err != nil {
  6.   log.Fatalf("Command failed: %v", err)
  7.  }
  8. // 注意,这里不能使用 Run 方法阻塞等待,应该使用非阻塞的 Start 方法
  9.  if err := cmdEcho.Start(); err != nil {
  10.   log.Fatalf("Command failed: %v", err)
  11.  }
  12.  cmdGrep := exec.Command("grep", "hello")
  13.  cmdGrep.Stdin = outPipe
  14.  output, err := cmdGrep.Output()
  15.  if err != nil {
  16.   log.Fatalf("Command failed: %v", err)
  17.  }
  18.  fmt.Println(string(output)) // hello world
  19. }
复制代码
首先创建一个用于实行 echo 下令的 *Cmd 对象 cmdEcho,并调用它的 StdoutPipe 方法获得尺度输出管道对象 outPipe;然后调用 Start 方法非阻塞的方式实行 echo 下令;接着创建一个用于实行 grep 下令的 *Cmd 对象 cmdGrep,将 cmdEcho 的尺度输出管道对象赋值给 cmdGrep.Stdin 作为尺度输入,如许,两个下令就通过管道串联起来了;最终通过 cmdGrep.Output 方法拿到 cmdGrep 下令的尺度输出。

实行示例代码,得到输出如下:

  1. $ go run main.go
  2. hello world
复制代码
使用 bash -c 实行复杂下令

如果你不想使用 os/exec 提供的管道功能,那么在下令中直接使用管道符 |,也可以实现同样功能。
不外此时就必要使用 sh -c 大概 bash -c 等 Shell 下令来解析实行更复杂的下令了:
  1. func main() {
  2.  // 命令中使用了管道
  3.  cmd := exec.Command("bash", "-c", "echo 'hello world\nhi there' | grep hello")
  4.  output, err := cmd.Output()
  5.  if err != nil {
  6.   log.Fatalf("Command failed: %v", err)
  7.  }
  8.  fmt.Println(string(output)) // hello world
  9. }
复制代码
这段代码中的管道功能同样生效。
指定工作目次

可以通过指定 *Cmd 对象的的 Dir 属性来指定工作目次:
  1. func main() {
  2.  cmd := exec.Command("cat", "demo.log")
  3.  cmd.Stdout = os.Stdout // 获取标准输出
  4.  cmd.Stderr = os.Stderr // 获取错误输出
  5.  // cmd.Dir = "/tmp" // 指定绝对目录
  6.  cmd.Dir = "." // 指定相对目录
  7.  if err := cmd.Run(); err != nil {
  8.   log.Fatalf("Command failed: %v", err)
  9.  }
  10. }
复制代码
捕获退出状态

上面讲解了很多实行下令相关操纵,但实在还有一个很重要的点没有讲到,就是怎样捕获外部下令实行后的退出状态码:
  1. func main() {
  2. // 查看一个不存在的目录
  3.  cmd := exec.Command("ls", "/nonexistent")
  4. // 运行命令
  5.  err := cmd.Run()
  6. // 检查退出状态
  7. var exitError *exec.ExitError
  8. if errors.As(err, &exitError) {
  9.   log.Fatalf("Process PID: %d exit code: %d", exitError.Pid(), exitError.ExitCode()) // 打印 pid 和退出码
  10.  }
  11. }
复制代码
这里实行 ls 下令来查看一个不存在的目次 /nonexistent,程序退出状态码一定不为 0。
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. 2025/01/15 23:31:44 Process PID: 78328 exit code: 1
  3. exit status 1
复制代码
搜索可实行文件

最后要介绍的函数就只剩一个 LookPath 了,它用来搜索可实行文件。
搜索一个存在的下令:
  1. func main() {
  2.  path, err := exec.LookPath("ls")
  3.  if err != nil {
  4.   log.Fatal("installing ls is in your future")
  5.  }
  6.  fmt.Printf("ls is available at %s\n", path)
  7. }
复制代码
实行示例代码,得到输出如下:
  1.  $ go run main.go
  2. ls is available at /bin/ls
复制代码
搜索一个不存在的下令:
  1. func main() {
  2.  path, err := exec.LookPath("lsx")
  3.  if err != nil {
  4.   log.Fatal(err)
  5.  }
  6.  fmt.Printf("ls is available at %s\n", path)
  7. }
复制代码
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. 2025/01/15 23:37:45 exec: "lsx": executable file not found in $PATH
  3. exit status 1
复制代码
功能练习

介绍完了 os/exec 常用的方法和函数,我们现在来做一个小练习,使用 os/exec 来实行外部下令 ls -l /var/log/*.log。
示比方下:
  1. func main() {
  2.  cmd := exec.Command("ls", "-l", "/var/log/*.log")
  3.  output, err := cmd.CombinedOutput() // 获取标准输出和错误输出
  4.  if err != nil {
  5.   log.Fatalf("Command failed: %v", err)
  6.  }
  7.  fmt.Println(string(output))
  8. }
复制代码
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. 2025/01/16 09:15:52 Command failed: exit status 1
  3. exit status 1
复制代码
实行报错了,这里的错误码为 1,但错误信息并不明白。
这个报错实在是因为 os/exec 默认不支持通配符参数导致的,exec.Command 不支持直接在参数中使用 Shell 通配符(如 *),因为它不会通过 Shell 来解析下令,而是直接调用底层的程序。
要解决这个问题,可以通过显式调用 Shell(比方 bash 或 sh),让 Shell 来解析通配符。
比如使用 bash -c 实行通配符下令 ls -l /var/log/*.log:
  1. func main() {
  2.     // 使用 bash -c 来解析通配符
  3.     cmd := exec.Command("bash", "-c", "ls -l /var/log/*.log")
  4.     output, err := cmd.CombinedOutput() // 获取标准输出和错误输出
  5.     if err != nil {
  6.         log.Fatalf("Command failed: %v", err)
  7.     }
  8.     fmt.Println(string(output))
  9. }
复制代码
实行示例代码,得到输出如下:
  1. $ go run main.go
  2. -rw-r--r--  1 root  wheel         0 Oct  7 21:20 /var/log/alf.log
  3. -rw-r--r--  1 root  wheel     11936 Jan 13 11:36 /var/log/fsck_apfs.log
  4. -rw-r--r--  1 root  wheel       334 Jan 13 11:36 /var/log/fsck_apfs_error.log
  5. -rw-r--r--  1 root  wheel     19506 Jan 11 18:04 /var/log/fsck_hfs.log
  6. -rw-r--r--@ 1 root  wheel  21015342 Jan 16 09:02 /var/log/install.log
  7. -rw-r--r--  1 root  wheel      1502 Nov  5 09:44 /var/log/shutdown_monitor.log
  8. -rw-r-----@ 1 root  admin      3779 Jan 16 08:59 /var/log/system.log
  9. -rw-r-----  1 root  admin    187332 Jan 16 09:05 /var/log/wifi.log
复制代码
此外,我们还可以用 Go 尺度库提供的 filepath.Glob 来手动解析通配符:
  1. func main() {
  2. // 匹配通配符路径
  3.  files, err := filepath.Glob("/var/log/*.log")
  4.  if err != nil {
  5.   log.Fatalf("Glob failed: %v", err)
  6.  }
  7.  if len(files) == 0 {
  8.   log.Println("No matching files found")
  9.   return
  10.  }
  11. // 将匹配到的文件传给 ls 命令
  12.  args := append([]string{"-l"}, files...)
  13.  cmd := exec.Command("ls", args...)
  14.  cmd.Stdout = os.Stdout
  15.  cmd.Stderr = os.Stderr
  16.  if err := cmd.Run(); err != nil {
  17.   log.Fatalf("Command failed: %v", err)
  18.  }
  19. }
复制代码
filepath.Glob 函数会返回模式匹配的文件名列表,如果不匹配则返回 nil。如许,我们就可以先解析文件名列表,再交给 exec.Command 来实行 ls 下令了。

 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

民工心事

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表