var=$(</dev/stdin) 将 stdin 读入变量有什么问题?

Sté*_*las 44 bash ksh zsh io-redirection stdin

我们最近在这里看到了一些使用此的帖子:

var=$(</dev/stdin)
Run Code Online (Sandbox Code Playgroud)

尝试将 shell 的标准输入读入变量。

然而,至少在基于 Linux 的系统和 Cygwin 上这不是正确的方法。

为什么?正确的方法有哪些?

Sté*_*las 62

(不,这一次,这与 \xc2\xb9周围缺少的引号$(...)无关)。

\n

$(<file)运营商

\n

Korn shell 运算符(也受zsh和支持)在了解 Bash 的读取文件命令替换bash中有详细描述。

\n

简而言之,这在功能上等同于$(cat < file)除了文件的读取是由 shell 在内部完成的,而不是要求cat执行和 except in bash,甚至不需要分叉额外的进程\xc2\xb2。

\n

在 中bash,它实际上与 内置的\xc2\xb2$(cat < file)相同。catcat

\n

bash还有其他限制,它仅适用于 stdin 输入文件重定向,不适用于其他形式的重定向,例如$(<&3)$(<<<foo)

\n

/dev/标准输入

\n

/dev/stdin/dev/stdout是80 年代添加到各种 Unice 中的特殊文件/dev/stderr/dev/fd/x因此可以通过名称引用进程的文件描述符。

\n

在这些 Unices 上,打开/dev/stdin(字符设备文件)会得到一个与 stdin (fd 0) 重复的文件描述符,因此相当于执行dup(0)\xc2\xb3。

\n

当 Linux 在 90 年代添加类似的功能时,实现方式明显不同且不兼容。

\n

在 Linux 上,这些/dev/std...,/dev/fd/x文件不是特殊的字符设备文件,而是到 , 的符号链接/proc/self/fd/x,而这些文件又是在 fd x上打开的文件的魔术符号链接

\n

所以,在那里打开与;/dev/stdin不同。dup(0)假设您有权限这样做,它会重新打开原始文件,并且从头开始(不在 stdin 当前指向文件内的偏移量处)并处于请求的模式。这也意味着,如果您从独立于 fd 0 的 fd 中读取/写入/查找,则不会更新文件中 stdin 的偏移量。

\n

Cygwin 复制了 Linux 的方式,在 2000 年代添加了类似的功能。大多数(如果不是全部)其他 Unice 都以原始方式运行(当它们/dev/fd/x完全支持这些方式时)。

\n

那么为什么会出错呢?

\n

因为在 Linux 和 Cygwin 上$(</dev/stdin)打开/dev/stdin读取并从由此产生的文件描述符读取,而不是直接从 stdin 读取,这不是同一回事,因此您很容易最终无法读取正确的内容,或者失败完全读取任何内容并且无法告诉脚本的其余部分您已经读取了标准输入。

\n

考虑这些例子:

\n
$ cat wrong\n#! /bin/bash -\nvar=$(</dev/stdin)\nprintf \'I got: "%s"\\n\' "$var"\nprintf "This is how many bytes are left to read on stdin: "\nwc -c\n
Run Code Online (Sandbox Code Playgroud)\n
$ cat right\n#! /bin/bash -\nvar=$(cat)\nprintf \'I got: "%s"\\n\' "$var"\nprintf "This is how many bytes are left to read on stdin: "\nwc -c\n
Run Code Online (Sandbox Code Playgroud)\n
$ cat file\n1\n2\n3\n4\n5\n$\n
Run Code Online (Sandbox Code Playgroud)\n
$ ./wrong < file\nI got: "1\n2\n3\n4\n5"\nThis is how many bytes are left to read on stdin: 10\n$ ./right < file\nI got: "1\n2\n3\n4\n5"\nThis is how many bytes are left to read on stdin: 0\n
Run Code Online (Sandbox Code Playgroud)\n

看看即使在这种情况下,wrong似乎确实读取了标准输入的所有行,但它实际上看起来好像没有消耗它。wc -c仍然能够从中读取 10 个字节。

\n
$ { read var; ./wrong; } < file\nI got: "1\n2\n3\n4\n5"\nThis is how many bytes are left to read on stdin: 8\n$ { read var; ./right } < file\nI got: "2\n3\n4\n5"\nThis is how many bytes are left to read on stdin: 0\n
Run Code Online (Sandbox Code Playgroud)\n

了解如何wrong获取第一行,file即使当脚本的标准输入超过第一行时调用它。

\n
$ socat -u file:file exec:./wrong\n./wrong: line 2: /dev/stdin: No such device or address\nI got: ""\nThis is how many bytes are left to read on stdin: 10\n$ socat -u file:file exec:./right\nI got: "1\n2\n3\n4\n5"\nThis is how many bytes are left to read on stdin: 0\n
Run Code Online (Sandbox Code Playgroud)\n

wrong无法打开/dev/stdin,因为它是一个套接字,而您无法打开()套接字。

\n
$ chmod 600 file\n$ sudo -u other_user ./wrong < file\n./wrong: line 2: /dev/stdin: Permission denied\nI got: ""\nThis is how many bytes are left to read on stdin: 10\n$ sudo -u other_user ./right < file\nI got: "1\n2\n3\n4\n5"\nThis is how many bytes are left to read on stdin: 0\n
Run Code Online (Sandbox Code Playgroud)\n

right只是从我打开的 fd 0 中读取,但wrong正在尝试以无权这样做的其他用户file身份重新打开。

\n

在 Linux/Cygwin 上,$(</dev/stdin)仅适用于一些简单的情况:当在/dev/stdin可打开(不是套接字,并且您具有读取权限)的不可搜索文件(如管道和某些字符设备,如 tty)上打开时。对于其他一些情况,例如当在您有权打开的可查找文件的开头打开 stdin 时,它可能看起来可以工作,但无法使用输入。

\n

正确的方法

\n

如上所示:

\n
var=$(cat)\n
Run Code Online (Sandbox Code Playgroud)\n

正确的方式是\xe2\x81\xb4。cat从它的 fd 0 (stdin) 读取并写入到它的 fd 1,这里是一个管道,而 shell 读取另一端的输出来填充$var

\n

cat不是唯一执行此操作的命令,但它是最简单的命令,当未传递任何选项时,它不会尝试将输入解释为文本,也不会修改它。

\n

在 ksh93 或 zsh 中,您可以var=$(<&0)这样做(<&0作为无操作,但您至少需要一次重定向),但在 中zsh,这不是一种优化,因为默认情况下它就是这样var=$($NULLCMD <&0)做的$NULLCMDcat

\n

对于文本输入(文本不包含 NUL 字符),使用zshbash,您可以执行以下操作:

\n
{ ! IFS= read -rd \'\' var; } < file\n
Run Code Online (Sandbox Code Playgroud)\n

read读取第一个 NUL 分隔符,如果找到分隔符则返回成功。在这里,我们不希望它找到分隔符,因此我们否定它的退出状态。这确实意味着,如果file可以打开但无法读取,我们将无法获得正确的退出状态。

\n

进一步的考虑

\n

命令替换 ( $(cat)) 和运算符从输入中$(<file)删除所有尾随换行符。因此从技术上讲, after var=$(cat)$var不会包含整个输入,而是包含整个输入减去尾随换行符。

\n

对于整个输入,您可以执行以下操作:

\n
var=$(cat; ret=$?; echo . && exit "$ret")\nret=$? var=${var%.}\n
Run Code Online (Sandbox Code Playgroud)\n

(退出状态保留cat$ret)。

\n

除了 之外zsh,如果输入中有 NUL 字节,它们将不会被保留,$var因为没有其他 shell 支持将它们存储在变量中。

\n
$ printf \'a\\0b\' | ksh -c \'var=$(cat); printf "Got: <%s>\\n" "$var"\' | sed -n l\nGot: <a>$\n$ printf \'a\\0b\' | mksh -c \'var=$(cat); printf "Got: <%s>\\n" "$var"\' | sed -n l\nGot: <ab>$\n$ printf \'a\\0b\' | bash -c \'var=$(cat); printf "Got: <%s>\\n" "$var"\' | sed -n l\nbash: line 1: warning: command substitution: ignored null byte in input\nGot: <ab>$\n$ printf \'a\\0b\' | dash -c \'var=$(cat); printf "Got: <%s>\\n" "$var"\' | sed -n l\nGot: <ab>$\n$ printf \'a\\0b\' | zsh -c \'var=$(cat); printf "Got: <%s>\\n" "$var"\' | sed -n l\nGot: <a\\000b>$\n
Run Code Online (Sandbox Code Playgroud)\n
\n

\xc2\xb9 这里$(...)用于标量(而不是数组)变量赋值的值,而不是在列表上下文中,因此扩展时不会发生 split+glob 。因此,虽然它们不会造成损害,但在其周围添加引号并$(<...)没有什么区别。

\n

\xc2\xb2 另一个区别是,除了 zsh 的最新版本之外,读取错误都会被默默地忽略。var=$(</); echo "$? <$var>"例如,虽然 bash(与 ksh93 或 mksh 相对)确实以非零退出状态返回,但不会报告错误。

\n

\xc2\xb3 至少只要文件以与打开 fd 的模式兼容的模式打开即可。exec >/dev/stdin例如,如果 stdin (fd 0) 以只读模式打开,通常将无法工作。

\n

\xe2\x81\xb4 是标准的,相比之下,$(<file)它仅在 ksh/zsh/bash 中找到,并且/dev/stdin并非在所有 Unices 上找到。

\n