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


9

我编写了一个脚本,该脚本在运行时切换用户,并使用文件重定向到standard in来执行该脚本user-switch.sh

#!/bin/bash

whoami
sudo su -l root
whoami

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

$ bash < user-switch.sh
vagrant
root

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

$ sh < user-switch.sh 
vagrant
vagrant

为什么bash < user-switch.sh给出的输出不同于sh < user-switch.sh

笔记:

  • 发生在运行Debian Jessie的两个不同盒子上

1
不是一个答案,但对于sudo suunix.stackexchange.com/questions/218169/...
Kusalananda

1
/ bin / sh = Debian上的破折号?我无法在RHEL上进行复制,其中/ bin / sh = bash
Jeff Schaller

1
/bin/shDebian(我的副本)是Dash,是的。直到现在我什至不知道那是什么。到目前为止,我一直坚持使用bash。
popedotninja

1
同样,任何记录的语义也不能保证bash行为有效。
Charles Duffy

今天有人向我指出,我正在运行的脚本违反了bash打算如何使用的语义。这个问题有两个很好的答案,但是值得指出的是,一个人可能不应该在脚本中间切换用户。我最初尝试user-switch.sh在Jenkins的工作中执行脚本。在Jenkins外部运行相同的脚本会产生不同的结果,我求助于文件重定向以获取在Jenkins中看到的行为。
popedotninja

Answers:


12

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

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

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

随着bash,脚本的其余部分则作为输入sed,使用dash,外壳解释它。

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|...

这意味着文件句柄位于文件末尾,并且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|...

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

$ cat script.sh | bash

由于无法找到管道和承插口,因此无法进行倒带。在这种情况下,Bash会退回到一次读取一个字符来避免过度读取。(fd_to_buffered_stream()input.c)做一个完整的系统调用每个字节是不是在原则上是非常有效的。实际上,与外壳大多数情况确实涉及产生整个新进程这一事实相比,我认为读取操作不会带来太大的开销。

类似的情况是这样的:

echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'

子外壳程序必须确保read仅读取第一行换行符,以便head看到下一行。(这也适用dash。)


换句话说,Bash会花更多的时间来支持为脚本本身以及从脚本执行的命令读取相同的源。dash没有。的zsh,并ksh93封装在Debian中击走在这条。


1
坦白说,我对此感到有些惊讶。
ilkkachu

感谢您的好评!我将在strace以后使用这两个命令并比较输出。我没有使用那种方法。
popedotninja

2
是的,我对阅读此问题的反应令人惊讶,它确实适用于bash-我将它归档为肯定不起作用的东西。我猜我只有一半:)
hobbs

12

Shell正在从标准输入中读取脚本。在脚本内,您运行一个也想读取标准输入的命令。哪个输入要去哪里?你不能可靠地告诉你

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

如果脚本中的命令尝试从外壳程序读取脚本的文件描述符中读取该文件,则该命令将查找在读取的最后一块之后的内容。此位置是不可预测的:它取决于外壳程序选择的块大小,并且不仅取决于外壳程序及其版本,还取决于计算机配置,可用内存等。

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

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

#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.