The documentation for
-i (simulate initial login) option runs the shell specified by the password database entry of the target user as a login shell. This means that login-specific resource files such as .profile or .login will be read by the shell. If a command is specified, it is passed to the shell for execution via the shell's -c option.
That is, it genuinely runs the user's login shell, and then passes whatever command you gave
sudo to it using
-c - unlike what
sudo cmd arg arg usually does without the
-i option. Ordinarily,
sudo just uses one of the
exec* functions directly to start the process itself, with no intermediate shell and all arguments passed through exactly as-is.
-i, it sets up the environment, runs the user's shell as a login shell, and reconstructs the command you asked to run as an argument to
bash -c. In your case, it runs (say, approximately)
/bin/bash -c "bash -c ' ... '" (imagine quoting that works).
The problem lies in how
sudo turns the command you wrote into something that
-c can deal with, explained in the next section. The last section has some possible solutions, and in between is some debugging and verification technique,
Why does this happen?
When passing the command to
-c, it needs some preprocessing to make it do the right thing, which
sudo does in advance of running the shell. For example, if your command is:
sudo -iu yy echo 'three spaces'
then those spaces need to be escaped in order for the command to mean the same thing (i.e., for the single argument not to be split into two words). What ends up being run is:
/bin/bash -c 'echo three\ \ \ spaces'
Let's start with your simplified command:
sudo bash -c 'echo 1
In this case,
sudo changes user, then runs
execvp("bash", \["bash", "-c", "echo 1\necho 2"\]) (for an invented array literal syntax).
sudo -i bash -c 'echo 1
instead it changes user, then runs
execv("/bin/bash", ["-bash", "-c", "bash -c echo\\ 1\\\necho\\ 2"]), where
\\ equates to a literal
\n is a linebreak. It's escaped the spaces and the newline in your main command by preceding them with backslashes.
That is, there's an outer login shell, which has two arguments:
-c and your entire command, reconstructed into a form the shell is expected to understand correctly. Unfortunately, it doesn't. The inner
bash command ultimately tries to run:
where the first physical line ends with a line continuation (backslash followed by newline), which is deleted entirely. The logical line is then just
echo 1echo 2, which doesn't do what you wanted.
There's an argument that this is a flaw in
sudo's escaping, given the standard behaviour of backslash-newline pairs. I think it should be safe to leave them unescaped here.
The same happens for your command with a here-document. It runs as, roughly:
/bin/bash -c 'bash -c cat\ \<\<\ \"EOF\"\\012script-content\\012EOF\\012'
\012 represents an actual newline -
sudo has inserted a backslash before each of them, just like the spaces. Note the double-escaping on
ps's rendition of an actual backslash followed by newline, which I'm using here (see below). What eventually runs is:
bash -c 'cat << "EOF"\
with line continuations
\ + newline everywhere, which are just removed. That makes it one long line, with no actual newlines in it, and an invalid heredoc:
bash -c 'cat << "EOF"script-contentEOF'
So that's your problem: the inner
bash process gets only one line of command, and so the here-document never gets a chance to end (or start). I have some imperfect solutions at the bottom, but this is the underlying cause.
How can you check what's happening?
To get those commands out correctly quoted and validate what was happening I modified my login shell's profile file (
.zprofile, etc) to say just:
ps awx|grep $$
That shows me the command line of the running shell at the time and gives me an extra couple of lines of output before the warning.
hexdump -C /proc/$$/cmdline will also be helpful on Linux.
What can you do about it?
I don't see an obvious and reliable way of getting what you want out of this. You don't want
sudo to touch your command at all if possible. One option that will largely work for a simple case is to pipe the commands into the shell, rather than specifying them on the command line:
printf 'cat > ... << ... \n ...' | sudo -iu yy
That requires careful internal escaping still.
Probably better is just to put them into a temporary script file and run them that way:
printf 'command' > "$f"
chmod +r "$f"
sudo -iu yy "$f"
A made-up filename of your own will work too. Depending on your sudoers settings you might be able to keep a file descriptor open and have it read from that as a file (
/dev/fd/3) if you really don't want it on disk, but a real file is going to be easier.