前書き:os.Args[1]にはパイプのデータがない
無名パイプは、Terminal上で用いる"|“の事です。例えば、以下の例ではechoコマンドがパイプで"PIPE test"を渡し、受け取り側のcatコマンドがパイプから受け取ったデータを標準出力しています。
$ echo "PIPE test" | cat
PIPE test
上記のcatコマンドのようにパイプからデータを受け取るには、「os.Args[1](コマンドライン引数の1つ目)を参照すれば良い」と私は勘違いしていました(正しくは標準入力からデータを受け取ります)。
本記事では、無名パイプ("|")からデータを受け取る実装例を紹介します。
無名パイプからデータを受け取る実装
以下に実装例と実行結果を示します。実装の詳細は、実行結果の後に後述します。
package main
import (
"fmt"
"io/ioutil"
"os"
"golang.org/x/term"
)
func main() {
if hasPipeData() {
data, _ := fromPIPE()
fmt.Print(data)
}
}
func fromPIPE() (string, error) {
if hasPipeData() {
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return "", err
}
return string(b), nil
}
return "", nil
}
func hasPipeData() bool {
return !term.IsTerminal(syscall.Stdin)
}
実行例は以下の通りです。
$ go build -o pipe main.go
$ echo "PIPE test" | ./pipe
PIPE test
$ ./pipe not_pipe
(注釈) 何も表示されない
実装説明:hasPipeData() 部分
golang.org/x/termのterminal.IsTerminal()は、引数で渡されたファイルディスクリプタ(今回は標準入力、fd=0)がターミナルからの入力かどうかを返します。以前は、golang.org/x/crypto/ssh/terminalのterminal.IsTerminal()で確認していたようですが、deprecated(非推奨)となっています。
func hasPipeData() bool {
return !term.IsTerminal(syscall.Stdin)
}
標準入力がターミナルかどうかを確認する理由は、下図の通りです。
パイプを用いて実行したかどうかによって、プロセスの標準入力(STDIN)がターミナルもしくはパイプ(他プロセスの標準出力)のどちらに結びつくのかが異なります。標準入力がターミナルと結びついていなければ、パイプからデータがあるとみなせます。

実装説明:fromPIPE() 部分
パイプからのデータは、標準入力を読み込む事によって取得できます。
func fromPIPE() (string, error) {
if hasPipeData() {
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return "", err
}
return string(b), nil
}
return "", nil
}
os.Args(プロセス実行時の引数)からパイプのデータが取得できなかった理由は、
- シェル(例:Bash)は、プロセスの標準入力を別プロセスの標準出力と紐付け
- シェルは、os.Args(コマンドライン引数)にパイプからのデータを追加しない
という挙動だからでしょう。
昔読んだ「Xinuオペレーティングシステムデザイン 改訂2版」のシェルを思い返すと、シェルがターミナルから渡された文字列をスタックにセットする事によって、プロセスに対してコマンドライン引数を渡していました。この挙動は、上記の1.〜2とほぼ一致します(補足:Xinuにパイプの仕組みはありません)。
後書き
GolangでBusyBoxのパクリを実装しています。その中で、「パイプからデータを受け取る処理」が登場したわけですが、実装してみて初めてパイプを正しく理解できた気がします。
