bashのFD(file descriptor)操作について
- 前置き
- よく使われる例
> - よく使われる例
&> &の用例>&の曖昧動作- Redirection Operators の名前
- 右辺にファイルを指定する
Operatorの構文解析の特性 - 左辺(と右辺)で使われている 0,1,2 という数字の意味
- よくある例
command 1> all_log.txt 2>&1 - 標準入力に書き込む
- ファイルを開く操作と
file descriptor操作の区別 FD操作の寿命execによるfile descriptor操作の永続化
前置き
ここが分かりやすい。
Author: @ueokande
Title: シェルとファイルデスクリプタのお話
Date: 2016年12月01日
https://qiita.com/ueokande/items/c75de7c9df2bcceda7a9
リンク先文書や他文書を読んでも分からない人をターゲットに、各操作の分類と誤認識の修正を焦点に書く。
冗長なので斜め読み推奨。
|
!
|
この文書の説明はbashを対象にした説明であることに注意。他のシェルではしばしば上手く動かない。 |
よく使われる例 >
>(less-than sign)によるRedirecting Output
echo ok > result.txt
result.txtには"ok"が入る。
リダイレクト操作配置の自由度
以下はどれも同じ意味である。
echo ok >result.txt
echo > result.txt ok
> result.txt echo ok
echo ok 1> result.txt
echo 1>result.txt ok
など
よく使われる例 &>
&>: Redirecting Standard Output and Standard ErrorFD1とFD2の内容を合わせて右辺のfileへ書き込む。
右辺にFDを示す数値は指定出来ない。
右辺に数字を指定しても、それはfilepathとして扱われる。
左辺パラメーターはない。
|
*
|
&は>に付くオプションスイッチではない。&>でひとつのOperator。
|
リダイレクト操作配置の自由度
以下はどれも同じ意味。
echo ok &> filepath
&> filepath echo ok
echo ok > filepath 2>&1
echo ok > filepath 2>& 1
1> filepath echo ok 2>& 1
など
&の用例
&>(一括書き込み), &>>(一括追記), |&(一括パイプ)の3つでは、&はFD1とFD2の内容を合わせて扱うという意味を表現している。
>&(書き込みFD複製)と<&(読み込みFD複製)では、右辺がfile descriptorであるという意味を表現している(しかし例外あり。次項へ)。
>&の曖昧動作
>&(右&;書き込みFD複製)は、右辺が数値ではない場合に限って&>(左&;一括書き込み)と同じ意味を持つ。
右辺が数値の場合はfile descriptor操作として解釈される。
FD1(stdout)とFD2(stderr)を合わせて書き込む時は&>(左&)が推奨。
file descriptorの複製の場合は>&(右&)が推奨
Redirection Operators の名前
ここまでで説明しようとしてきたこと、また、以下のようなbash表現をRedirection Operatorと呼ぶ。
一つ一つのOperatorには動作の説明が付いているのみでbashもposixも名前を与えていない。
Redirecting InputならInput Redirectionと名詞形にすればいいだけの話だが、公式名ではないため安定感がない。
名前があれば調べる時に楽なのに。
-
<(greater-than sign;Redirecting Input) -
>(less-than sign;Redirecting Output) -
&>(ampersand, less-than sign;Redirecting Standard Output and Standard Error) -
>>(less-than sign, less-than sign;Appending Redirected Output) -
&>>(ampersand, less-than sign, less-than sign;Appending Standard Output and Standard Error) -
<&(greater-than sign, ampersand;Duplicating File Descriptors) -
>&(less-than sign, ampersand;Duplicating File Descriptors)
これらのOperator利用時に'quoteや"double-quoteで囲ってはならない。
囲った場合はコマンドに対する引数文字列として解釈される。
|
*
|
ヒアドキュメントも Redirection Operator だが、ここでは扱わない。いくつかの Operatorも省略してあることに注意。
|
右辺にファイルを指定するOperatorの構文解析の特性
<, >, >>, &> など。
(それぞれの)文章量の関係上 右辺、左辺の順で説明する。
filepathである右辺の表現
'quoteや"double-quote で囲ってもよい。
Operatorと右辺の間に空白文字があってもよい。
以下3つは等価。
echo aaa > file.txt
echo aaa >'file.txt'
echo aaa > "file.txt"
file descriptorである左辺の表現
左辺は省略可能なため、しばしば省略形式で使われている。
|
*
|
ちなみに&>には左辺は存在しないし指定できない
|
省略した場合の左辺値はそれぞれ下記。
-
<読み込み:0<; 右辺ファイルを読み込みopenして結果をFD0に格納 -
>書き込み:1>; 右辺ファイルを書き込みopenして結果をFD1に格納 -
>>追記:1>>; 右辺ファイルの追記openして結果をFD1に格納
左辺の構文解析
Operatorと左辺の間に空白文字を入れてはいけない。
空白文字を入れるとOperatorの左側にあってもOperatorへの引数としては扱われずに、コマンドの引数として扱われる。
以下2つは意味が違う
echo ok 1> filepath.txt # `filepath.txt`の中身は`"ok"`
echo ok 1 > filepath.txt # `filepath.txt`の中身は`"ok 1"
左辺(と右辺)で使われている 0,1,2 という数字の意味
いくつかの Redirection Operator の左辺や FD操作の両辺に現れるこれらの数値は何か?
これら3つの数値は FDfile-descriptorと呼ばれるもの。ちなみに3以上の数値の数値もFD。
0,1,2は、あらかじめ入出力ストリームが設定されているという点のみが特別。それ以外では、他のFD数値と同じように扱える。
他のFD(3以上の数値)は初期状態では空っぽの状態にある。
FD数値の上限値は(リソース制限・調査コマンドの)ulimit -nの値-1で求められる。この値は環境によって異なる。(たぶん 255 までならどの環境でも使える)
例:
ulimit -n→1024ならば1023まで使うことが出来る
なぜ0,1,2だけでなく3以上が用意されているか?
なぜなら使い途があるから。
標準入力・標準出力・標準エラー出力の3種類だけでは表現出来ないパイプライン構造・リダイレクト構造・入出力構造を表現するために使用出来るから。
ファイルをopenした結果を格納したり、既にあるFDの複製元・複製先として使う。
|
*
|
<, >, >> の左辺に来る数値はいずれもfile descriptor。
|
0,1,2にはあらかじめ以下のストリームが関連付けられている。
-
0:stdin(標準入力) -
1:stdout(標準出力) -
2:stderr(標準エラー出力)
(繰り返しになるが)これらの数値はマジックナンバーではない。
stdoutを指す3を作れるし、stdinを指す100を作れる。
0,1,2を閉じることも出来るし、0,1,2が指す先を変えることも出来る。
よくある例 command 1> all_log.txt 2>&1
標準出力と標準エラー出力の両方を同じファイルに出力したい場合の定型句。
cat not_found.txt 1> all_log.txt 2>&1 ([正の例] 2>&1が後にある)
なぜ
cat not_found.txt 2>&1 1> all_log.txt ([誤の例] 2>&1が先にある)
とは動作が違うか?
Redirection Operatorの評価順序
Redirection Operatorは左から右へ評価される。
動作
上記例で使われている2種類のOperator(>Output-Redirectionと>&^^)の動作を、C++言語風に説明すると以下の通り。
この疑似コード説明の焦点は、file descriptorとstreamの二重構造があるということ。
// [L]>filepath 形式
// left hand side, file descriptor number
// right hand side, filepath
void operator >(int left_fd=1, string right_path) {
// 右辺ファイルを書き込みでopenして左辺のFDに格納する。
BashFDs[left_fd].stream = open(right_path, WRITE | CREATE | TRUNCATE);
}
// [L]>&[R] 形式
void operator >&(int left_fd=1, int right_fd) {
// 右辺のFDの中身を左辺のFDへ
BashFDs[left_fd].stream = BashFDs[right_fd].stream;
}
なおFDの中にあるものを「ストリーム」や「ストリームハンドル」という名前で呼ぶことは一般的ではない。
FD操作を理解する際は>を入出力の向きの比喩として読まない方が理解しやすい。
>はファイルをopenする操作であることを理解すること。
上記の疑似コードは
streamが上書きされる時にcloseされることや、BashFDsの変更がコマンドライン1行で放棄されることは表現されていない。また、これは
bashにおけるfile descriptor操作のための比喩コードである。
Unix系OS一般におけるfile descriptorの比喩コードだと解釈してはいけない。
正の例の動作; 1> all_log.txt 2>&1
cat not_found.txt 1> all_log.txt 2>&1をC++風コードで挙動を書くと以下。
// ファイルを開いてストリームハンドルを FD1 に格納
BashFDs[1].stream = open("all_log.txt", WRITE | CREATE | TRUNCATE);
// FD2 に FD1 のストリームハンドルを複製
BashFDs[2].stream = BashFDs[1].stream;
// catコマンド実行
cat(BashFDs, "not_found.txt");
FD1のstreamにもFD2のstreamにも書き込みモードで開いた同じストリームが入っている。
期待通りに、標準出力とエラー出力に出力されるはずだった内容がファイルに書き込まれる。
誤の例の動作; 2>&1 1> all_log.txt
一方でcat not_found.txt 2>&1 1> all_log.txtをC++風コードで挙動を書くならば
// 初期状態では FD1 にはstdout(標準出力ストリームハンドル)が設定されいてる
// FD2 に FD1 のストリームハンドルを複製
BashFDs[2].stream = BashFDs[1].stream;
// ファイルを開いてストリームハンドルを FD1 に格納
BashFDs[1].stream = open("all_log.txt", WRITE | CREATE | TRUNCATE);
// catコマンド実行
cat(BashFDs, "not_found.txt");
FD1のstreamにはファイルへのストリームが入っているが、FD2にはstdoutが入ってしまっている。
後者の例が必要になる場合もあるが、標準出力もエラー出力も同じファイルに書き出したいという要求には一致しない。
二重オープン; さらに別の誤の例; 1> all_log.txt 2> all_log.txt
もう1例cat not_found.txt 1> all_log.txt 2> all_log.txtの場合をC++風コードで挙動を書く
BashFDs[1].stream = open("all_log.txt", WRITE | CREATE | TRUNCATE);
BashFDs[2].stream = open("all_log.txt", WRITE | CREATE | TRUNCATE);
// 以下省略
2回のopenとTRUNCATE(切り詰め)が行なわれてしまうし、2つのストリームが生成されてしまう。
そのために、同じファイルに出力とエラーを書き込もうとしているのに、1と2はseek位置を共有しない。
これを動作させた結果はややこしい。
>>追記, <読み込み, <&FD複製 の動作
他のRedirection OperationをC++風コードで挙動を書くならば
void operator >>(int fd=1, string path) {
// 追記モードで右辺ファイルをopenしてストリームハンドルを格納
BashFDs[fd].stream = open(path, WRITE | CREATE | APPEND);
}
void operator <(int fd=0, string path) {
// 読み込みモードで右辺ファイルをopenしてストリームハンドルを格納
BashFDs[fd].stream = open(path, READ);
}
// [n]<&[m]
void operator <&(int left_fd=0, int right_fd) {
// 左辺FDに右辺FDの中身を複製
BashFDs[left_fd].stream = BashFDs[right_fd].stream;
}
<&読み込みFD複製と>&書き込みFD複製は左辺省略時のデフォルト引数以外に違いはないことに注目。
両辺を明示するならば、この二つのOperatorsは同じ挙動をする。
標準入力に書き込む
環境依存
$ echo ok | tr -d 'o'
k
$ # tr によってoが消された
$ # FD1にstdinを割り当ててみる
$ echo ok 1>&0 | tr -d 'o'
ok
$ # oが表示される上にパイプを回避している
ファイルを開く操作とfile descriptor操作の区別
<,>,& といった文字を使った操作には大別して4種類ある。
<, >, >>, <> など
左辺にFD数値を、右辺にファイルパスを取る表現
ファイルをそれぞれの条件で開き左辺のFDに割り当てる。
左辺を省略した場合の初期値はそれぞれ 0<, 1>, 1>>, 0<>
<&, >&
左辺と右辺の両方に数値を取る表現(ここではこれをfile descriptor操作, FD操作と呼ぶ)
<&, >& ; 右辺のFDを複製して左辺に割り当てる。
左辺を省略した場合の初期値はそれぞれ 0<&, 1>&
&>, &>>
左辺なし、右辺にファイルパスを取る表現
上記の組み合わせを簡単に表現するためのもの。
<<, <<<
左辺にFD数値を、右辺以降にデータを表現するもの
Here Document, Here String。
説明省略。
区別まとめ
-
左辺に
FD数値を取るか、何も取らないか -
右辺に
FD数値を取るか、ファイルパスを取るか、データを取るか
FD操作の寿命
ここまでで説明したFD操作やリダイレクト操作は、指定したコマンドに対してしか効果がない。
以下に例を示すが、直感に反する動作はないと思う。
FD操作影響は次の行のコマンドには持ち越されないcommandA > result.txt
commandB
# commandB の標準出力は result.txt には入らない
;(semicolon)の左側に対する操作は、右側には影響しないcommandA > result.txt ; commandB
# commandB の標準出力は result.txt には入らない
|(vertical bar)の左側に対する操作は、右側には影響しないcommandA 2> error.txt | commandB
# commandA の標準エラー出力は result.txt に入る
# commandB の標準エラー出力は result.txt には入らない
|(vertical bar)の右側に対する操作は、左側には影響しないcommandA | commandB 1> all.txt 2>&1
# commandA の標準出力は commandB の入力に入る。
# commandA の標準エラー出力は端末の標準エラー出力に出る。
# commandB の標準出力と標準エラー出力は all.txt に入る
execを理解するための準備
当然、ファイルを取るリダイレクト操作はファイルをopenしている。
操作に応じてreadかwriteされて、コマンドの終了後にcloseする。
コマンドに直接リダイレクト操作を書く方法では、ファイルをcloseさせない方法(ファイルを開きっぱなしにする方法)は存在しない。
単純なリダイレクト操作では、いつopenされて、いつcloseされるかは気にしなくてもよい。
しかし、fifoをリダイレクトに使う際や、execを使う際には、openとcloseのタイミングを知っておくと動作が予測しやすい。
execによるfile descriptor操作の永続化
execは bash の built in command です。
exec自体もコマンドだが、execの引数もコマンドであるため用語曖昧さがある。
この項目では以降、単語「コマンド」を引数側に指定されるコマンドを指すものとしてのみ使う。
execの機能は2つある。そのうちの[リダイレクト操作の永続化]が理解しにくい。
機能1: 現在のbashプロセスをexecに続くコマンドで置き換える
exec以降にスクリプトやコマンドがあったとしても実行されない。
|
*
|
試しにexec catした後にCtrl+cで強制終了すると、bashに戻らないことを確認できる。
|
パイプと組み合わせた時の挙動は推測しにくいが、ここでは説明しない。
末尾呼び出し最適化を想定した機能。
以降この機能を「プロセス書き換えexec」と呼ぶ(が、もうこれ以降登場しない)。
|
*
|
bash の man で exec は it replaces the shell と説明されているため「置き換え」という語を使いたい。けれど、Process Substitution(日本語訳: プロセス置換)と紛らわしいため、区別のために「書き換え」という語をここでは使う。プロセス置換とexecによるプロセスの書き換えは違う機能である。
|
|
*
|
プロセス置換とは例えばcat <(sed -e 's/a/A/' < fileB) <(tr -d 'b' < fileD)のようなリダイレクト機能のこと。
|
機能2: 現在のbashプロセス内でリダイレクト操作の永続化
execにコマンドを指定せずにリダイレクト操作だけを指定すると、以降のコマンドにも指定したリダイレクト状態が影響するようになる。
これをリダイレクト状態が永続化すると呼ぶ。
(当然だが別プロセスのbashには影響しない)
Redirection Operatorsリダイレクト操作オペレーターはexecに対する引数としても、コマンドに対する引数としても解釈されない。
そのため、引数との順序関係を考慮する必要はない (リダイレクト操作同士の順序は前述の通りである。そちらは考慮する必要がある)。
リダイレクト操作永続化の用途
使ったらできること。
-
コマンドごとに指定していたリダイレクト操作指定を減らす。
-
間接的に指定する書き方によって、プログラミング適性を(わずかに)上げる。
-
fifoをopenしたままにして、そこへ複数のコマンド結果を入れる。-
fifoは書き込み側closeするとそれが読み込み側に伝わるため、これを抑制するのに必要になる(複数の書き込み側テクニックを使うことにより、閉じないようにする方法もある)。
-
-
書き込みリダイレクトを複雑に結合する
-
ファイルを開く閉じるが繰り返されてもいいならば追記書き込み
>>で足りる。また、単純な例ならば(),{}で囲えば足りる。これら以外の場合にだけ必要になる。
-
-
読み込みリダイレクトを複雑に結合する。
-
一度開いたストリームは、seek位置を保持し続けるため、コマンドAに読ませる、その続きをコマンドBに読ませる といったことが出来る(どれだけのデータを読み出すかは各コマンド側が決める)。
-
-
実行中の
bashの端末・仮想端末を別のものに切り替える; あるいは複数の仮想端末を現bashにつなぐ。
bashを動作させたままに別の標準入力ストリーム・標準出力ストリーム・標準エラーストリームを0,1,2に割り当てることが出来る。
これはつまり、別の(仮想)端末に現在のbashを移動させることが出来ることを意味する。
|
*
|
プログラミング言語の定番機能であるファイル入出力機能のopenにあたる機能があるから、bashにもseek機能があるかと期待するが、この機能は無い。
|
上のリストのうちexecでなければ出来ないことは、ファイルをopenしたままにすることと、仮想端末の変更だけ。
fifoとの組み合わせ
まずは比較のための非fifoなおかつexecを使わない例。
出力先が普通のファイルならば、以下2例は同じ結果になる(ファイルの中身が"xxxyyy"になる)。
# 分割の例
echo -n xxx > dest # 1行目
echo -n yyy >> dest # 2行目
# 結合の例
echo -n xxxyyy > dest
|
*
|
echo -nは末尾の改行を抑制するオプション
|
しかし、出力先がfifoだった場合(destがfifoだった場合)上記2例は等価ではない。
fifoの読み出し側が"xxx"が来た時点で end-of-file を理由としてcloseが起きる。
|
*
|
fifoの動作仕様は数行では説明できないため、別ページの[パイプの終了に関して]を参照のこと。
|
"xxxyyy"をfifoの読み出し側に届けたいのであれば、1行目で起きるclose(書き込み側)を抑制する必要がある。
そのための方法その1は括弧()parenthesisや{}curly-braceで囲うこと。
# 括弧の例
{
echo -n xxx
echo -n yyy
} > dest
方法2はexecを使うこと
# execの例
exec 1> dest # open
echo -n xxx
echo -n yyy
exec 1>&- # close
# after
方法2(execの例)の after 以降で何かコマンドを実行するとFD1がcloseしていてどこにも繋がっていないためエラーする。
$ echo zzz
bash: echo: write error: Bad file descriptor
このエラーを回避するには元々のFD1を保存して、後に元に戻す必要がある。
$ # execの例その2
$ exec 10>& 1 # copy(FD10 = FD1); FD10 に FD1 の中身を保存
$ exec 1> dest # open
$ echo -n xxx
$ echo -n yyy
$ exec 1>&10 # close(FD1); copy(FD1 = FD10)
$ exec 10>&- # close(FD10); してもしなくても良い
$ echo zzz
zzz