bash 文件重定向到标准 in 与 Linux 上的 shell (`sh`) 有何不同?

pop*_*nja 9 bash io-redirection dash stdin

我写了一个脚本,它在运行时切换用户,并使用文件重定向到标准输入来执行它。所以user-switch.sh是......

#!/bin/bash

whoami
sudo su -l root
whoami
Run Code Online (Sandbox Code Playgroud)

并运行它bash给了我我期望的行为

$ bash < user-switch.sh
vagrant
root
Run Code Online (Sandbox Code Playgroud)

但是,如果我用 运行脚本sh,我会得到不同的输出

$ sh < user-switch.sh 
vagrant
vagrant
Run Code Online (Sandbox Code Playgroud)

为什么bash < user-switch.sh给出的输出与sh < user-switch.sh?

笔记:

  • 发生在运行 Debian Jessie 的两个不同的机器上

ilk*_*chu 12

类似的脚本,没有sudo,但结果类似:

$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta
Run Code Online (Sandbox Code Playgroud)

使用bash,脚本的其余部分作为 的输入sed,使用dash,shell 解释它。

strace在这些上运行:dash读取脚本块(此处为 8 kB,足以容纳整个脚本),然后生成sed

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 8192) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...
Run Code Online (Sandbox Code Playgroud)

这意味着文件句柄在文件的末尾,sed不会看到任何输入。其余部分在dash. (如果脚本长于 8 kB 的块大小,则剩余部分将由 读取sed。)

另一方面,Bash 会返回到最后一个命令的末尾:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 36) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
...
lseek(0, -7, SEEK_CUR)                  = 29
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...
Run Code Online (Sandbox Code Playgroud)

如果输入来自管道,如下所示:

$ cat script.sh | bash
Run Code Online (Sandbox Code Playgroud)

无法进行重绕,因为无法找到管道和插座。在这种情况下,Bash 回退到一次读取一个字符以避免过度阅读。( fd_to_buffered_stream()ininput.c ) 对每个字节进行完整的系统调用原则上不是很有效。在实践中,我不认为读取将是一个很大的开销,例如与 shell 所做的大多数事情都涉及产生全新进程的事实相比。

类似的情况是这样的:

echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'
Run Code Online (Sandbox Code Playgroud)

子shell 必须确保read只读取第一个换行符,以便head看到下一行。(这也适用dash。)


换句话说,Bash 增加了更多长度以支持读取脚本本身以及从中执行的命令的相同源代码。dash没有。在 Debian 中打包的zsh, 和ksh93与 Bash 一起使用。

  • 是的,我在阅读这个问题时的反应令人惊讶的是它 * 确实 * 与 bash 一起工作——我把它归档为绝对行不通的东西。我想我只对了一半:) (2认同)

Gil*_*il' 12

shell 正在从标准输入读取脚本。在脚本中,您运行一个命令,该命令也想读取标准输入。哪个输入会去哪里?你不能可靠地分辨

shell 的工作方式是读取源代码块,解析它,如果找到完整的命令,则运行该命令,然后继续处理块的其余部分和文件的其余部分。如果块不包含完整的命令(末尾有一个终止字符——我认为所有 shell 都读到一行的末尾),shell 读取另一个块,依此类推。

如果脚本中的命令试图从 shell 从中读取脚本的同一个文件描述符中读取,那么该命令将找到它读取的最后一个块之后的任何内容。这个位置是不可预测的:它取决于 shell 选择的块大小,这不仅取决于 shell 及其版本,还取决于机器配置、可用内存等。

在执行命令之前,Bash 在脚本中寻找命令源代码的结尾。这不是您可以指望的,不仅因为其他 shell 不这样做,而且因为这仅在 shell 从常规文件中读取时才有效。如果外壳正在从管道(例如ssh remote-host.example.com <local-script-file.sh)中读取数据,则已读取的数据将被读取并且不能未读取。

如果要将输入传递给脚本中的命令,则需要显式执行此操作,通常使用here document。(here 文档对于多行输入通常是最方便的,但任何方法都可以。)您编写的代码仅适用于少数 shell,仅当脚本作为输入从常规文件传递给 shell 时;如果您希望第二个whoami将作为输入传递给sudo …,请再想一想,请记住,大多数情况下脚本不会传递给 shell 的标准输入。

#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF
Run Code Online (Sandbox Code Playgroud)

请注意,这十年,您可以使用sudo -i root. 跑步sudo su是过去的黑客。