[golang][譯]使用os/exec執行命令

  • 2019 年 10 月 7 日
  • 筆記

[golang][譯]使用os/exec執行命令

https://colobu.com/2017/06/19/advanced-command-execution-in-Go-with-os-exec/

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

Go可以非常方便地執行外部程式,讓我們開始探索之旅吧。

執行命令並獲得輸出結果

最簡單的例子就是運行ls -lah並獲得組合在一起的stdout/stderr輸出。

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

將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 %sn", err)      }      outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())      fmt.Printf("out:n%snerr:n%sn", outStr, errStr)  }

命令執行過程中獲得輸出

如果一個命令需要花費很長時間才能執行完呢?

除了能獲得它的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 %sn", err)      }      if errStdout != nil || errStderr != nil {          log.Fatalf("failed to capture stdout or stderrn")      }      outStr, errStr := string(stdout), string(stderr)      fmt.Printf("nout:n%snerr:n%sn", outStr, errStr)  }

命令執行過程中獲得輸出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 %sn", err)      }      if errStdout != nil || errStderr != nil {          log.Fatalf("failed to capture stdout or stderrn")      }      outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())      fmt.Printf("nout:n%snerr:n%sn", outStr, errStr)  }

命令執行過程中獲得輸出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 %sn", err)      }      if errStdout != nil || errStderr != nil {          log.Fatal("failed to capture stdout or stderrn")      }      outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes())      fmt.Printf("nout:n%snerr:n%sn", outStr, errStr)  }

改變執行程式的環境(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 %sn", err)  }  fmt.Printf("%s", out)

包 shurcooL/go/osutil提供了便利的方法設置環境變數。

預先檢查程式是否存在

想像一下你寫了一個程式需要花費很長時間執行,再最後你調用foo做一些基本的任務。

如果foo程式不存在,程式會執行失敗。

當然如果我們預先能檢查程式是否存在就完美了,如果不存在就列印錯誤資訊。

你可以調用exec.LookPath方法來檢查:

func checkLsExists() {      path, err := exec.LookPath("ls")      if err != nil {          fmt.Printf("didn't find 'ls' executablen")      } else {          fmt.Printf("'ls' executable is in '%s'n", path)      }  }

另一個檢查的辦法就是讓程式執行一個空操作, 比如傳遞參數"–help"顯示幫助資訊。

下面的章節是譯者補充的內容

管道

我們可以使用管道將多個命令串聯起來, 上一個命令的輸出是下一個命令的輸入。

使用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()  }

管道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))  }

https://www.cnblogs.com/landv/p/11584253.html