Go でコードを書いているが、Google に頼りすぎて嫌になって来たので、腰を落ち着けて、がっつり理解する事にした。やりたい事は外部コマンドの実行ではあるが、その前後に色々やりたいことがある。
- 外部コマンドが存在するか確認したい
- 外部コマンドを実行したい
- 外部コマンドに引き数は動的に設定したい
- 外部コマンドを実行中はインタラクティブに実行結果が表示されて欲しい
- 外部コマンドの成功失敗を取得したい
今回は、terraform のプロバイダを作ろうと思っているので、時間がかかる処理なので、実行中何も表示されないのは利用者としてきついと思うので、インタラクティブに実行結果が出て欲しい。作っているのはマルチプラットフォームで動作するCLIなので、Linux, Mac, Windows で動作して欲しい。
外部コマンドが存在するか確認したい
方法としては、コマンドを実行してみて、結果を見る方法。例えば、terraform
の実行に限って言えば、次の方法で良い。ちなみに、"--help" をつけないとどうなるか?というと、ステータスコード、127 で落ちた。コマンドが見つからないとのこと。
func isCommandAvailable(name string) bool {
cmd := exec.Command(name, "--help") // terraform works. it is not for everyCommand. Make it simple. it should depend on provider.
if err := cmd.Run(); err != nil {
fmt.Println(err.Error())
return false
}
return true
}
ちなみに、shell で実行ステータスコードを確認すると次のようになる。見た目はどちらもヘルプが出るので同じように見えるがステータスコードは異なっている様子。
--help
あり
$ terraform --help
:
$ echo $?
0
なし
$ terraform
:
$ echo $?
127
ちなみに、Windows の PowerShell では、次のようにしてステータスコードを同様に確認できる。
$lastExitCode
試してみたが結果は同じだった。
Terraform の実行に関してはこれで良い。ただし、他のコマンドでは--help
があるかわからないので、残念ながら汎用的ではない。
参考:(ただし、下記の方法は、Linux しか考慮していない)
-
[Golang] Check If A Program (Command) Exists
*Linx: 終了ステータス
*Windows: 1.10. Determine the Status of the Last Command
外部コマンドの実行
上記のコードでもすでに実行しているが、各種オプションに関して調査して見る。
コマンドの実行は、Command
関数を実行し、Cmd 構造体を取得、その関数を実行することで、得られる。
cmd := exec.Command("sleep", "1")
err := cmd.Run()
:
仕様はこちら
ちなみに、Run()
の他に、Start()
という関数もあるが、Run()
がコマンドの完了を待つのに対して、Start()
は待たないという違いがある。Start()
はその代わり、Wait()
という関数もあり、そちらは Start()
で始めたコマンドを Wait()
で待つということができる。つまり、コマンド実行の間他の処理を実施して、Wait()
で待つというコードを書ける。
外部コマンド実行のパラメータを動的に設定する
Command
の定義は、func Command(name string, arg ...string) *Cmd
となっており、可変長の引数を受け付ける (Variadic Functions) 関数の方では、スライスから値を受け取れるようになる。引き渡す方をスライスにするためにはどうしたらいいだろう?これは簡単でスライスに...
をつけて渡せば良い。
commandAndArgs := []string{
"init",
}
cmd := exec.Command("terraform", commandAndArgs...)
外部コマンド実行中にコマンド実行結果を表示する
terraform
のような実行時間がかかるコマンドに対しては、実行中の様子を表示して欲しいと思うだろう。Cmd
の中に Stdout io.Writer
と Stderr io.Writer
がある。 nil の場合は、NullDevice
に、io.Writer
を実装している os.File
を渡せば良い。ということは、Stdout
のデバイスファイルを渡せば良いということになる。
多分 Windows では実装が違うけど、こんな雰囲気で実装されているっぽい。
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
実際にうまくいったコード
commandAndArgs := []string{
"init",
}
cmd := exec.Command("terraform", commandAndArgs...) // ...enable us to pass them slice
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
他に便利コマンドとして、cmd.Output()
で標準出力が、cmd.CombineOutput()
関数で、標準出力とエラー出力の合わさったものが[]byte
に出力される。ただし、これは実行が終わった後になる。
エラー出力を画面出力すると同時に、バッファに出力してみる
ユーザーの利便性のために、画面出力すると同時に、ファイルや、バッファに出力したいという要望もあるかもしれない。その際は、io.MultiWriter
を使うと良さげ。
var buf bytes.Buffer
multiWriter := io.MultiWriter(&buf, os.Stdout)
cmd.Stdout = multiWriter
:
fmt.Println(buf.String())
コマンドの失敗成功を取得したい
コマンドの失敗成功は、Run() の戻り値で判断すると良い。
err := cmd.Run() // Run for Wait for the execution.
if err != nil {
panic(err)
}
例えば上記のコードでコマンド実行の戻り値が0以外だと次のようになる。err の中身はこのケースでは、exec.ExitError
になる様子。ただし、他のオブジェクトになるケースもあるので、素直にエラーコードを取得して終了するのが良さげ。
panic: exit status 127
goroutine 1 [running]:
main.main()
/Users/ushio/Codes/Strikes/src/github.com/TsuyoshiUshio/strikes/spikes/command/main.go:32 +0xa4e
ちなみに、StdoutPipe()
関数もあって、試してみたが、これを使うと、次のようになる。内部で、
cmd := exec.Command("terraform", commandAndArgs...)
stdout, err := cmd.StdoutPipe()
:
err = cmd.Start()
result,_ := ioutil.ReadAll(stdout)
fmt.Println(string(result))
if err = cmd.Wait(); err != nil {
:
まとめ
Go の exec
パッケージのコマンド実行に関してはかなりわかってきた。exec.Command
は組み込みのコマンドを実行する場合は一工夫必要という情報もある。自分はまずは、terraform
の実行なので、この程度の理解があれば、安全に実行できるだろう。