I wrote an "inline script" which uses positional parameters, e.g.

$ sudo sh -c 'echo "p0=$0" && echo "p1=$1" && echo "p2=$2" && echo "all=$@"' sh 1 2
all=1 2

I actually want to run my inline script using sudo --login. But then the positional parameters don't work. Is there a way to make them work?

(I'm also interested if the behaviour is documented or standardized somewhere).

$ sudo --login sh -c 'echo "p0=$0" && echo "p1=$1" && echo "p2=$2" && echo "all=$@"' sh 1 2
all=1 2

Software versions:

$ sh --version
GNU bash, version 5.0.7(1)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

$ sudo --version
Sudo version 1.8.27
Sudoers policy plugin version 1.8.27
Sudoers file grammar version 46
Sudoers I/O plugin version 1.8.27
  • 1
    sudo -i is essentially running it with the user's shell. It's as if the whole thing was wrapped in bash -c. Wrap your own command in bash -c and maybe some mess of quoting might work. – muru Aug 16 '19 at 18:58
  • @muru I see, yes. Thanks. I was rather hoping to avoid quoting messes :-). – sourcejedi Aug 16 '19 at 19:01
  • @muru I used strace -f -e trace=execve, and found you were right that sudo is wrapping everything into a single string, to call bash --login -c '...'. Although, that doesn't tell us exactly why the values can be seen in "$@", but not $1 or $2. (And then I checked further; they are also not in $3 nor $4 nor $5 nor $6). – sourcejedi Aug 16 '19 at 19:16
  • if you want to avoid a mess of quoting (and escaping), just write a shell script and run it with sudo. It'll be a lot easier to write and to debug than trying to do it on the command-line, even if it's just a one-use throwaway script. – cas Aug 17 '19 at 2:30

For the same reason

sudo --login echo '$HOME'

Doesn't output $HOME, but the content of the $HOME variable (of the target user, as set by sudo).

In your case, it's not because sh -c doesn't expand the positional parameters, but because the $0, $1, $2 were already expanded (by the login shell) by the time sh was started.

What is more surprising here is why you see all=1 2. And it's the same reason why you see $@ upon sudo --login echo '$@'.

Both -s/--shell and -i/--login run a shell to run the command. But sudo does it in a very strange way.

I'd expect sudo -s 'some shell code' to run a shell to interpret some shell code, but that's not what it does. With -s, it still kind-of expects to run a single simple command, and tries to quote (with \) some characters in the hope of making it happen through that shell.

You're not meant to do sudo -s 'echo test', but sudo -s echo test. In the first case, sudo actually passes echo\ test as shell code. In Bourne-like shells, that tries to run a command called 'echo test'. With a rc shell, that would run a command called echo\ with test as argument.

Beside SPC, there are a few characters that sudo escapes. That includes backtick, ;, |, (, ), *, &, =, \ but strangely enough, not $ (or maybe that's the only raison d'être of those -s/--login options: to have a shell expand variables).

It does include @ though. If you look at the code, it escapes all bytes except the ASCII alnums, _, - and $.

 if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
   *dst++ = '\\';


sudo -s echo '$@'

sudo actually runs "$SHELL" -c 'echo $\@' and that explains why in your example, $@ is not expanded by the shell started by sudo --login but by the one you tell it to run.

In your

sudo sh -c 'echo "p0=$0" && echo "p1=$1" && echo "p2=$2" && echo "all=$@"' sh 1 2

sudo runs root's login shell (bash in your case) and tells it to interpret:

sh -c echo\ \"p0\=$0\"\ \&\&\ echo\ \"p1\=$1\"\ \&\&\ echo\ \"p2\=$2\"\ \&\&\ echo\ \"all\=$\@\" sh 1 2

The arguments have been joined with spaces and all the SPC, ", =, &, @ characters, but not $ have been escaped. Because those $ have not been escaped that bash login shell will expand $0,$1, $2 but not $\@ because of that backslash.

You can see it with:

$ SHELL=echo sudo -s sh -c 'echo "p0=$0" && echo "p1=$1" && echo "p2=$2" && echo "all=$@"' sh 1 2
-c sh -c echo\ \"p0\=$0\"\ \&\&\ echo\ \"p1\=$1\"\ \&\&\ echo\ \"p2\=$2\"\ \&\&\ echo\ \"all\=$\@\" sh 1 2

So that login bash shell ends up running sh with -c as first argument,

echo "p0=-bash" && echo "p1=" && echo "p2=" && echo "all=$@"

as second argument and sh, 1, 2 as 3rd, 4th and 5th.

To work around it, you can use ${0} instead of $0, because sudo transforms it to $\{0\} which prevents the login shell from expanding it to the content of its $0:

$ sudo --login sh -c 'echo "p0=${0}" && echo "p1=${1}" && echo "p2=${2}" && echo "all=${@}"' sh 1 2
all=1 2

Edit: history brings light to the reason behind it

more findings while looking at the code. The non-escaping of $ was apparently introduced in 2013 by this change. Which refers to bug#564 which refers to bug#413.

It looks like before that bug#413 was "fixed", sudo behaved as I expected.

That is:

sudo -i 'some shell code'

Had the login shell interpret that shell code (and sudo -s 'some shell code' had $SHELL interpret that shell code). But the bug#413 resolution broke it, because the person who reported the bug didn't understand it worked that way. And the bug#564 broke it further (in an attempt to revert only part of the breakage introduced by the bug#413 resolution).

I went as far as compiling sudo 1.7.1 from 2009 and the -s/-i options were working as I'd expect then. 1.7.3 (with bug#413 resolution) worked kind of like you apparently expected:

$ sudo-1.7.1 -i 'echo "$SHELL"'
$ sudo-1.7.3 -i 'echo "$SHELL"'
-bash: echo "$SHELL": command not found
$ sudo-1.8.21p2 -i 'echo "$SHELL"'
-bash: echo "/bin/bash": No such file or directory
$ sudo-1.7.3 -i sh -c 'echo "p0=$0" && echo "p1=$1" && echo "p2=$2" && echo "all=$@"' sh 1 2
all=1 2

That escaping introduced in the fix for that bug#413 is easily fooled because banging a \ in front of all bytes (not character) doesn't work for all.

Beside the obvious case of rc where \ is not even a quoting operator, in most shells, newline can't be quoted with \:

$ SHELL=sh sudo -s echo $'a\nb'
$ SHELL=csh  sudo -s echo $'a\nb'
a b

It also means empty arguments are discarded:

$ sudo printf '<%s>\n' a '' b
$ sudo -s printf '<%s>\n' a '' b

Also, by inserting a \ before every byte, it's transforming characters into others in charsets where byte 0x5c (the encoding of \) is found in other characters:

$ SHELL=bash LC_ALL=zh_HK.big5hkscs sudo -s echo $'\xa3``uname`\xa3`'
bash: line 2: Linuxα: command not found

0xa3 0x60 is Greek Epsilon in that locale. And sudo changes the 0x60s to 0x5c 0x60 and 0xa3 0x5c is Greek Alpha, which explains why the uname command was run. sudo changed the




And of course the behaviour ends up being surprising in that the $-, $$, positional parameters and $varname (where varname is a valid POSIX variable name) are expanded but not the other parameters (like $@ here, but also $!, $?, $*, $#) or ${varname} or $var[1] (csh/tcsh/zsh) or $var(1) (rc/es/...).

|improve this answer|||||
  • Ow, that's horrible. What do they propose to "fix" with quoting the arguments? – Kusalananda Aug 16 '19 at 20:29
  • Answer accepted, in the spirit of "sudo does it in a very strange way". I do not guarantee that sudo on my system does everything written (or implied?) in this answer, as per my comment "I was rather hoping to avoid quoting messes :-)". – sourcejedi Aug 16 '19 at 20:30
  • 1
    @Kusalananda, I never understood why sudo handled -s/--login that way. To me, it makes it pointless. – Stéphane Chazelas Aug 16 '19 at 20:31
  • @Kusalananda, see edit with the historical context that explains the current mess. – Stéphane Chazelas Aug 16 '19 at 21:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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