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つの数値は FD
file-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