コマンド実行をたくさんするライブラリを書いているので、exec.Command
をしっかりと理解したいと思った。
様々な側面でその挙動を見てみたい。
exec パッケージの概要
まずは公式の Package execの概要を訳してみよう。
Package exec は外部コマンドを実行します。
os.StartProcess
をラップして、簡単にstdin
とstdout
リマップしたり、パイプで、 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.ReadCloser
を Wait()
が閉じてしまうためです。他にも 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
まとめ
大体やりたいことがカバーできたので、これで高速にコーディングできそうです。