関連記事
GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ
概要
以下、自分用のメモです。
exec.Commandを使って、外部コマンドを実行して
結果を取り込んだりする際、どの言語でもそうですが実行したコマンドが大量の出力を行う場合は注意が必要です。
出力が大きすぎるとOSのバッファが一杯になって出力が詰まってしまいます。そのような場合、出力結果を一発で取得するのではなく
ストリームとして扱って順次出力結果を取得していくことが必要になります。
*exec.Cmdには、Stdin,Stdout,Stderrと必要なio.Readerとio.Writerが用意されていますので
それを利用するか、StdinPipe,StdoutPipe,StderrPipeを使って処理することも出来ますね。
*exec.Cmd関連は、以前に何個かメモかいていますので、そちらもよかったら参照ください。
今回は、自前のパイプをコマンドの出力に接続して、大きなコマンド出力を処理するサンプルです。os.Pipeの扱いに慣れたいという意味もあります。
サンプル
ospipe.go
package cmdexec import ( "bufio" "fmt" "io" "os" "os/exec" ) // OsPipe は、(*Cmd).Stdout に os.Pipe の io.Writer を接続して処理するサンプルです. // // # REFERENCES // - https://pkg.go.dev/os@go1.24.4#Pipe func OsPipe() error { // // (*Cmd).StdoutPipe()で同じことが出来るが // os.Pipe()の使い方を勉強するために // 意図的に利用している // /////////////////////////////////// // パイプ取得 /////////////////////////////////// var ( pr *os.File pw *os.File err error ) if pr, pw, err = os.Pipe(); err != nil { return err } defer pr.Close() /////////////////////////////////// // コマンド実行 // // git log コマンドを実行しているため // リポジトリによっては長大な出力が発生する。 // // 簡易なコマンド実行である (*Cmd).Output() で取得しようとすると // OSのバッファが一杯になってしまう可能性があるため、このような場合は // ストリーミング処理が必須となる。 // // 以下で実行している git コマンドのオプションは以下の通り // - --no-pager : ページャーを使用しない // - log : ログを表示 // - -m : マージコミットの差分も表示 // - -r : 再帰的に処理 // - --name-only : ファイル名のみ表示 // - --pretty=raw: 生フォーマットで表示 // - -z : NULL文字で区切る /////////////////////////////////// var ( name = "git" args = []string{"--no-pager", "log", "-m", "-r", "--name-only", "--pretty=raw", "-z"} cmd = exec.Command(name, args...) ) cmd.Stdout = pw if err = cmd.Start(); err != nil { pw.Close() return err } /////////////////////////////////// // 終了待機用のゴルーチンを用意 /////////////////////////////////// var ( done = make(chan error, 1) ) go func() { defer pw.Close() done <- cmd.Wait() }() /////////////////////////////////// // コマンドの出力を読み出し /////////////////////////////////// const ( MaxTokenSize = 1024 * 1024 // 1行のサイズが大きい可能性を考慮してバッファサイズを底上げ ) var ( scanner = bufio.NewScanner(pr) buf = make([]byte, MaxTokenSize) count int ) scanner.Buffer(buf, MaxTokenSize) for scanner.Scan() { io.Discard.Write(scanner.Bytes()) count++ } if err = scanner.Err(); err != nil { return err } /////////////////////////////////// // コマンド終了待機 /////////////////////////////////// if err = <-done; err != nil { return err } /////////////////////////////////// // 結果出力 /////////////////////////////////// fmt.Printf("Total lines: %d\n", count) return nil }
実行結果
$ task task: [build] go build -o "/workspace/try-golang/try-golang" . task: [run] ./try-golang -onetime ENTER EXAMPLE NAME: cmdexec_ospipe [Name] "cmdexec_ospipe" Total lines: 29996 [Elapsed] 122.721215ms
自前のリポジトリですが、結構なコミット数があるので git log すると大量の出力が流れます。
$ git rev-list --count HEAD 2288
参考情報
exec package - os/exec - Go Packages
Goのおすすめ書籍
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。