bashの入出力を勉強してみた

bashの入出力について勉強してみました。

はじめに

bashの入出力のことはファイルディスクリプタとも呼ばれています。

ファイルディスクリプタは下記のサイトで分かりやすく説明されています。

絵で見てわかるファイルディスクリプタ・パイプ・リダイレクト – あしのあしあと

上記を踏まえた上で、bashの入出力について整理してみました。

ファイルディスクリプタ

コマンドはファイルディスクリプタを通して実施されます。

通常のコマンドは0~2のファイルディスクリプタを使用します。
各ディスクリプタの担当は以下の通りです。

ファイルディスクリプタ 抽象機能 シェルのデフォルト割り当て
0 入力 標準入力 (大抵はキーボードなどで入力)
1 非エラー出力 標準出力 (大抵は画面)
2 エラー出力 標準エラー出力 (大抵は画面)

各コマンドのプログラムは、ファイルディスクリプタを抽象機能として利用します。
例えば、エラーメッセージを出す場合は、ファイルディスクリプタ2 に送ります。

ファイルディスクリプタ入出力の架け橋はシェルが行っています。

ファイル入出力

先に挙げた3つのデフォルト割り当てでは、ファイルへの入出力ができません。
ファイルへの入出力を制御するには、コマンドで記述する必要があります。

ファイルへの出力

下記のコマンドは、ファイルディスクリプタ1の割り当てをoutput.txtに変更しています。

$ echo 'hoge' 1> output.txt

echo 'hoge'の実行結果はファイルディスクリプタ1へ出力されますが
それが標準出力ではなく、output.txtへのファイル出力となります。

なお、1>は例外的に>と省略表記することができます。

$ echo 'hoge' > output.txt

いつものやつになりましたね。

ディスクリプタの割り当てを変更するのは 1> output.txt の部分です。
なので、邪道ですが以下のコマンドでもOKです。

# ディスクリプタの割り当て変更
$ 1> output.txt echo 'hoge'
# 勿論、省略も可能
$ > output.txt echo 'hoge'

この知識は一見不要に見えますが、後で説明する n>&m で必要になります。

ファイルへの入力

下記のコマンドは、ファイルディスクリプタ0の割り当てをinput.txtに変更しています。

$ wc -l 0< input.txt

出力同様、0のみ省略が可能で見慣れた形になります。

$ wc -l < input.txt

input.txt が以下の内容と仮定して話を続けます。
コマンドの意味が分からない人は記事末尾の付録 (ヒアドキュメント)を参照して下さい。

$ cat > input.txt << EOF
> b
> a
> c
> EOF

標準入力にファイルを指定した場合の出力結果です。

$ wc -l 0< input.txt
3

標準入力に直接記述する場合は以下の様になります。
cの後に Ctrl+D を押しています。

$ wc -l
b
a
c
3

一方、以下の記述でもほぼ同様の結果を得ることができます。

$ wc -l input.txt
3 input.txt

この場合、指定された input.txt は標準入力ではありません。
input.txt は引数($1)です。
wcコマンドが標準入力とは別に、引数($1)を解釈して別の処理を行っています。

他いくつかのbashコマンド、例えばcatやheadなども同じです。
標準入力ではなく引数を指定しても、上手く動くように作られています。

なお、パイプが受け取るのは標準入力ですので混乱しないよう気をつけて下さい。

出力を捨てる

場合によって、エラー出力を表示したくない場合もあるでしょう。
その場合は割り当てを /dev/null にすればOKです。

# 存在しないファイルをcat -> 標準エラー出力として画面に表示
$ cat none
cat: none: No such file or directory
# 存在しないファイル -> /dev/null なので捨てられる
$ cat none 2> /dev/null

余談ですが、標準入出力の場合は以下を割り当てています。

$ ll /dev | grep std
lrwxrwxrwx  1 MAMAN なし       15 7月   9 21:24 stderr -> /proc/self/fd/2
lrwxrwxrwx  1 MAMAN なし       15 7月   9 21:24 stdin -> /proc/self/fd/0
lrwxrwxrwx  1 MAMAN なし       15 7月   9 21:24 stdout -> /proc/self/fd/1

私の端末の場合、いずれも最終的には /dev/pty0 を向いていました。

ファイルディスクリプタの割り当てコピー

非エラーをsuccess.log、エラーをerror.logに出力したい場合は以下の様になります。

$ ls -l 1> success.log 2> error.log

ただ、非エラー出力とエラー出力同じexec.logに出力したいケースもあると思います。
その場合は以下の様にします。

$ ls -l 1> exec.log 2>&1

n>&m の意味は以下の通りです。

  • ファイルディスクリプタ2 の割り当てを ファイルディスクリプタ1 の割り当てと同じにする

上記コマンドは以下のことをしています。

  • ファイルディスクリプタ1 の割り当てを exec.log にする
  • ファイルディスクリプタ2 の割り当てを ファイルディスクリプタ1 の割り当てと同じにする
    exec.log にする

ファイルディスクリプタn と記述するのは面倒なので、以降は抽象機能名を使います。
例えば、ファイルディスクリプタ1非エラー出力と呼びます。

ファイルディスクリプタのコピーは参照コピーか?

n>&m は割り当てのコピーであり、割り当て参照のコピーではありません。
例えば、以下のケースを考えてみます。

$ ls -l 1> exec.log 2>&1 1> exec2.log

1> exec.log 2>&1 では非エラー出力エラー出力exec.log になっていますね。
しかし、その後に 1> exec2.log非エラー出力exec2.log に切り替えています。
この時、エラー出力exec2.log に切り替わるのでしょうか?

結論を言うと、参照コピーではないため切り替わりません。
試してみて下さい。

ファイルディスクリプタの割り当て順序

ファイルディスクリプタの割り当て切り替えは、コマンドで記述した順序になります。
以下の例を見てみましょう。

$ ls -l 1> exec.log 2>&1

この例では、非エラー出力exec.log になっています。
その後、2>&1エラー出力exec.log になります。

一方、以下のケースはどうでしょうか?

$ ls -l 2>&1 1> exec.log

この例では、エラー出力が標準エラー出力になっています。
非エラー出力exec.log になります。
なぜなら、2>&1 でコピーした時点では、非エラー出力の割り当ては標準出力だからです。

付録 (ヒアドキュメント)

本記事では入力に使うファイルの中身をコマンドで記述しました。
その時にヒアドキュメントを使用します。

例えば、以下の様なファイルhoge.txtがあったとします。

hoge1
hoge2
hoge3
hoge4
hoge5

viを開いたり、リダイレクトを複数回繰り返して作成してもいいですが
説明をスムーズにするため、以下の様な記述をします。

$ cat > hoge.txt << EOF
hoge1
hoge2
hoge3
hoge4
hoge5
EOF

できあがったhoge.txtを開いてみましょう。

$ cat hoge.txt
hoge1
hoge2
hoge3
hoge4
hoge5

また、ヒアドキュメント内で変数を使う場合はコーテーションを利用します。

# $TERMを記載すると変数展開される
$ cat > hoge.txt << EOF
> $TERM
> EOF
$ cat hoge.txt
xterm

# EOFをコーテーションで囲むと変数展開されない
$ cat > hoge2.txt << 'EOF'
$TERM
EOF
$ cat hoge2.txt
$TERM

# コーテーションで囲まなくても、\でエスケープすれば変数展開されない
$ cat > hoge3.txt << EOF
\$TERM
EOF
$ cat hoge3.txt
$TERM

総括

bashの入出力について整理してみました。

より詳しい内容はオライリー本などを読むことをオススメします。
その障壁が少しでも下がれば幸いです。

コメントを残す