[译]使用os/exec执行命令 GO语言

原文: Advanced command execution in Go with os/exec by Krzysztof Kowalczyk.
完整代码在作者的github上: advanced-exec

Go可以非常方便地执行外部程序,让我们开始探索之旅吧。

1/ 执行命令并获得输出结果

最简单的例子就是运行ls -lah并获得组合在一起的stdout/stderr输出。

  func main() {
      cmd := exec.Command("ls", "-lah")
      out, err := cmd.CombinedOutput()
      if err != nil {
      log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))

}

2/将stdout和stderr分别处理

和上面的例子类似,只不过将stdout和stderr分别处理。

  func main() {
      cmd := exec.Command("ls", "-lah")
      var stdout, stderr bytes.Buffer
      cmd.Stdout = &stdout
      cmd.Stderr = &stderr
      err := cmd.Run()
      if err != nil {
          log.Fatalf("cmd.Run() failed with %s\n", err)
      }
      outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
      fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr)
  }

3/ 命令执行过程中获得输出

如果一个命令需要花费很长时间才能执行完呢?

除了能获得它的stdout/stderr,我们还希望在控制台显示命令执行的进度。

有点小复杂。

  func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
      var out []byte
      buf := make([]byte, 1024, 1024)
      for {
          n, err := r.Read(buf[:])
          if n > 0 {
              d := buf[:n]
              out = append(out, d...)
              os.Stdout.Write(d)
          }
          if err != nil {
              // Read returns io.EOF at the end of file, which is not an error for us
              if err == io.EOF {
                  err = nil
              }
              return out, err
          }
      }
      // never reached
      panic(true)
      return nil, nil
  }
  func main() {
      cmd := exec.Command("ls", "-lah")
      var stdout, stderr []byte
      var errStdout, errStderr error
      stdoutIn, _ := cmd.StdoutPipe()
      stderrIn, _ := cmd.StderrPipe()
      cmd.Start()
      go func() {
          stdout, errStdout = copyAndCapture(os.Stdout, stdoutIn)
      }()
      go func() {
          stderr, errStderr = copyAndCapture(os.Stderr, stderrIn)
      }()
      err := cmd.Wait()
      if err != nil {
          log.Fatalf("cmd.Run() failed with %s\n", err)
      }
      if errStdout != nil || errStderr != nil {
          log.Fatalf("failed to capture stdout or stderr\n")
      }
      outStr, errStr := string(stdout), string(stderr)
      fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
  }

4/ 命令执行过程中获得输出2

上一个方案虽然工作,但是看起来copyAndCapture好像重新实现了io.Copy。由于Go的接口的功能,我们可以重用io.Copy

我们写一个CapturingPassThroughWriterstruct,它实现了io.Writer接口。它会捕获所有的数据并写入到底层的io.Writer

  // CapturingPassThroughWriter is a writer that remembers
  // data written to it and passes it to w
  type CapturingPassThroughWriter struct {
    buf bytes.Buffer
  w io.Writer
  }
  // NewCapturingPassThroughWriter creates new CapturingPassThroughWriter
  func NewCapturingPassThroughWriter(w io.Writer) *CapturingPassThroughWriter {
  return &CapturingPassThroughWriter{
    w: w,
  }
  }
  func (w *CapturingPassThroughWriter) Write(d []byte) (int, error) {
  w.buf.Write(d)
    return w.w.Write(d)     
  }
  // Bytes returns bytes written to the writer
  func (w *CapturingPassThroughWriter) Bytes() []byte {
    return w.buf.Bytes()
  }
  func main() {
        var errStdout, errStderr error
        cmd := exec.Command("ls", "-lah")
        stdoutIn, _ := cmd.StdoutPipe()
        stderrIn, _ := cmd.StderrPipe()
        stdout := NewCapturingPassThroughWriter(os.Stdout)
        stderr := NewCapturingPassThroughWriter(os.Stderr)
        err := cmd.Start()
if err != nil {
    log.Fatalf("cmd.Start() failed with '%s'\n", err)
}
go func() {
    _, errStdout = io.Copy(stdout, stdoutIn)
}()
go func() {
    _, errStderr = io.Copy(stderr, stderrIn)
}()
err = cmd.Wait()
if err != nil {
    log.Fatalf("cmd.Run() failed with %s\n", err)
}
if errStdout != nil || errStderr != nil {
    log.Fatalf("failed to capture stdout or stderr\n")
}
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
  }

5/ 命令执行过程中获得输出3

事实上Go标准库包含一个更通用的io.MultiWriter,我们可以直接使用它。

  func main() {
    var stdoutBuf, stderrBuf bytes.Buffer
    cmd := exec.Command("ls", "-lah")
    stdoutIn, _ := cmd.StdoutPipe()
    stderrIn, _ := cmd.StderrPipe()
    var errStdout, errStderr error
    stdout := io.MultiWriter(os.Stdout, &stdoutBuf)
    stderr := io.MultiWriter(os.Stderr, &stderrBuf)
    err := cmd.Start()
    if err != nil {
        log.Fatalf("cmd.Start() failed with '%s'\n", err)
    }
    go func() {
        _, errStdout = io.Copy(stdout, stdoutIn)
    }()
    go func() {
        _, errStderr = io.Copy(stderr, stderrIn)
    }()
    err = cmd.Wait()
    if err != nil {
        log.Fatalf("cmd.Run() failed with %s\n", err)
    }
    if errStdout != nil || errStderr != nil {
        log.Fatal("failed to capture stdout or stderr\n")
    }
    outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes())
    fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
  }

自己实现是很好滴,但是熟悉标准库并使用它更好。

6/ 改变执行程序的环境(environment)

你已经知道了怎么在程序中获得环境变量,对吧: `os.Environ()`返回所有的环境变量[]string,每个字符串以FOO=bar格式存在。FOO是环境变量的名称,bar是环境变量的值, 也就是os.Getenv("FOO")的返回值。

有时候你可能想修改执行程序的环境。

你可设置exec.CmdEnv的值,和os.Environ()格式相同。通常你不会构造一个全新的环境,而是添加自己需要的环境变量:

  cmd := exec.Command("programToExecute")
  additionalEnv := "FOO=bar"
  newEnv := append(os.Environ(), additionalEnv))
  cmd.Env = newEnv
  out, err := cmd.CombinedOutput()
  if err != nil {
        log.Fatalf("cmd.Run() failed with %s\n", err)
  }
  fmt.Printf("%s", out)

shurcooL/go/osutil提供了便利的方法设置环境变量。

7/ 预先检查程序是否存在

想象一下你写了一个程序需要花费很长时间执行,再最后你调用foo做一些基本的任务。

如果foo程序不存在,程序会执行失败。

当然如果我们预先能检查程序是否存在就完美了,如果不存在就打印错误信息。

你可以调用exec.LookPath方法来检查:

  func checkLsExists() {

        path, err := exec.LookPath("ls")

        if err != nil {

              fmt.Printf("didn't find 'ls' executable\n")

        } else {

              fmt.Printf("'ls' executable is in '%s'\n", path)

              }

  }

另一个检查的办法就是让程序执行一个空操作, 比如传递参数"--help"显示帮助信息。

下面的章节是译者补充的内容

8/ 管道

我们可以使用管道将多个命令串联起来, 上一个命令的输出是下一个命令的输入。

使用os.Exec有点麻烦,你可以使用下面的方法:

  package main
  import (
      "bytes"
      "io"
      "os"
      "os/exec"
  )
  func main() {
      c1 := exec.Command("ls")
      c2 := exec.Command("wc", "-l")
      r, w := io.Pipe() 
      c1.Stdout = w
      c2.Stdin = r
      var b2 bytes.Buffer
      c2.Stdout = &b2
      c1.Start()
      c2.Start()
      c1.Wait()
      w.Close()
      c2.Wait()
      io.Copy(os.Stdout, &b2)
  }

或者直接使用CmdStdoutPipe方法,而不是自己创建一个io.Pipe`。

  package main
  import (
      "os"
      "os/exec"
  )
  func main() {
      c1 := exec.Command("ls")
      c2 := exec.Command("wc", "-l")
      c2.Stdin, _ = c1.StdoutPipe()
      c2.Stdout = os.Stdout
      _ = c2.Start()
      _ = c1.Run()
      _ = c2.Wait()
  }

9/ 管道2

上面的解决方案是Go风格的解决方案,事实上你还可以用一个"Trick"来实现。

  package main
  import (
    "fmt"
    "os/exec"
  )
  func main() {
    cmd := "cat /proc/cpuinfo | egrep '^model name' | uniq | awk '{print substr($0, index($0,$4))}'"
    out, err := exec.Command("bash", "-c", cmd).Output()
    if err != nil {
        fmt.Printf("Failed to execute command: %s", cmd)
    }
    fmt.Println(string(out))
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容