パイプ(Shell)

このエントリーをはてなブックマークに追加

パイプはUnixユーザならば誰でも普段から使う機能です。例えば、"ls|grep string"のようなコマンドです。この"|"をパイプと言いますが今回はこのパイプについて勉強します。
まず、前回までの流れを思い出してください。lsはprintf(3)等で出力するだけですが、grepはlsの出力結果を入力に使います。このような場合には問題が発生しますので問題とその対策を見ていきます。

入力側

ではまず、入力側から見ていきます。入力側はlsとcatを使います。catは引数があればそれをファイルとしてファイルの中身を読み込んで表示します。ところが入力側がパイプの場合入力結果をファイルの代わりに使用します。

$ touch aaa bbb
$ ls
aaa bbb
$ ls|cat
aaa
bbb

ここで注目すべきは、lsの出力結果をパイプに渡すと出力結果が変わっている事です。これはgrep、sort、awk等のコマンドに渡す場合を想定しているからです。これらのコマンドは通常、行単位で処理を行いますが、lsの通常の動作はターミナルに合わせて人間が見やすいように出力しています。そのためにlsコマンドは出力側がパイプだった場合に動作を変えているのです。

stdinと"0"の動作テスト

ではまず実験してみましょう。前回の標準入力をそのまま使用します。ソースコードをもう一度見て、実行結果を確認してください。

//stdin.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	char buf[2];

	printf("input:");
	read(0, buf, 2);
	printf("get:%c\n", buf[0]);

	return 0;
}

動作を見て分かる通り、"0"は何もしなくてもパイプから標準入力として読み込んでくれます。stdinも同様です。つまり、入力側は通常のプログラムで問題ありません。
$ gcc -o stdin stdin.c
$ ./stdin
input:1
get:1
$ echo 2|./stdin
input:get:2

入力側がパイプかを判断する

出力側に入る前に、入出力側が通常の場合かパイプかを判断するする方法を紹介します。
これにはisatty(3)を使用します。動作は次のようになります。

#include <unistd.h>
if (isatty(ファイルディスクリプタ)) {
  //端末
}
else {
  //パイプ
}

ではstdinで試してみましょう stdinの場合はFILE*型なのでfilenoを使用する必要があります。"0"の場合はそのまま使えます。

//check_stdin.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
  if (isatty(fileno(stdin))) {
    printf("stdin:terminal\n");
  }
  else {
    printf("stdin:pipe\n");
  }
  if (isatty(0)) {
    printf("stdin:terminal\n");
  }
  else {
    printf("stdin:pipe\n");
  }

  return 0;
}

実行してみると、stdinも"0"でも正しく判断していることが分かります。
$ gcc -o check_stdin check_stdin.c
$ ./check_stdin
stdin:terminal
stdin:terminal
$ echo 2|./check_stdin
stdin:pipe
stdin:pipe


出力側

標準入出力のパイプかターミナルの判定方法が分かったので、出力側の動作を見ていきます。
ではまず最初にlsのようなプログラムを作ります。ファイルやディレクトリの操作はまだ理解していないので、lsの動作のように文字列をいくつか出力するだけの単純なプログラムです。

//like_ls.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
  char *buf[] = {"aaa", "bbb"};
  printf("%s %s\n", buf[0], buf[1]);

  return 0;
}

パイプ経由で実行すると、lsのような動作にはなりません。
$ gcc -o like_ls like_ls.c
$ ./like_ls
aaa bbb
$ ./like_ls | cat
aaa bbb

ではisatty(3)を使用するようにプログラムを変更します。

//like_ls2.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
  char *buf[] = {"aaa", "bbb"};
  if (isatty(fileno(stdout))) {
    printf("%s %s\n", buf[0], buf[1]);
  }
  else {
    printf("%s\n%s\n", buf[0], buf[1]);
  }

  return 0;
}

実行すると、lsのようにパイプの場合動作が変わっているのが分かります。
$ gcc -o like_ls2 like_ls2.c
$ ./like_ls
aaa bbb
$ ./like_ls2 | cat
aaa
bbb


まとめ

ひとまずこれで標準入出力についての解説を終わります。
いかがだったでしょうか?普段何気に使っているコマンドに標準入出力が深く関わっていることを理解できたと思います。これらの標準入出力は出力先を変更するだけでファイル入出力にも使えます。
ファイル入出力とともにこれらの入出力系処理はそのままUnix系OSの操作の奥義ともなりえます。これらの部分は次のファイルで見ていきます。