What can explain the examples below and how do I fix this, preferably without heavy quoting acrobatics? I am using the $n to simulate multiple line command strings, just in case it distracts you from the real question.

~$ n=$'\n'; sudo -i echo "line1${n}line2${n}"


~$ n=$'\n'; sudo echo "line1${n}line2${n}"

  • 1
    What is root’s shell? – Jeff Schaller Jun 3 '18 at 10:38
  • They are both bash. Also, diff -s <(sudo bash -c 'shopt') <(sudo -i bash -c 'shopt') returns Files /dev/fd/63 and /dev/fd/62 are identical. But, more importantly, are you actually implying you tested it on some box of yours and you couldn't replicate the (...) output I pasted above? – argle Jun 3 '18 at 10:45
  • also interesting: FOO=X$'\t'Y$'\n'Z; sudo -i echo "$FOO" linebreak gets removed, but not the tab – tinita Jun 3 '18 at 11:04
  • Fair enough; I would just suggest a replacement of "endline" with "newline", as I think that's the more common term. – Jeff Schaller Jun 3 '18 at 12:04
  • @JeffSchaller Disclaimer: this is my first rollback ever. This has nothing to do with echo. Actually I created this fake "echo-based" example to keep things simple. It's sudo -i that does this [and it does it] to any command. Test it with bash -c. – argle Jun 3 '18 at 12:04

Running that sudo -i echo $'line1\nline2' under strace shows Bash gets started like this:

9183  execve("/bin/bash", ["-bash", "--login", "-c", "echo line1\\\nline2\\\n"], ...

Now, strace presents special characters with backslash-escapes when it displays the strings, so what Bash actually gets as the argument to -c is echo line1[backslash][newline]line2[backslash][newline] and for the shell, a backslash at the end of a line marks a continuation line and removes the backslash and the following newline.

Without -i, sudo runs echo directly, without going through the shell:

9189  execve("/bin/echo", ["echo", "line1\nline2\n"], ... 

Here, that's a literal newline going to echo, and echo duly prints that.

The idea here must be that sudo tries to add a layer of shell escaping to accommodate for the fact that sh -c takes a single string, while sudo itself takes the command as distinct arguments.

Compare the following cases:

sudo escapes the space (this is just the name of the command, no arguments!):

$ sudo -i 'echo foo'
-bash: echo foo: command not found

sudo escapes the backslash, so that this actually works (Bash's echo doesn't process the backslash):

$ sudo -i echo 'foo\bar'

Same with a tab:

$ sudo -i echo $'foo\tbar'
foo     bar

Here, there's no extra quoting on the backslash, so Bash removes it while processing the shell command line (b isn't a special character to the shell, and doesn't need quoting. This is basically the same as bash -c 'echo foo"b"ar'):

$ bash -c 'echo foo\bar'

The problem is just that you can't escape a newline with a backslash, and sudo doesn't seem to take that into account.

In any case, quoting issues like this probably turn quite a bit easier if you store the commands you want in a file, and run that as a script.

| improve this answer | |
  • So do you think there is no workaround other than using (conventional) files? I would like to at least use something like sudo -i <(echo "$command"). Or some substitution cheats. Any idea? – argle Jun 3 '18 at 13:19
  • I can't really say anything about other workarounds, I just said that using a file would probably not have that many quoting issues. Process substitution would be a good idea since it makes stuff look like files. Alas, it doesn't work at least on my system, I suppose sudo closes the file handle used by it. Of course you could use a temporary named pipe but that seems a bit complicated to do manually. – ilkkachu Jun 3 '18 at 13:23

You could change the structure of the command:

echo "echo \"line1${n}line2${n}\"" | sudo -i bash -s

This way sudo does not see the argument and thus cannot mess it up.

| improve this answer | |

Not the answer you're looking for? Browse other questions tagged or ask your own question.