我编写了一个在运行时切换用户的脚本,并使用文件重定向到标准输入来执行它。所以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
,但结果类似:
$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami
$ bash < script.sh
--whoami
$ dash < script.sh
itvirta
使用 时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|...
这意味着文件句柄位于文件末尾,并且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
)原则上,对每个字节进行完整的系统调用并不是很有效。在实践中,我认为与 shell 所做的大多数事情都涉及生成全新进程的事实相比,读取不会是很大的开销。
类似的情况是这样的:
echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'
子 shell 必须确保read
只读取第一个换行符,以便head
看到下一行。 (这也适用dash
。)
换句话说,Bash 付出了额外的努力来支持读取脚本本身以及从中执行的命令的相同源。dash
没有。 Debian 中打包的zsh
、 和ksh93
与 Bash 配合使用。
答案2
shell 正在从标准输入读取脚本。在脚本内部,您运行一个也想读取标准输入的命令。哪个输入将去往何处?你无法可靠地判断。
shell 的工作方式是读取一段源代码,对其进行解析,如果找到完整的命令,则运行该命令,然后继续处理该块的其余部分和文件的其余部分。如果该块不包含完整的命令(末尾有一个终止字符 - 我认为所有 shell 都会读取到一行的末尾),则 shell 会读取另一个块,依此类推。
如果脚本中的命令尝试从 shell 从中读取脚本的同一文件描述符中读取,则该命令将找到它读取的最后一个块之后的任何内容。这个位置是不可预测的:它取决于 shell 选择的块大小,并且不仅取决于 shell 及其版本,还取决于机器配置、可用内存等。
Bash 在执行命令之前会在脚本中查找命令源代码的末尾。这不是您可以指望的,不仅因为其他 shell 不这样做,而且还因为只有当 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
是过去的一种技巧。