1. ホーム
  2. Golang

golang exec シェル実行 出力の同期方法/実行結果の取得方法

2022-02-15 07:05:50
<パス

背景

このプロジェクトではシェルコマンドを実行する必要があり、exec パッケージでは CombinedOutput() メソッドは、シェルの実行終了時にその出力を返すものですが、ユーザーはタスクの起動時にログを更新しており、ログの同期表示を実現したい場合がありますが、その際に CombinedOutput() メソッドは、コマンドが完全に実行されたときにシェル出力全体を返すだけなので、うまくいかないので、ログが出力されている間にコマンドを実行する別の方法を見つける必要があります。

1. リダイレクトの利用

シェルがシンプルで、ログファイルへのパスがわかりやすい場合は、以下の手順を参考に、シェルで実行されるコマンドに直接リダイレクトを追加するのが最も簡単です。

package main

import (
	"fmt"
	"os/exec"
)

func main() {
	// Define a shell with 1 output per second
	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") // redirect
	err := cmd.Start()
	if err ! = nil {
		fmt.Println(err)
	}
	err = cmd.Wait()
	if err ! = nil {
		fmt.Println(err)
	}
}


上記のプログラムは、1秒間に1回のシェルを定義していますが、シェルが実行される前に、リダイレクトを使って、シェルを縫い合わせるので、別のターミナルで、ログの変化をリアルタイムで見ることができるのです

2. シェルが実行されたときの出力を指定する

exec.Commandを使ってShellを作成すると、2つの変数を持つようになります。

	// 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
	// Otherwise, during the execution of the command a separate goroutine // reads from the process over a pipe and delivers that data to the
	// 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
	// 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.


この2つの変数は、プログラムの標準出力と標準エラー出力の場所を指定するためのものなので、この2つの変数を使って直接ファイルを開き、この2つの変数にオープンファイルのポインタを代入して、プログラムの出力を直接ファイルに出力しても、同じ効果が得られるので、次の参考プログラムを使って、このようにします。

package main

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

func main() {
	// Define a shell with 1 output per second
	cmdStr := `
#! /bin/bash
for var in {1..10}
do
	sleep 1
     echo "Hello, Welcome ${var} times "
done`
	cmd := exec.Command("bash", "-c", cmdStr)
	//open a file
	f, _ := os.OpenFile("file.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	defer f.Close()
	//specify the output location
	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. シェル実行結果のパイプラインから出力を取得する

2番目の方法と同様に exec. を提供するだけでなく Stdout Stdin この2つの変数と、以下の2つのメソッドを使用します。

// 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 the command's // standard error.
// 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) {
	...
}


上記の2つのメソッドは、コマンド実行時に標準出力と標準エラー出力の2つのパイプラインを返すので、これを通して、以下の参考プログラムにより、コマンド実行時の出力を得ることができます。

package main

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

// function to get logs synchronously via pipeline
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 {
			//read to the end
			if err == io.EOF || strings.Contains(err.Error(), "file already closed") {
				err = nil
			}
		}
	}
}

func main() {
	// Define a shell with 1 output per second
	cmdStr := `
#! /bin/bash
for var in {1..10}
do
	sleep 1
     echo "Hello, Welcome ${var} times "
done`
	cmd := exec.Command("bash", "-c", cmdStr)
	// Here we get the two pipes for standard output and standard error output, here we get the error handling
	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)
	}
}



Extension - 乱雑なログの書式を解決する

上記の3番目の方法は、我々はファイルを開くことによって直接されており、その後、ファイルに読み込まれたプログラムの出力を書き込むが、実際には、誰かがロガーをカプセル化している可能性があり、あなたがロガーの内部に書き込むことができます、例えば、私はログを提供し、プログラムの実行結果を書くためにここでログパッケージを使用するがために、書き込みとログパッケージ自体の形式は、いくつかのフォーマットエラーが発生するので、コメントで説明されて私のソリューションを参照して、次のようになります。

package main

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

// function to get logs synchronously via pipeline
func syncLog(logger *log.Logger, reader io.ReadCloser) {
	//because the logger's print method automatically adds a newline, we need a cache to temporarily store logs that are less than one line
	cache := ""
	buf := make([]byte, 1024, 1024)
	for {
		strNum, err := reader.Read(buf)
		if strNum > 0 {
			outputByte := buf[:strNum]
			// the slice here is to extract the whole line of log, and then print the unhappy whole line with the next
			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() {
	// Define a shell with 1 output per second
	cmdStr := `
#! /bin/bash
for var in {1..10}
do
	sleep 1
     echo "Hello, Welcome ${var} times "
done`
	cmd := exec.Command("bash", "-c", cmdStr)
	// Here we get the two pipes for standard output and standard error output, here we get the error handling
	cmdStdoutPipe, _ := cmd.StdoutPipe()
	cmdStderrPipe, _ := cmd.StderrPipe()
	err := cmd.Start()
	if err ! = nil {
		fmt.Println(err)
	}
	//open a file to be used as log wrapper output
	f, _ := os.OpenFile("file.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	defer f.Close()
	// create a wrapped log, the third parameter sets the format of the log output
	logger := log.New(f, "", log.LstdFlags)
	logger.Print("start print log:")
	oldFlags := logger.Flags()
	// To ensure that the shell's output does not conflict with the standard log format, and to keep things tidy, turn off the logger's own format
	logger.SetFlags(0)
	go syncLog(logger, cmdStdoutPipe)
	go syncLog(logger, cmdStderrPipe)
	err = cmd.Wait()
	//open the format of the log output after execution
	logger.SetFlags(oldFlags)
	logger.Print("log print done")
	if err ! = nil {
		fmt.Println(err)
	}
}


プログラムが実行され、次のような結果が得られます。

このように、シェル実行時のログにはlogsという接頭辞が付かないので、ログがきちんと残っていることも確認できます :)