10

If I type sudo at the beginning of a one liner in bash, does it apply to the rest of the commands?

In other words, is this:

sudo foo | foo2 | foo3

equivalent to this:

sudo foo | sudo foo2 | sudo foo3
slm
  • 369,824
Mike B
  • 8,900

4 Answers4

13

As a way to convince yourself that sudo only runs using the first command provided it, and everything else after the first pipe is run as your original user ID you can use this useless chain of commands to see it.

Example #1

$ sudo whoami > file1 | whoami > file2 | whoami > file3

Then when you cat those files you'll see the following usernames:

$ cat file{1..3}
root
saml
saml

Example #2

However if you run a subshell:

$ sudo sh -c 'whoami > file4 | whoami > file5 | whoami > file6'

Then when you cat these files you'll see the following usernames:

$ cat file{4..6}
root
root
root

Example #3

Your comment about sudo foo1 | sudo foo2 ... would never work, since the output from sudo foo1 would be fed to sudo foo2. Using my whoami examples this shows that that chain of commands does nothing.

$ sudo whoami | sudo whoami | sudo whoami
root

The 1st one ran, but the 2nd and 3rd don't do anything since they aren't equipped to take input. Rather I think you meant to write something like this:

$ sudo whoami;sudo whoami;sudo whoami
root
root
root

Which is equivalent to running it 3 times on 3 different command prompts. Or this:

$ sudo whoami && sudo whoami && sudo whoami
root
root
root

But don't ever do this last one. It belongs in the next section.

Obscure ways to call sudo

These are not my best work but are other ways you might have seen me executing multiple commands using sudo. I show them here only to teach not so that others will necessarily do them!

Way #1

$ echo "echo 1 > /proc/sys/vm/drop_caches" | sudo sh

How it works?

The echo program inside the double quotes is running as root, because of sudo, but the shell that's redirecting echo's output to the root-only file is still running as you. Your current shell does the redirection before sudo starts.

Way #2

$ sudo tee /proc/sys/vm/drop_caches <<<1

How it works?

This method runs the tee program as root AND takes input from a here string which runs prior to sudo invoking the tee command.

Way #3

# this way
$ sudo -s -- 'whoami'

# or this way
sudo -s -- sh -c 'whoami;whoami'

How it works?

These might look different but they're really doing the same thing. When using the -s switch, sudo will run a single command. I could never figure out if there was a way to escape it. Nothing like these would work.

# this
$ sudo -s -- 'whoami;whoami'

# or this
$ sudo -s -- 'whoami\;whoami'

But in looking at the man page, the -s switch says that it will pass a single command to the shell defined in the user's entry of the /etc/passwd file. So we use a trick in the second form, mainly passing the shell, another shell (sh -c) in which we "backdoor" our string of commands to run.

There are more but I will stop here. These are only to show you what you can do if you understand things, but should not necessarily just chain junk together because you can, you should try and keep your code pragmas to as logical level that makes sense so that others can both understand and support them in the future.

slm
  • 369,824
6

No, in your example only foo is executed by sudo. If you want to run all the commands with escalated privileges, you can spawn a shell:

sudo sh -c 'foo | foo2 | foo3'
jordanm
  • 42,678
2

No. But why?

No, sudo does not apply to the rest of the pipeline. This other answer gives some examples, but it doesn't explain the mechanics behind all this.

The most important thing is sudo is a separate executable that has nothing to do with any shell you may run it from. The command in question is:

sudo foo | foo2 | foo3

and for the shell it's no different than cat foo | foo2 | foo3. The shell recognizes a pipeline of three parts and sets up the piping before sudo (or cat) is even started. The shell is not aware that foo will ultimately be a command. For the shell foo is just an argument it should pass to sudo.

Neither of the following happens in the above case, but even if sudo got foo, |, foo2, | and foo3 as arguments, or if it got foo | foo2 | foo3 as one argument, then the whole command would not work as you expect anyway, because | belongs to the shell syntax and sudo neither does the job of a shell nor runs a shell by default.


Running multiple commands with sudo; two approaches

You need a shell to interpret |. This leads to two basic approaches:

  1. sudo foo | sudo foo2 | sudo foo3 where the main shell handles the piping;
  2. sudo sh -c 'foo | foo2 | foo3' where a new elevated shell handles the piping.

It's similar with && and/or || instead of or along with |.

Note in the case number 2 the main shell starts sudo, while foo | foo2 | foo3 is just one of the arguments, a string that could be anything; then later sudo starts sh and passes foo | foo2 | foo3 also as an argument to it. Only inside sh the string is interpreted as shell code.

I wrote sh -c, but you can use another shell. This especially makes sense if you need features not supported by plain sh.


Choose wisely

In general I prefer the approach with an additional shell (the approach number 2). These are my reasons:

  • There is just one sudo, it will ask for credentials at most once. In case of sudo … | sudo …, depending on the configuration (in sudoers) each sudo may ask separately. The tool is smart enough, so multiple instances using the same terminal won't disturb one another and will ask in sequence, even though the shell runs them in parallel. It's worse in case of sudo … && sudo … because here in general the second sudo may (conditionally) start hours later, so even if sudo is configured not to ask for your password too frequently, the second sudo may ask and you may be unable to predict when this happens.

  • If there's a redirection involved then you won't need the trick with tee (nor sudo cat … | … in place of input redirection). Letting the elevated shell handle the redirection(s) solves the problem in the most natural way. This assumes you do want to open some file(s) as root.

On the other hand there are disadvantages of the approach number 2. In some circumstances you may want to choose number 1. Things to consider:

  • sudo some_shell -c … runs a new elevated some_shell. There may be bugs in some_shell or there may be bugs in the shell code you pass to it; a bug may cause the shell do something unexpected. An elevated process doing something unexpected is a very bad thing. For this reason you may want to elevate foo, foo2 and foo3, but nothing more.

  • Shell code passed to sh -c must be a single argument. In almost any case (and certainly in your case) this requires quoting or escaping spaces and other characters. The additional level of quoting/escaping adds complexity. In general you need to know how to properly quote/escape already quoted or escaped things.

    Potentially useful resource: How can I single-quote or escape the whole command line in Bash conveniently?

  • The shell started by sudo cannot access the variables of your main shell. Exporting the variables may or may not help; sudo may be configured to remove them from the environment. Embedding a variable expanded by the main shell in code for another shell is as wrong as embedding {}, unless you know the value is safe for this. I mean in general this is wrong:

    sudo sh -c "foo \"$var\" | foo2"

    There are several ways to pass a variable safely. I will only list them (and there may be others):

    var="$var" sudo -E sh -c 'foo "$var" | foo2'   # may or may not work depending on sudoers and security policy
    sudo var="$var" sh -c 'foo "$var" | foo2'      # may or may not work depending on sudoers and security policy
    sudo env var="$var" sh -c 'foo "$var" | foo2'
    sudo sh -c "foo ${var@Q} | foo2"               # if the main shell is Bash
    sudo sh -c 'foo "$1" | foo2' sh "$var"
    

    The approach number 1 will be:

    sudo foo "$var" | sudo foo2
    

    which is straightforward and safe.

  • Suppose you're interested in the exit status of foo and/or foo2 etc. sudo sh -c 'foo | foo2 | foo3' won't give it to you. Even if you use a shell that stores this information after running a pipeline (like Bash with its $PIPESTATUS[@]), reliably passing the information from the inner shell run by sudo to the outer main shell is a non-trivial task and it requires some cumbersome code (for the inner shell and separately for the outer shell).

    Again, the approach number 1 is straightforward:

    sudo foo | sudo foo2 | sudo foo3
    

    Each sudo (if only it does its job successfully) will relay the exit status of the respective foo* command. Now all you need is an ability of your main shell to tell you the exit status of each command in the pipeline ($PIPESTATUS[@] if the main shell is Bash).

  • If sudoers allows you to run a limited number of commands with sudo then it most likely disallows sh (and any other unrestricted shell or shell-like utility, because otherwise you would be able to run anything with sudo anyway). If foo, foo2 and foo3 are allowed with sudo but shells are not, the approach number 1 is your only choice.


Broader picture

There are other commands that behave like sudo: nohup, nice, ionice, unshare, …

Even echo is like this. You don't expect echo in

echo foo | foo2 | foo3

to take | and the rest as its arguments. From the shell it gets foo and that's it, hardly anyone is surprised.

In any(?) shell echo is a builtin, i.e. a part of the shell. "Builtin" means the shell does not run an external echo executable, it handles the job of echo by itself; but the builtin behaves like an external echo would (there are some subtleties involving signals though).

There is at least one command that behaves differently in some circumstances, it's time. It is a separate executable and when called as such it behaves like sudo in the context of your question. E.g. this:

command time -p sleep 2 | sleep 4

in an idle OS should report 2 seconds of real time (time prints to stderr, so you will see a report despite the pipe). However in Bash time is a keyword that alters the way Bash parses what follows. It's deliberately like this, so you can measure an entire pipeline (or a shell code snippet including e.g. a shell loop or so), without any bias from spawning an additional shell. Try this:

# in Bash
time -p sleep 2 | sleep 4

and you will see 4 seconds in the report.

Compare this question and my answer there.

1

Yes, but the command ends at a ;, |, && or ||.

No, it does not apply to all of a complex command (e.g. a|b ), or script.