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
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
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.
$ 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
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
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.
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!
$ 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.
$ 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.
# 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.
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'
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.
sudo
; two approachesYou need a shell to interpret |
. This leads to two basic approaches:
sudo foo | sudo foo2 | sudo foo3
where the main shell handles the piping;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
.
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.
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.
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.
;
,|
,&&
or||
– ctrl-alt-delor Dec 07 '22 at 13:27