我们最近在这里看到了一些使用此的帖子:
var=$(</dev/stdin)
尝试将 shell 的标准输入读入变量。
然而,至少在基于 Linux 的系统和 Cygwin 上这不是正确的方法。
为什么?正确的方法有哪些?
答案1
(不,这一次,这与周围缺少引号$(...)
1).
运营$(<file)
商
Korn shell 运算符(也由zsh
和支持bash
)在以下位置进行了详细描述:了解 Bash 的读取文件命令替换。
简而言之,这在功能上等同于$(cat < file)
除了文件的读取是由 shell 在内部完成的,而不是要求cat
执行和 except in bash
,甚至不需要分叉额外的进程²。
在 中bash
,它实际上与 内置$(cat < file)
²相同。cat
cat
bash
还有其他限制,它仅适用于 stdin 输入文件重定向,而不适用于其他形式的重定向,如$(<&3)
或$(<<<foo)
。
/dev/标准输入
、和是80 年代添加到各种 Unice 中的特殊文件/dev/stdin
,因此可以通过名称引用进程的文件描述符。/dev/stdout
/dev/stderr
/dev/fd/x
在这些 Unices 上,打开/dev/stdin
(字符设备文件)会得到一个与 stdin (fd 0) 重复的文件描述符,因此相当于执行dup(0)
³。
当 Linux 在 90 年代添加类似的功能时,实现方式明显不同且不兼容。
在 Linux 上,这些/dev/std...
,/dev/fd/x
文件不是特殊的字符设备文件,而是指向 , 的符号链接/proc/self/fd/x
,而这些文件又是神奇的符号链接到 fd 上打开的文件X。
所以,在那里打开与;/dev/stdin
不同。dup(0)
假设您有权限这样做,它会重新打开原始文件,并且从头开始(不在 stdin 当前指向文件内的偏移量处)并处于请求的模式。这也意味着,如果您从独立于 fd 0 的 fd 中读取/写入/查找,则不会更新文件中 stdin 的偏移量。
Cygwin 复制了 Linux 的方式,在 2000 年代添加了类似的功能。大多数(如果不是全部)其他 Unice 都以原始方式运行(当它们/dev/fd/x
完全支持这些方式时)。
那么为什么会出错呢?
因为在 Linux 和 Cygwin 上$(</dev/stdin)
打开/dev/stdin
读取并从由此产生的文件描述符读取,而不是直接从 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 上,$(</dev/stdin)
仅适用于一些简单的情况:当在/dev/stdin
可打开(不是套接字,并且您具有读取权限)的不可查找文件(如管道和某些字符设备,如 tty)上打开时。对于其他一些情况,例如在您有权打开的可查找文件的开头打开 stdin 时,可能会出现以下情况:出现可以工作,但无法消耗输入。
正确的方法
如上所示:
var=$(cat)
是正确的方法⁴。cat
从它的 fd 0 (stdin) 读取并写入到它的 fd 1,这里是一个管道,而 shell 读取另一端的输出来填充$var
。
cat
不是唯一执行此操作的命令,但它是最简单的命令,当未传递任何选项时,它不会尝试将输入解释为文本,也不会修改它。
在 ksh93 或 zsh 中,您可以var=$(<&0)
这样做(<&0
作为无操作,但您至少需要一次重定向),但在 中zsh
,这不是一种优化,因为默认情况下它只是这样做var=$($NULLCMD <&0)
。$NULLCMD
cat
对于文本输入(文本不包含 NUL 字符),使用zsh
或bash
,您可以执行以下操作:
{ ! IFS= read -rd '' var; } < file
read
读取第一个 NUL 分隔符,如果找到分隔符则返回成功。在这里,我们不希望它找到分隔符,因此我们否定它的退出状态。这确实意味着,如果file
可以打开但无法读取,我们将无法获得正确的退出状态。
进一步的考虑
命令替换 ( $(cat)
) 和$(<file)
运算符删除全部输入中的尾随换行符。因此从技术上讲, after var=$(cat)
,$var
不会包含整个输入,而是包含整个输入减去尾随换行符。
对于整个输入,您可以执行以下操作:
var=$(cat; ret=$?; echo . && exit "$ret")
ret=$? var=${var%.}
(退出状态cat
保留在$ret
)。
除了 之外zsh
,如果输入中有 NUL 字节,它们将不会被保留,$var
因为没有其他 shell 支持将这些字节存储在其变量中。
$ 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 这里的$(...)
是在标量(不是数组)变量赋值中使用的,而不是在列表上下文中,因此在扩展时不会发生 split+glob。因此,虽然它们不会造成伤害,但在其周围添加引号$(<...)
并没有什么区别。
² 另一个区别是,除了 zsh 的最新版本之外,读取错误都会被默默地忽略。var=$(</); echo "$? <$var>"
例如,不会报告错误,但 bash(与 ksh93 或 mksh 相对)确实返回非零退出状态。
³ 至少只要文件以与打开 fd 的模式兼容的模式打开即可。exec >/dev/stdin
例如,如果 stdin (fd 0) 以只读模式打开,则通常无法工作。
⁴ 和 是标准的,相比之下,$(<file)
它仅在 ksh/zsh/bash 中找到,并且/dev/stdin
并非在所有 Unices 上找到。