前略
とある日、Bashのリダイレクト「>&2」の「&」とは何かと質問されました。
その時、マニュアルに載っていることを噛み砕いて説明したり、動作を見せたりしました。
しかし、なぜそのような動作をするのか説明できませんでした。私はマニュアルを読んで納得していた気分になっていたことを自覚しました。
そこで、リダレクトに関係する部分のソースコード(bash-5.0)を読んで、なぜそのような挙動になっているのか確認しました。
リダイレクトとは?
まずはリダレクトとは何かの説明をします。
Bashのマニュアルには以下のように書かれています。
Before a command is executed, its input and output may be redirected using a special notation interpreted by the shell. Redirection allows commands’ file handles to be duplicated, opened, closed, made to refer to different files, and can change the files the command reads from and writes to.
つまり、Shell上で特別な意味を持つ記号によって入力や出力を複製したり、他のファイルに書き込んだりする動作、ということですね。
リダイレクトを使えば、特定のファイルに書かれていることを入力値として使ったり、コマンドの実行結果をファイルに書き出したり、/dev/null(ゴミ箱)に吐き出すことができます。
「>&2」とは?
「>&2」の意味を手っ取り早く知りたい人のために、ここで書いておきます。
「>&2」はコマンドの実行結果を標準出力から標準エラーにコピーするという意味です。
ここからは、一つ一つの記号の意味を掘り下げていきたいと思います。
用語の都合上「2」→「>」→「&」の順で説明していきます。
「2」という特殊な数値
「2」はファイルディスクリプタ(以下FD)です。
FDとはプロセスやプログラムが特定のファイルにアクセスするための識別子です。識別子の正体は連番の整数です。
全てのプロセスにおいて0〜2のFDは予め以下のように決まっています。
FD | 名前 |
---|---|
0 | 標準入力(/dev/stdin) |
1 | 標準出力(/dev/stdout) |
2 | 標準エラー(/dev/stderr) |
Ubuntu18.04で確認してみた結果です↓
$ ls -l /dev | grep std
stderr -> /proc/self/fd/2
stdin -> /proc/self/fd/0
stdout -> /proc/self/fd/1
既にお分かりだと思いますが、「2」はFDで標準エラーを意味します。
「>」の意味
標準出力や標準エラーをリダイレクト先に書き出すという意味です。
parse.y
に処理内容が書かれています。
注) ①などのコメントは説明の便宜上付け加えたものです。本物のソースコードには記載されていません。
/* ① */
'>' WORD
{
source.dest = 1;
redir.filename = $2;
$$ = make_redirection (source, r_output_direction, redir, 0);
}
/* ② */
NUMBER '>' WORD
{
source.dest = $1;
redir.filename = $3;
$$ = make_redirection (source, r_output_direction, redir, 0);
}
変数・関数 | 説明 |
---|---|
source.dest | 本来の出力先のFD |
redir.filename | リダイレクト先のファイル名 |
make_redirection | 与えられた引数によってREDIRECT構造体を生成する関数 REDIRECT構造体によって、どのような動作をするか決まる |
① '>' WORD
>
の前に何もなければ標準出力をWORD、つまり指定されたファイルに書き出します。
make_redirection
にr_output_direction
を第二引数で渡しています。それを渡すことによってFDにフラグを設定することができます。該当する処理部分は以下です。
case r_output_direction: /* >foo */
case r_output_force: /* >| foo */
case r_err_and_out: /* &>filename */
temp->flags = O_TRUNC | O_WRONLY | O_CREAT;
break;
リダイレクト先のファイルを操作するためにFDには3つのフラグが設定されます。
フラグ | 説明 |
---|---|
O_TRUNC | ファイルが存在していれば、ファイル長を0に切り詰める 既に書かれていた内容は消える |
O_WRONLY | 書き込み権限のみ付与(ファイルの中身を読み込めない) |
O_CREATE | ファイルがないなら作成する |
② NUMBER '>' WORD
>
の前に数字があればFDとして扱い、FDに書き出される内容をWORD(ファイル)に書き込みます。ファイルを操作するためのFDには①と同様なフラグが設定されます。
仮に>
の前に数字ではなく文字列があった場合はどうなるのでしょうか?
$ echo test a>b
$ cat b
test a
結果から分かる通り、>
の前の文字列もWORD(ファイル)に書き込まれます。
##「&」とは何なのか
結論からいうと、「&」のみでは意味を持ちません。&
の前後につく記号・数値とセットになって意味を持ちます。
「>&2」では「>&」のセットでようやく意味を持ちます。このときの「&」はFDとファイル名を区別するための識別子みたいなものです。「&」の後ろにWORD(ファイル名)があると「>&」は別の意味を持ちます。話がややこしくなるので、「>&2」のケースだけに絞って説明していきます。
まずはparse.y
に書かれている内容を確認しましょう。
GREATER_AND NUMBER
{
source.dest = 1;
redir.dest = $2;
$$ = make_redirection (source, r_duplicating_output, redir, 0);
}
STRING_INT_ALIST other_token_alist[] = {
/* Multiple-character tokens with special values */
(省略)
{ ">&", GREATER_AND },
(省略)
}
本来の出力先はFD「1」なので標準出力、リダイレクト先のFDは「>&」の後にある数値となります。今回のケース「>&2」ではFD「2」(標準エラー)がリダイレクト先です。
make_redirection
の第二引数ではr_duplicating_output
が渡されています。
case r_deblank_reading_until: /* <<-foo */
case r_reading_until: /* << foo */
case r_reading_string: /* <<< foo */
case r_close_this: /* <&- */
case r_duplicating_input: /* 1<&2 */
case r_duplicating_output: /* 1>&2 */
break;
ソースコードから分かる通り、フラグの設定は何もしていません。なぜならリダイレクト先の標準エラーにはFD「2」が既に割り当てられており、追加で設定すべきものもないからです。
次に具体的な動作の違いを見てみます。
$ echo hello >2
$ cat 2
hello
$ rm 2
$ echo hello >&2
hello
$ cat 2
cat: 2: No such file or directory
「&」をつけない時はWORDとして判断されて、2
というファイル名に標準出力がリダイレクトされています。一方で「&」をつけるとFDの数値として扱われてファイルの作成は行われません。
このように「&」があるかどうかで後に続く文字や数値の意味合いも変化することがわかったと思います。
まとめ
「&」は単体では意味を持たず、前後の記号・数値とセットになって初めて意味を持ちます。
「>&2」では、「>」と「2」のセットで標準出力を標準エラーにコピーするという意味になります。
「>&2」のケース以外でも様々なところで「&」は使用されています。その他のパターンを知りたい人はマニュアルやソースコードを読んでみて下さい。