Edited at

/dev/stderr(/dev/std{in,out}も)は使うべきではない

More than 3 years have passed since last update.


実験してみよう

Linuxで、一般ユーザー権限と、root権限のどちらも所有しているホストがある人は次の操作を試してみてもらいたい。

Using username "GENERAL_USER".                         ← 1)一般ユーザーでログイン

GENERAL_USER@your.server's password:
Last login: Mon Jul 13 00:00:00 2015 from your.client
$ su - ← 2)rootになる
Password:
# su -m apache ← 3)別の一般ユーザー(例えばapache)になる
bash: /root/.bashrc: Permission denied ← (とりあえずこのエラーは気にしない)
$ echo HOGE >/dev/stderr ← 4)標準エラー出力に文字列を出力してみる
??? ← (果たしてどういう挙動を示すか?)
$ echo HOGE >/dev/stdout ← 5)ついでに標準出力にも出力してみる
??? ← (これも果たしてどういう挙動を示すか?)
$


予想される挙動

恐らくパーミッションがないと言われて失敗するだろう。

$ echo HOGE >/dev/stderr

bash: /dev/stderr: Permission denied
$ echo HOGE >/dev/stdout
bash: /dev/stdout: Permission denied
$


なぜそうなるのか?

おそらくLinuxカーネルの仕様の起因すると思うのだが、Linuxの/dev/std*多重のシンボリックリンクになっており、その実体のアクセス権限がないことが原因である。その様子はlsコマンドの-lオプションを使って追いかけていけば簡単に調べられる。

$ ls -l /dev/stderr

lrwxrwxrwx 1 root root 15 Jul 13 00:00 /dev/stderr -> /proc/self/fd/2
$ ls -l /proc/self/fd/2
lrwx------ 1 apache apache 64 Jul 13 12:34 /proc/self/fd/2 -> /dev/pts/0
$ ls -l /dev/pts/0
crw--w---- 1 GENERAL_USER tty 136, 0 Jul 13 12:34 /dev/pts/0
$

このように/dev/stderr/dev/std{in,out}も)の実体を辿ってみると、Linuxでは最終的に/dev/pts/0(ログイン中の仮想端末、0でない場合もある)にリンクされていることがわかる。ところが、これには最初にログインしたユーザー(及びttyグループ)しか出力が許可されない仕様になっている。

確かにこれでは失敗するはずだ。


代替策は?

とはいえ、/dev/std*を指定したいことはある。というわけでそういう時はどうすればよいのか紹介する。


/dev/stderrを指定したい場合

例えばコマンドの出力をデフォルトの標準出力ではなく標準エラー出力に書き出したいという場合は、コマンドの後ろに1>&2(デフォルトで2番に接続されている標準エラー出力を1番に複製する)をつければよい。

echo ERRMSG 1>&2 # このメッセージは標準エラー出力に出力される

もし各行末にこれを付けるのが大変なくらい行数が多いなどして、一時的に切り替えたいというのであれば、execコマンドを使って空いている番号(例えば3番)に退避しておき、元に戻すというやり方でもよい。

exec 3>&1 1>&2 # 標準出力(デフォルトで1番に接続)を3番に複製してから

# 標準エラー出力(デフォルトで2番に接続)を1番に複製
echo ERRMSG1 # ← この区間(3行)は
echo ERRMSG2 # ← 全て
echo ERRMSG3 # ← 標準エラー出力に出力される

exec 1>&3 3>&- # 3番に複製していた標準出力を1番に複製、その後3番を閉じる


/dev/stdoutを指定したい場合

基本的に、指定を省略すればよいだけだ。シェルはデフォルトの接続先をこれらに指定しているのだから。

echo MSG # 行末に">/dev/stdout"なんてわざわざ書かなければよいだけ

ただ先ほど標準エラー出力の例で示したように、一時的にデフォルトではない接続先(ファイル等)を指定して、後で元に戻したいという場合もあるだろう。

そんな時は同様に、execコマンドで空いている番号(例えば3番)に退避しておき、後で元に戻すようにすればよい。

exec 3>&1 >/PATH/TO/HOGEFILE # 標準出力(デフォルトで1番に接続)を3番に複製してから

# 1番に別のファイルを接続
echo MSG1 # ← この区間(3行)は
echo MSG2 # ← 全て
echo MSG3 # ← /PATH/TO/HOGEFILEに追記されていく

exec 1>&3 3>&- # 3番に複製していた標準出力を1番に複製、その後3番を閉じる


/dev/stdinを指定したい場合

これも通常は指定を省略すればよいだけだ。

cat # "</dev/stdin"の記述を省略すれば、デフォルトでは標準入力から読み込まれる

必要性は皆無に等しいが、標準入力もexecコマンドで空いている番号に退避させることができる。

exec 3>&0 </PATH/TO/HOGEFILE # 標準入力(デフォルトで0番に接続)を3番に複製してから

# 0番に別のファイルを接続
cat # ← ここでは/PATH/TO/HOGEFILEから読み込まれる

exec 0>&3 3>&- # 3番に複製していた標準入力を0番に複製、その後3番を閉じる


AWKの中ではどうすればいい?

/dev/std*は、AWKコマンドの中でも指定したい場合がある。しかし、AWKコマンドの中ではexecコマンドを使うことができないので先ほどの代替策は使えない。それでは一体どうすればよいのかというと、どうもしなくてよい。

なぜか、AWKの中では/dev/std*を指定してもPermission deniedにならない。試しに、冒頭で示した操作の6)番目として次のコマンドを打ち込んでみてもらいたい。何も怒られることなく、標準エラー出力が使える。

$ awk 'BEGIN{print "ERRMSG" > "/dev/stderr";}' ← 6)AWKの中から"/dev/stderr"に出力

ERRMSG
$