Go の exec.Commandを調査する

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

# exec パッケージの概要

まずは公式の [Package exec](https://golang.org/pkg/os/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でも動作しません。

* [glob](https://golang.org/pkg/path/filepath/#Glob)

# 簡単な使用方法

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

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

```go
	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`の後は、通常シングルクオートでくくりそうなものですが、くくっていないところが面白いです。

```go
	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](https://stackoverflow.com/questions/66610827/how-to-pass-single-quote-to-the-exec-command-on-go-lang)

```go
	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()` が使えます。なかなか便利そう。

```go
	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: 
```

# パイプをつかいたい

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

```go 
	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
```

これで完璧

```go
	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` は使えないということになりますね。 

```go
	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()` という関数もあるようです。なぜかコマンドの絶対パスを返しています。

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

_result_

```
hello ps:
/usr/bin/ps -ef
```

# まとめ

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