LoginSignup
26
8

More than 3 years have passed since last update.

Go の exec.Commandを調査する

Posted at

コマンド実行をたくさんするライブラリを書いているので、exec.Command をしっかりと理解したいと思った。
様々な側面でその挙動を見てみたい。

exec パッケージの概要

まずは公式の Package execの概要を訳してみよう。

Package exec は外部コマンドを実行します。os.StartProcess をラップして、簡単に stdinstdout リマップしたり、パイプで、 I/O と接続したり、他の調整も実施することが可能です。
Cや他の言語のシステムライブラリコールと異なり、os/exec パッケージは意図的にシステムシェルを実行しません。そして、glob のパターンを展開しませんし、他の拡張、パイプライン、リダイレクションといったシェルが実施することを行いません。このパッケージは、Cのexecファミリーのファンクションのように振るいまいます。glob パターンを展開するために、シェルを直接呼んで、危険なインプットをエスケープし、path/file パッケージの Glob ファンクションを使います。環境変数の展開は、os の ExpandEnv を使います。
サンプルは、Unixシステムを想定しています。Windowsでは動作しません。また、Go Playgroundでも動作しません。

簡単な使用方法

いちばん簡単な方法は、なんでも良いので、コマンドを実行して、標準出力、エラー出力を表示してほしいという用途でしょう。

exec.Command() だけではコマンドは実行されません。Output() の時点でコマンドが実行されて、標準出力が返されます。

    ls, err := exec.Command("ls").Output()
    fmt.Printf("hello ls:\n%s :Error:\n%v\n", ls, err)

result

hello ls:
go.mod
main.go

標準出力とエラー出力を返す

しかし、これだと、エラーが発生したときに、エラーが起こったことはわかりますが、エラー出力の内容は表示されません。そこで、CombineOutput() を使うと、標準出力、エラー出力の両方が返却されます。普通にやると、このコマンドは実行できませんが、sh -c を使うことで回避しています。また、-cの後は、通常シングルクオートでくくりそうなものですが、くくっていないところが面白いです。

    ps, err := exec.Command("sh", "-c", "echo stdout; echo stderr 1>&2").CombinedOutput()
    fmt.Printf("hello ps -ef :\n%s :Error:\n%v\n", ps, err)

result

stderr
 :Error:
<nil>

標準出力とエラー出力のリーダーを取得する。

単に標準出力とエラー出力をとりたいだけではなく、両者を区別して取得したい場合は、次のようなことができます。このサンプルは、実際にエラー出力が取れてなくてデバッグに時間がかかったもので --restart='Never' の箇所が、bash では動作するのに、exec.Command では動作しません。ちなみに同じ現象が、Windows の cmd でも起りました。シングルクオートのエスケープが起こっていることが想像できます。気持ち悪いので StackOverflow で聞いてみました。How to pass single quote to the exec.Command on Go lang

    cmd := exec.Command("kubectl", "run", "my-release-kafka-client", "--restart='Never'", "--image", "docker.io/bitnami/kafka:2.7.0-debian-10-r68", "--namespace", "default", "--command", "--", "sleep", "infinity")
    var stdout bytes.Buffer
    var stderr bytes.Buffer
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr
    err := cmd.Run()
    if err != nil {
        fmt.Printf("Stdout: %s\n", stdout.String())
        fmt.Printf("Stderr: %s\n", stderr.String())
    } else {
        fmt.Printf("Stdout: %s\n", stdout.String())
    }

result

$ go run main.go 
Stdout: 
Stderr: error: invalid restart policy: 'Never'
See 'kubectl run -h' for help and examples

タイムアウトを設定する

コマンド実行のプログラムを書くなら、ロングランニングのコマンドがあったら、ある程度の時間でタイムアウトしたいことがありそうです。そんなときは、exec.CommandContext() が使えます。なかなか便利そう。

    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Microsecond)
    defer cancel()
    if output, err := exec.CommandContext(ctx, "sleep", "5").CombinedOutput(); err != nil {
        fmt.Printf("Exceed the timeout 100ms. %v\n", err)
        fmt.Printf("Combine Output: %s\n", output)
    }

result

Exceed the timeout 100ms. signal: killed
Combine Output: 

パイプをつかいたい

パイプを使いたいときどうしましょう?普通に書くとしっかり失敗します。

    output, err := exec.Command("ps -ef | grep jvm").CombinedOutput()
    fmt.Printf("CombineOutput: %s, Error: %v\n", output, err)

result

CombineOutput: , Error: exec: "ps -ef | grep jvm": executable file not found in $PATH

これで完璧

    output, err = exec.Command("sh", "-c", "ps -ef | grep jvm").CombinedOutput()
    fmt.Printf("CombineOutput: %s, Error: %v\n", output, err)

result

CombineOutput: ushio    30477 30451  0 01:01 pts/8    00:00:00 sh -c ps -ef | grep jvm
ushio    30479 30477  0 01:01 pts/8    00:00:00 grep jvm

StdErrorPipe

StdErrorPipe io.ReadCloser を返します。いわゆるストリーム系です。もちろん、StdOutPipeも存在します。ここでのポイントは cmd.Start(), cmd.Wait() を使っているところです。Start()は、コマンドを実行しますが、待ち受けしません。そこで、Wait() で待ち受けします。このStdErrorPipe は、Wait() の前に実行することが必要です。io.ReadCloserWait() が閉じてしまうためです。他にも cmd.Run() というコマンドがありますが、これは内部で、cmd.Start()cmd.Wait() を読んでいる常に待つタイプのコマンドです。しかし、その理屈でいうと、StdErrorPipe は使えないということになりますね。

    cmd = exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
    stdErrorPipe, err := cmd.StderrPipe()
    if err != nil {
        log.Fatal(err)
    }

    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }

    slurp, _ := ioutil.ReadAll(stdErrorPipe)
    fmt.Printf("stderr: %s\n", slurp)

    if err := cmd.Wait(); err != nil {
        log.Fatal(err)
    }

デバッグ用の String()

あくまでデバッグ用でプロダクションに使うなと書いてありますが、String() という関数もあるようです。なぜかコマンドの絶対パスを返しています。

    pwd := exec.Command("ps", "-ef").String()
    fmt.Printf("hello ps:\n%s\n", pwd)

result

hello ps:
/usr/bin/ps -ef

まとめ

大体やりたいことがカバーできたので、これで高速にコーディングできそうです。

26
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
8