路漫漫其修远兮
吾将上下而求索

go学习:运行shell命令

执行命令并获得输出结果

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

package main

import (
	"fmt"
	"log"
	"os/exec"
)

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

将stdout和stderr分别处理

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

package main

import (
	"bytes"
	"fmt"
	"log"
	"os/exec"
)

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.Printf("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,我们还希望在控制台显示命令执行的进度。

有点小复杂。

package main

import (
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
)

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...)
			w.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
		}
	}
}
func main() {
	cmd := exec.Command("/bin/bash", "test.sh")
	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.Printf("cmd.Run() failed with %s\n", err)
	}
	if errStdout != nil || errStderr != nil {
		log.Printf("failed to capture stdout or stderr\n")
	}
	outStr, errStr := string(stdout), string(stderr)
	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
}


下面是test.sh文件内容
while [[ $i -le 10 ]]; do
    let i=i+1
    echo $i abcdefghigklmnopqrstovwxyz
    sleep 1
done

以上程序达到的效果,模拟有些shell执行时间过长,只有执行完成后才知道执行结果
上述程序可以将shell执行的标准输出实时打印出来
[root@k8s-test-2 /home/andy/go-test]#go run main.go 
1 abcdefghigklmnopqrstovwxyz
2 abcdefghigklmnopqrstovwxyz
3 abcdefghigklmnopqrstovwxyz
4 abcdefghigklmnopqrstovwxyz
5 abcdefghigklmnopqrstovwxyz
6 abcdefghigklmnopqrstovwxyz
7 abcdefghigklmnopqrstovwxyz
8 abcdefghigklmnopqrstovwxyz
9 abcdefghigklmnopqrstovwxyz
10 abcdefghigklmnopqrstovwxyz
11 abcdefghigklmnopqrstovwxyz

out:
1 abcdefghigklmnopqrstovwxyz
2 abcdefghigklmnopqrstovwxyz
3 abcdefghigklmnopqrstovwxyz
4 abcdefghigklmnopqrstovwxyz
5 abcdefghigklmnopqrstovwxyz
6 abcdefghigklmnopqrstovwxyz
7 abcdefghigklmnopqrstovwxyz
8 abcdefghigklmnopqrstovwxyz
9 abcdefghigklmnopqrstovwxyz
10 abcdefghigklmnopqrstovwxyz
11 abcdefghigklmnopqrstovwxyz

err:

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

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

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

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
)

// 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.Printf("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.Printf("cmd.Run() failed with %s\n", err)
	}
	if errStdout != nil || errStderr != nil {
		log.Printf("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

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

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
)

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.Printf("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.Printf("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)

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

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

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

package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
)

func main() {
	cmd := exec.Command("programToExecute")
	additionalEnv := "FOO=bar"
	newEnv := append(os.Environ(), additionalEnv))
	cmd.Env = newEnv
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Printf("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)
}

或者直接使用Cmd的StdoutPipe方法,而不是自己创建一个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://github.com/kjk/go-cookbook/tree/master/advanced-exec

未经允许不得转载:江哥架构师笔记 » go学习:运行shell命令

分享到:更多 ()

评论 抢沙发

评论前必须登录!