おひとり

できる限りひとりで楽しむための情報やプログラミング情報など。

Golangでリアルタイムにstdoutに出力された文字列を取得する方法

f:id:hitoridehitode:20191003205941j:plain:w100
Golangから*.shのシェルスクリプトを実行するさい、実行中の結果をリアルタイムに取得したかったので、試行錯誤してみました。
とてもマニアックかもしれませんが。。。

やりたいこと

例えば、以下のような時間がかかるシェルスクリプトがあったとします。

#!/bin/bash
echo "Hello world!"
sleep 1
echo "I'm so happy!"
sleep 1
echo "Thank you!!!"
sleep 1
echo "Good bye!"

echo の間には sleep 1 コマンドがあります。
そのため、すべてのコマンドが終了するのにはおよそ3秒かかるのでとても遅いです。
このとき、とりあえずコマンドの進捗をgolang側で取得して、必要な処理を進めることができればいいなと思いました。

そこで、例えば1行目の Hello world! が出力された時点でいったん文字列を取得して何か処理、
その後 I'm so happy! が出力されたタイミングで何か処理、 といった感じで、コマンドの実行結果をリアルタイムに取得するプログラムを書いてみました。
言い換えれば、実行完了を待たずに、途中結果を取得したいということですね。

プログラム

まず、先ほどのシェルスクリプトは command.sh として保存しました。 ※わかりやすさのためエラーハンドリングは省略しています。

package main

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

func main() {
    //わかりやすさのため、エラーハンドリングは省略

    // シェルで実行するコマンドを作成
    cmd := exec.Command("./command.sh")

    // `StdoutPipe()` 経由で`io.ReadCloser`を取得
    stdout, _ := cmd.StdoutPipe()

    // バッファを作成
    buff := make([]byte, 1024)

    // コマンドを実行する
    cmd.Start()

    // 出力結果を取得(C言語系だとdo~whileみたいなことをしている)
    n, err := stdout.Read(buff)

    // EOFの場合は err == io.EOF となる
    for err == nil || err != io.EOF {
        // データがGET出来た場合、文字列に変換して表示
        if n > 0 {
            fmt.Print(string(buff[:n]))
        }

        // 再び読み込む
        n, err = stdout.Read(buff)
    }
}

ここでのポイントは、実行にはcmd.Start()を利用していることです。Start()を利用することで、非同期にコマンドを実行することが出来ます。
通常、start()を利用した場合、コマンドの実行終了を待つには cmd.Wait() の用にします。
ここでは、必要なく、バッファ経由でコマンドの実行を追いかけます。

このようにすると、コマンドの終了を待たずに途中結果を取得して何か処理をすることが出来そうです!

リンク

公式ドキュメント
https://golang.org/pkg/os/exec/#Cmd.StdoutPipe
https://golang.org/pkg/os/exec/#Cmd.Start