为什么“sudo -i”登录 shell 会破坏 here-doc 命令字符串参数?

为什么“sudo -i”登录 shell 会破坏 here-doc 命令字符串参数?

在下面的五个命令序列中,所有命令都依赖于单引号将可能的变量替换传递给被调用bashshell,而不是调用 shell。呼叫用户是xx,但被调用的 shell 将以用户身份运行yy。第一个命令将 $HOME 替换为调用 shell 的值,因为被调用 shell 不是登录 shell。第二个命令替换了登录 shell 加载的 $HOME 的值,因此它是属于用户的值yy。第三个命令不依赖于 $HOME 值,并在猜测的用户主目录中创建一个文件yy

为什么第四个命令失败?目的是它写入相同的文件,但依赖属于用户的 $HOME 变量yy以确保它确实最终出现在她的主目录中。我不明白为什么登录 shell 会破坏作为静态单引号字符串传入的 here-doc 命令的行为。第五条命令的失败验证了这个问题与变量替换无关。

xx@host ~ $ sudo -u yy bash -c 'echo HOME=$HOME'
HOME=/home/xx
xx@host ~ $ sudo -iu yy bash -c 'echo HOME=$HOME'
HOME=/home/yy
xx@host ~ $ sudo -u yy bash -c 'cat > /home/yy/test.sh << "EOF"
> script-content
> EOF
> '
xx@host ~ $ sudo -iu yy bash -c 'cat > $HOME/test.sh << "EOF"
> script-content
> EOF
> '
bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOFscript-contentEOF')
xx@host ~ $ sudo -iu yy bash -c 'cat > /home/yy/test.sh << "EOF"
> script-content
> EOF
> '
bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOFscript-contentEOF')

这些命令是在基于 Ubuntu 16.04 (Xenial Xerus) 的 Linux Mint 18.3 Cinnamon 64 位系统上发出的。

更新:这里的文档方面只是让问题变得模糊。这是问题的简化:

$ sudo bash -c 'echo 1
> echo 2'
1
2
$ sudo -i bash -c 'echo 1
> echo 2'
1echo 2

为什么这两个命令中的第一个保留换行符而第二个不保留?sudo对于这两个命令来说都是通用的,但似乎转义/过滤/插值的方式不同,只取决于“-i”选项。

答案1

的文档-i状态:

(模拟初始登录)选项-i将目标用户的密码数据库条目指定的 shell 作为登录 shell 运行。这意味着 shell 将读取特定于登录名的资源文件,例如 .profile 或 .login。如果指定了命令,则会通过 shell 的 -c 选项将其传递到 shell 执行。

也就是说,它真正运行用户的登录 shell,然后sudo使用 - 传递您给它的任何命令-c-不像sudo cmd arg arg没有该选项通常会做什么-i。通常,sudo只使用其中之一功能exec*直接启动进程本身,没有中间 shell,所有参数都按原样传递。

使用-i,它会设置环境,将用户的 shell 作为登录 shell 运行,并重建您要求作为 的参数运行的命令bash -c。在你的情况下,它运行(比如说,大约)/bin/bash -c "bash -c ' ... '"(想象一下引用有效)。

问题在于如何sudo把你写的命令变成-c可以处理的东西,在下一节中解释。最后一部分有一些可能的解决方案,中间是一些调试和验证技术,


为什么会发生这种情况?

当将命令传递给 时-c,它需要一些预处理以使其执行正确的操作,这sudo在运行 shell 之前进行。例如,如果您的命令是:

sudo -iu yy echo 'three   spaces'

那么需要对这些空格进行转义,以便命令含义相同(即,单个参数不被拆分为两个单词)。最终运行的是:

/bin/bash -c 'echo three\ \ \ spaces'

让我们从简化的命令开始:

sudo bash -c 'echo 1
echo 2'

在这种情况下,sudo更改用户,然后运行execvp("bash", \["bash", "-c", "echo 1\necho 2"\])(对于发明的数组文字语法)。

-i

sudo -i bash -c 'echo 1
echo 2'

相反,它会更改用户,然后运行execv("/bin/bash", ["-bash", "-c", "bash -c echo\\ 1\\\necho\\ 2"])​​,其中\\相当于文字\并且\n是换行符。它通过在主命令中添加反斜杠来转义空格和换行符。

也就是说,有一个外部登录 shell,它有两个参数:-c以及整个命令,被重构为 shell 期望正确理解的形式。不幸的是,事实并非如此。内部bash命令最终尝试运行:

echo 1\
echo 2

其中第一个物理行以续行符结束(反斜杠后跟换行符),该行将被完全删除。逻辑行只是echo 1echo 2,它不符合您的要求。

有一种观点认为,这是sudos 转义的一个缺陷,因为反斜杠换行符的标准行为对。我想把他们留在这里应该是安全的。


对于使用此处文档的命令也会发生同样的情况。它的运行方式大致如下:

/bin/bash -c 'bash -c cat\ \<\<\ \"EOF\"\\012script-content\\012EOF\\012'

其中\012代表一个实际的换行符 -sudo在每个换行符之前插入一个反斜杠,就像空格一样。请注意 上的双重转义\\012:这是ps实际反斜杠后跟换行符的再现,我在这里使用它(见下文)。最终运行的是:

bash -c 'cat << "EOF"\
script-content\
EOF\
'

续行\+换行到处都是,刚刚被删除。这使得它成为一长行,其中没有实际的换行符,并且有一个无效的heredoc:

bash -c 'cat << "EOF"script-contentEOF'

这就是您的问题:内部bash进程仅获得一行命令,因此此处文档永远没有机会结束(或开始)。我在底层有一些不完善的解决方案,但这就是根本原因。


你如何检查发生了什么?

为了正确引用这些命令并验证发生了什么,我修改了登录 shell 的配置文件(.profile.bash_profile.zprofile等),仅说明:

ps awx|grep $$

这向我显示了当时正在运行的 shell 的命令行,并在警告之前给了我几行额外的输出。

hexdump -C /proc/$$/cmdline对 Linux 也很有帮助。


你能为这个做什么?

我看不出有什么明显且可靠的方法可以从中得到你想要的东西。sudo如果可能的话,你根本不想碰你的命令。在很大程度上适用于简单情况的一个选项是将命令通过管道传输到 shell,而不是在命令行上指定它们:

printf 'cat > ... << ... \n ...' | sudo -iu yy

这仍然需要小心的内部转义。

可能更好的方法是将它们放入临时脚本文件中并以这种方式运行它们:

f=`mktemp`
printf 'command' > "$f"
chmod +r "$f"
sudo -iu yy "$f"
rm "$f"

您自己编造的文件名也可以。根据您的 sudoers 设置/dev/fd/3,如果您确实不想将其保存在磁盘上,您也许可以保持文件描述符打开并将其作为文件 ( ) 读取,但真正的文件会更容易。

答案2

揭示这一点的更普遍的需求是记录可以直接复制并粘贴到终端会话的命令序列。其中一些命令以人类可读的方式创建小文件,并且仍然记录每个步骤。创建临时文件或指导读者“使用编辑器”偏离了本质上 100% 播放可能性的目标。 –4 分钟前

如果那是所有你想做的,只需使用:

sudo -u yy tee ~yy/test.sh >/dev/null <<'EOF'
script-content
EOF

简单得多,您不需要解决将整个事物粘贴到bash -c '...'构造中的引用问题。特别是如果您的脚本中有任何引号,这将使事情变得更加容易。

(请注意,此处引用的定界符定义如下所示,'EOF'以便您在脚本内容中使用的任何变量都不会在创建test.sh,这可能不是您的意图。)

另请注意使用波浪号扩展来获取用户yy的主目录:~yy。请参阅LESS='+/^ *Tilde Expansion' man bash参考资料 了解更多相关内容。这就是为什么你不需要sudo -i.

相关内容