背景
项目中需要执行shell命令,虽然exec包提供了CombinedOutput()
方法,在shell运行结束会返回shell执行的输出,但是用户在发起一次任务时,可能在不停的刷新log,想达到同步查看log的目的,但是CombinedOutput()
方法只能在命令完全执行结束才返回整个shell的输出,所以肯定达不到效果,所以,需要寻找其它方法达到命令一边执行log一边输出的目的。
1. 使用重定向
如果你的shell比较简单,并且log的文件路径也很容易确定,那么直接对shell执行的命令添加重定向最简单不过了,程序参考如下
package main
import (
"fmt"
"os/exec"
)
func main() {
//定义一个每秒1次输出的shell
cmdStr := `
#!/bin/bash
for var in {1..10}
do
sleep 1
echo "Hello, Welcome ${var} times "
done`
cmd := exec.Command("bash", "-c",
cmdStr+" >> file.log") //重定向
err := cmd.Start()
if err != nil {
fmt.Println(err)
}
err = cmd.Wait()
if err != nil {
fmt.Println(err)
}
}
上面程序定义了一个每秒1次的shell,但是在shell执行前,对shell进行了拼接,使用了重定向,所以我们可以在另外一个 terminal中实时的看到 log 的变化
2. 指定Shell执行时的输出
使用exec.Command创建一个Shell后,就具有了两个变量:
// Stdout and Stderr specify the process's standard output and error.
//
// If either is nil, Run connects the corresponding file descriptor
// to the null device (os.DevNull).
//
// If either is an *os.File, the corresponding output from the process
// is connected directly to that file.
//
// Otherwise, during the execution of the command a separate goroutine
// reads from the process over a pipe and delivers that data to the
// corresponding Writer. In this case, Wait does not complete until the
// goroutine reaches EOF or encounters an error.
//
// If Stdout and Stderr are the same writer, and have a type that can
// be compared with ==, at most one goroutine at a time will call Write.
Stdout io.Writer
Stderr io.Writer
这两个变量是用来指定程序的标准输出和标准错误输出的位置,所以我们就利用这两个变量,直接打开文件,然后将打开的文件指针赋值给这两个变量即可将程序的输出直接输出到文件中,也能达到相同的效果,参考程序如下:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
//定义一个每秒1次输出的shell
cmdStr := `
#!/bin/bash
for var in {1..10}
do
sleep 1
echo "Hello, Welcome ${var} times "
done`
cmd := exec.Command("bash", "-c", cmdStr)
//打开一个文件
f, _ := os.OpenFile("file.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer f.Close()
//指定输出位置
cmd.Stderr = f
cmd.Stdout = f
err := cmd.Start()
if err != nil {
fmt.Println(err)
}
err = cmd.Wait()
if err != nil {
fmt.Println(err)
}
}
3. 从shell执行结果的管道中获取输出
和第二种方法类似,exec.Shell
不仅提供了Stdout
和Stdin
这两个变量,还有下面两个方法
// StdoutPipe returns a pipe that will be connected to the command's
// standard output when the command starts.
//
// Wait will close the pipe after seeing the command exit, so most callers
// need not close the pipe themselves; however, an implication is that
// it is incorrect to call Wait before all reads from the pipe have completed.
// For the same reason, it is incorrect to call Run when using StdoutPipe.
// See the example for idiomatic usage.
func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
...
}
// StderrPipe returns a pipe that will be connected to the command's
// standard error when the command starts.
//
// Wait will close the pipe after seeing the command exit, so most callers
// need not close the pipe themselves; however, an implication is that
// it is incorrect to call Wait before all reads from the pipe have completed.
// For the same reason, it is incorrect to use Run when using StderrPipe.
// See the StdoutPipe example for idiomatic usage.
func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
...
}
上面两个方法会返回命令执行过程中标准输出和标准错误输出的两个管道,我们可以通过这个管道获取命令执行过程中的输出,参考程序如下:
package main
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
)
//通过管道同步获取日志的函数
func syncLog(reader io.ReadCloser) {
f, _ := os.OpenFile("file.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer f.Close()
buf := make([]byte, 1024, 1024)
for {
strNum, err := reader.Read(buf)
if strNum > 0 {
outputByte := buf[:strNum]
f.WriteString(string(outputByte))
}
if err != nil {
//读到结尾
if err == io.EOF || strings.Contains(err.Error(), "file already closed") {
err = nil
}
}
}
}
func main() {
//定义一个每秒1次输出的shell
cmdStr := `
#!/bin/bash
for var in {1..10}
do
sleep 1
echo "Hello, Welcome ${var} times "
done`
cmd := exec.Command("bash", "-c", cmdStr)
//这里得到标准输出和标准错误输出的两个管道,此处获取了错误处理
cmdStdoutPipe, _ := cmd.StdoutPipe()
cmdStderrPipe, _ := cmd.StderrPipe()
err := cmd.Start()
if err != nil {
fmt.Println(err)
}
go syncLog(cmdStdoutPipe)
go syncLog(cmdStderrPipe)
err = cmd.Wait()
if err != nil {
fmt.Println(err)
}
}
扩展 - 解决log格式乱的问题
上面第三种方式,我们直接是通过打开文件,然后将读取到的程序输出写入文件,但是实际上可能别人又已经封装好了一个logger,让你往logger里面写,比如,我这里就使用log包提供的log然后将程序的执行结果写入,但是因为使用了log包,所以写入的格式和log本身的格式造成格式会有部分的错乱,参考我的解决办法,解释都在注释里,如下:
package main
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
)
//通过管道同步获取日志的函数
func syncLog(logger *log.Logger, reader io.ReadCloser) {
//因为logger的print方法会自动添加一个换行,所以我们需要一个cache暂存不满一行的log
cache := ""
buf := make([]byte, 1024, 1024)
for {
strNum, err := reader.Read(buf)
if strNum > 0 {
outputByte := buf[:strNum]
//这里的切分是为了将整行的log提取出来,然后将不满整行和下次一同打印
outputSlice := strings.Split(string(outputByte), "\n")
logText := strings.Join(outputSlice[:len(outputSlice)-1], "\n")
logger.Printf("%s%s", cache, logText)
cache = outputSlice[len(outputSlice)-1]
}
if err != nil {
if err == io.EOF || strings.Contains(err.Error(), "file already closed") {
err = nil
}
}
}
}
func main() {
//定义一个每秒1次输出的shell
cmdStr := `
#!/bin/bash
for var in {1..10}
do
sleep 1
echo "Hello, Welcome ${var} times "
done`
cmd := exec.Command("bash", "-c", cmdStr)
//这里得到标准输出和标准错误输出的两个管道,此处获取了错误处理
cmdStdoutPipe, _ := cmd.StdoutPipe()
cmdStderrPipe, _ := cmd.StderrPipe()
err := cmd.Start()
if err != nil {
fmt.Println(err)
}
//打开一个文件,用作log封装输出
f, _ := os.OpenFile("file.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer f.Close()
//创建封装的log,第三个参数设置log输出的格式
logger := log.New(f, "", log.LstdFlags)
logger.Print("start print log:")
oldFlags := logger.Flags()
//为了保证shell的输出和标准的log格式不冲突,并且为了整齐,关闭logger自身的格式
logger.SetFlags(0)
go syncLog(logger, cmdStdoutPipe)
go syncLog(logger, cmdStderrPipe)
err = cmd.Wait()
//执行完后再打开log输出的格式
logger.SetFlags(oldFlags)
logger.Print("log print done")
if err != nil {
fmt.Println(err)
}
}
程序执行结果如下:
可以看到,shell的执行过程中的log是没有日志的前缀的,这样也保证了log的整齐:)