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

fro*_*age 7 bash sudo here-document

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

为什么第四个命令失败?目的是写入相同的文件,但依靠属于用户yy的 $HOME 变量来确保它确实最终位于她的主目录中。我不明白为什么登录 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')
Run Code Online (Sandbox Code Playgroud)

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

更新: here-doc 方面只是掩盖了问题。这是问题的简化:

$ sudo bash -c 'echo 1
> echo 2'
1
2
$ sudo -i bash -c 'echo 1
> echo 2'
1echo 2
Run Code Online (Sandbox Code Playgroud)

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

Mic*_*mer 10

状态的文档-i

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

也就是说,它真正运行在用户的登录shell,然后通过你给任何命令sudo使用它-c-不像什么sudo cmd arg arg通常不会没有-i选项。通常情况下,sudo只使用一个exec*功能,直接启动进程本身,没有中间壳和所有参数穿过正是原样。

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

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


为什么会发生这种情况?

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

sudo -iu yy echo 'three   spaces'
Run Code Online (Sandbox Code Playgroud)

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

/bin/bash -c 'echo three\ \ \ spaces'
Run Code Online (Sandbox Code Playgroud)

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

sudo bash -c 'echo 1
echo 2'
Run Code Online (Sandbox Code Playgroud)

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

-i

sudo -i bash -c 'echo 1
echo 2'
Run Code Online (Sandbox Code Playgroud)

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

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

echo 1\
echo 2
Run Code Online (Sandbox Code Playgroud)

其中第一个物理行以换行符(反斜杠后跟换行符)结束,该行被完全删除。逻辑线是 just echo 1echo 2,它不会做你想要的。

sudo考虑到反斜杠-换行符对的标准行为,有人认为这是' 转义中的一个缺陷。我认为让他们躲在这里应该是安全的。


使用 here-document 的命令也会发生同样的情况。它大致运行为:

/bin/bash -c 'bash -c cat\ \<\<\ \"EOF\"\\012script-content\\012EOF\\012'
Run Code Online (Sandbox Code Playgroud)

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

bash -c 'cat << "EOF"\
script-content\
EOF\
'
Run Code Online (Sandbox Code Playgroud)

线延续\+换行符无处不在,它们只是删除。这使它成为一个长行,其中没有实际的换行符,并且是一个无效的 heredoc:

bash -c 'cat << "EOF"script-contentEOF'
Run Code Online (Sandbox Code Playgroud)

所以这就是你的问题:内部bash进程只有一行命令,所以这里的文档永远没有机会结束(或开始)。我在底部有一些不完美的解决方案,但这是根本原因。


你怎么能检查发生了什么?

要获得这些命令进行正确地引用和验证发生了什么事我修改了登录shell的配置文件(.profile.bash_profile.zprofile等),只说:

ps awx|grep $$
Run Code Online (Sandbox Code Playgroud)

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

hexdump -C /proc/$$/cmdline 在 Linux 上也会有帮助。


你能为这个做什么?

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

printf 'cat > ... << ... \n ...' | sudo -iu yy
Run Code Online (Sandbox Code Playgroud)

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

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

f=`mktemp`
printf 'command' > "$f"
chmod +r "$f"
sudo -iu yy "$f"
rm "$f"
Run Code Online (Sandbox Code Playgroud)

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