私たちは最近これを使ったいくつかの投稿を見ました。
var=$(</dev/stdin)
シェルの標準入力を変数として読みます。
しかし、少なくともLinuxベースのシステムとCygwinでは、これは正しいアプローチではありません。
なぜ?正しい方法は何ですか?
答え1
(いいえ、今回はそれと関係があります。周囲に引用符がありません。$(...)
1)。
$(<file)
オペレーター
Kornシェル演算子(zsh
およびサポートされているbash
)については、後で詳しく説明します。Bashのファイル読み取りコマンドの置き換えについて。
簡単に言えば、これは、$(cat < file)
ファイルの読み込みがcat
実行を必要とせず、シェルで内部的に実行されることを除いて、または追加のbash
プロセス²が必要であることを除いて、機能的に同じです²。
では実際に内蔵²と同じbash
です。$(cat < file)
cat
cat
bash
その他の制限もあります。これはstdin入力ファイルのリダイレクトにのみ適用され、$(<&3)
他の形式のリダイレクトには適用されません$(<<<foo)
。
/dev/標準入力
/dev/stdin
、プロセスのファイル記述子を名前で参照できるように、1980年代にさまざまなUnicesに追加された特殊ファイルです。/dev/stdout
/dev/stderr
/dev/fd/x
これらのUnicesで/dev/stdin
(文字デバイスファイル)を開くと、stdin(fd 0)と重複するファイル記述子が生成されるため、3を実行するのと同じですdup(0)
。
1990年代のLinuxに同様の機能が追加されたとき、実装方法はかなり異なり、互換性がありませんでした。
Linuxでは、これらのファイル/dev/std...
は/dev/fd/x
特殊文字デバイスファイルではなくへのシンボリックリンクです/proc/self/fd/x
。魔法のシンボリックリンクfdで開いたファイルにX。
だから/dev/stdin
別のもので開いてみてください。dup(0)
これを行う権限があると仮定すると、最初から始まり(現在のstdinが指すファイル内のオフセットではない)、要求されたモードで元のファイルを再び開きます。これはまた、fd 0から独立したfdで読み取り/書き込み/検索を実行しても、ファイルのstdinオフセットが更新されないことを意味します。
CygwinはLinuxのアプローチをコピーし、2000年代に同様の機能を追加しました。すべてではありませんが、他のほとんどのUnicesは生の方法で実行されます(/dev/fd/x
完全にサポートされている場合)。
それではなぜ間違っているのですか?
LinuxとCygwinでは、stdinから直接読み取るのではなく、結果のファイル記述子で読み取るために開いて読み取ることが$(</dev/stdin)
同じ/dev/stdin
ではないため、正しい内容を読み取ることができないか、まったく読み取れず、内容を知らせることができません。標準入力を読み取った残りのスクリプト。
次の例を考えてみましょう。
$ cat wrong
#! /bin/bash -
var=$(</dev/stdin)
printf 'I got: "%s"\n' "$var"
printf "This is how many bytes are left to read on stdin: "
wc -c
$ cat right
#! /bin/bash -
var=$(cat)
printf 'I got: "%s"\n' "$var"
printf "This is how many bytes are left to read on stdin: "
wc -c
$ cat file
1
2
3
4
5
$
$ ./wrong < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 10
$ ./right < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 0
この場合でも、wrong
すべてのstdin行を読むように見えますが、実際にそれを消費するようには見えません。wc -c
それでも10バイトを読むことができます。
$ { read var; ./wrong; } < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 8
$ { read var; ./right } < file
I got: "2
3
4
5"
This is how many bytes are left to read on stdin: 0
スクリプトの標準入力が最初の行を超えると呼び出されても、wrong
最初の行を取得する方法を学びます。file
$ socat -u file:file exec:./wrong
./wrong: line 2: /dev/stdin: No such device or address
I got: ""
This is how many bytes are left to read on stdin: 10
$ socat -u file:file exec:./right
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 0
wrong
/dev/stdin
ソケットなので開けないし開けません。開いている()アウトレット。
$ chmod 600 file
$ sudo -u other_user ./wrong < file
./wrong: line 2: /dev/stdin: Permission denied
I got: ""
This is how many bytes are left to read on stdin: 10
$ sudo -u other_user ./right < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 0
right
私が開いたfd 0で読んでいますが、次のように再開しよwrong
うとしています。file
他のユーザー誰がこのようなことをする権利はありません。
Linux/Cygwin では、パイプや tty などの一部の文字デバイスなど、開いている (ソケットではなく読み取り権限のある) 検索できないファイルを開くときなど、いくつかの簡単な$(</dev/stdin)
場合にのみ機能します。/dev/stdin
開く権限を持つ検索可能ファイルの先頭でstdinを開くなど、他のいくつかのケースでは、以下が発生する可能性があります。現れる動作しますが、入力は使用できません。
正しい方法
上記のように:
var=$(cat)
正しい方法です⁴。cat
fd 0(stdin)から読み込み、パイプであるfd 1に書き込みます。そして、シェルはもう一方の端の出力を読み込んで埋めます$var
。
cat
これを行う唯一のコマンドではありませんが、最も単純でオプションが渡されない限り、入力をテキストとして解釈したり変更したりすることはありません。
ksh93またはzshではvar=$(<&0)
これを行うことができますが(<&0
no-opで、1つ以上のリダイレクトが必要です)、ksh93はデフォルトでzsh
これを実行するため、最適化ではありませんvar=$($NULLCMD <&0)
。$NULLCMD
cat
テキスト入力(テキストにNUL文字が含まれていない)の場合、またはを使用してzsh
次のbash
ことを実行できます。
{ ! IFS= read -rd '' var; } < file
read
最初の NUL 区切り文字を読み取り、区切り文字が見つかった場合は成功を返します。ここでは区切り文字を探したくないので、終了状態を無効にします。file
これは、開くことができますが読み取れない場合、正しい終了ステータスを取得できないことを意味します。
追加の考慮事項
命令置換( $(cat)
)と$(<file)
演算子の削除みんな入力の末尾の改行文字です。したがって、技術的には、以降はvar=$(cat)
入力$var
全体を含まず、入力全体から末尾の改行文字を引いた値を含む。
入力全体に対して次のことができます。
var=$(cat; ret=$?; echo . && exit "$ret")
ret=$? var=${var%.}
(終了状態はcat
そのまま維持されます$ret
)。
ただしzsh
、入力に NUL バイトがある場合、$var
他のシェルがこれらのバイトを変数に格納することをサポートしていないため保存されません。
$ printf 'a\0b' | ksh -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <a>$
$ printf 'a\0b' | mksh -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <ab>$
$ printf 'a\0b' | bash -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
bash: line 1: warning: command substitution: ignored null byte in input
Got: <ab>$
$ printf 'a\0b' | dash -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <ab>$
$ printf 'a\0b' | zsh -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <a\000b>$
ここで、1は$(...)
リストコンテキストではなくスカラー(配列ではない)変数の割り当てに使用されるため、拡張時に分割+globは発生しません。したがって、問題にはなりませんが、周囲に引用符を追加しても$(<...)
違いはありません。
² 別の違いは、最新バージョンの zsh を除くすべてのバージョンでは、読み取りエラーが自動的に無視されることです。var=$(</); echo "$? <$var>"
たとえば、エラーは報告されませんが、bash(ksh93またはmkshとは反対)はゼロ以外の終了ステータスを返します。
³ 少なくとも fd が開いたモードと互換性のあるモードでファイルを開く限り。exec >/dev/stdin
たとえば、stdin(fd 0)は読み取り専用モードで開くと通常は機能しません。
⁴標準ですが、$(<file)
ksh / zsh / bashでのみ見つかり、/dev/stdin
すべてのUnicesでは見つかりません。