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

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

 

执行命令并获得输出结果

ls -lah
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))
}

将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)
}

命令执行过程中获得输出

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

除了能获得它的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)
}

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

copyAndCaptureio.Copyio.Copy
CapturingPassThroughWriterio.Writerio.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)
}

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

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)

}

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

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

[]stringFOO=barFOObaros.Getenv("FOO")

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

exec.CmdEnvos.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提供了便利的方法设置环境变量。

预先检查程序是否存在

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"显示帮助信息。

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

管道

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

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
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"来实现。