Why sudo cat gives a Permission denied but sudo vim works fine? [duplicate]
I am trying to automate the addition of a repository source in my arch's pacman.conf file but using the echo
command in my shell script. However, it fails like this:-
sudo echo "[archlinuxfr]" >> /etc/pacman.conf
sudo echo "Server = http://repo.archlinux.fr/\$arch" >> /etc/pacman.conf
sudo echo " " >> /etc/pacman.conf
-bash: /etc/pacman.conf: Permission denied
If I make changes to /etc/pacman.conf manually using vim, by doing
sudo vim /etc/pacman.conf
and quiting vim with :wq
, everything works fine and my pacman.conf has been manually updated without "Permission denied" complaints.
Why is this so? And how do I get sudo echo
to work? (btw, I tried using sudo cat
too but that failed with Permission denied as well)
As @geekosaur explained, the shell does the redirection before running the command. When you type this:
sudo foo >/some/file
Your current shell process makes a copy of itself that first tries to open /some/file
for writing, then if that succeeds it makes that file descriptor its standard output, and only if that succeeds does it execute sudo
. This is failing at the first step.
If you're allowed (sudoer configs often preclude running shells), you can do something like this:
sudo bash -c 'foo >/some/file'
But I find a good solution in general is to use | sudo tee
instead of >
and | sudo tee -a
instead of >>
. That's especially useful if the redirection is the only reason I need sudo
in the first place; after all, needlessly running processes as root is precisely what sudo
was created to avoid. And running echo
as root is just silly.
echo '[archlinuxfr]' | sudo tee -a /etc/pacman.conf >/dev/null
echo 'Server = http://repo.archlinux.fr/$arch' | sudo tee -a /etc/pacman.conf >/dev/null
echo ' ' | sudo tee -a /etc/pacman.conf >/dev/null
I added > /dev/null
on the end because tee
sends its output to both the named file and its own standard output, and I don't need to see it on my terminal. (The tee
command acts like a "T" connector in a physical pipeline, which is where it gets its name.) And I switched to single quotes ('
...'
) instead of doubles ("
..."
) so that everything is literal and I didn't have to put a backslash in front of the $
in $arch
. (Without the quotes or backslash, $arch
would get replaced by the value of the shell parameter arch
, which probably doesn't exist, in which case the $arch
is replaced by nothing and just vanishes.)
So that takes care of writing to files as root using sudo
. Now for a lengthy digression on ways to output newline-containing text in a shell script. :)
To BLUF it, as they say, my preferred solution would be to just feed a here-document into the above sudo tee
command; then there is no need for cat
or echo
or printf
or any other commands at all. The single quotation marks have moved to the sentinel introduction <<'EOF'
, but they have the same effect there: the body is treated as literal text, so $arch
is left alone:
sudo tee -a /etc/pacman.conf >/dev/null <<'EOF'
[archlinuxfr]
Server = http://repo.archlinux.fr/$arch
EOF
But while that's how I'd do it, there are alternatives. Here are a few:
You can stick with one echo
per line, but group all of them together in a subshell, so you only have to append to the file once:
(echo '[archlinuxfr]'
echo 'Server = http://repo.archlinux.fr/$arch'
echo ' ') | sudo tee -a /etc/pacman.conf >/dev/null
If you add -e
to the echo
(and you're using a shell that supports that non-POSIX extension), you can embed newlines directly into the string using \n
:
# NON-POSIX - NOT RECOMMENDED
echo -e '[archlinuxfr]\nServer = http://repo.archlinux.fr/$arch\n ' |
sudo tee -a /etc/pacman.conf >/dev/null
But as it says above, that's not POSIX-specified behavior; your shell might just echo a literal -e
followed by a string with a bunch of literal \n
s instead. The POSIX way of doing that is to use printf
instead of echo
; it automatically treats its argument like echo -e
does, but doesn't automatically append a newline at the end, so you have to stick an extra \n
there, too:
printf '[archlinuxfr]\nServer = http://repo.archlinux.fr/$arch\n \n' |
sudo tee -a /etc/pacman.conf >/dev/null
With either of those solutions, what the command gets as an argument string contains the two-character sequence \n
, and it's up to the command program itself (the code inside printf
or echo
) to translate that into a newline. In many modern shells, you have the option of using ANSI quotes $'
...'
, which will translate sequences like \n
into literal newlines before the command program ever sees the string. That means such strings work with any command whatsoever, including plain old -e
-less echo
:
echo $'[archlinuxfr]\nServer = http://repo.archlinux.fr/$arch\n ' |
sudo tee -a /etc/pacman.conf >/dev/null
But, while more portable than echo -e
, ANSI quotes are still a non-POSIX extension.
And again, while those are all options, I prefer the straight tee <<EOF
solution above.
The problem is that the redirection is being processed by your original shell, not by sudo
. Shells are not capable of reading minds and do not know that that particular >>
is meant for the sudo
and not for it.
You need to:
- quote the redirection ( so it is passed on to
sudo)
-
and use
sudo -s
(so thatsudo
uses a shell to process the quoted redirection.)
http://www.innovationsts.com/blog/?p=2758
As the instructions are not that clear above I am using the instructions from that blog post. With examples so it is easier to see what you need to do.
$ sudo cat /root/example.txt | gzip > /root/example.gz
-bash: /root/example.gz: Permission denied
Notice that it’s the second command (the gzip command) in the pipeline that causes the error. That’s where our technique of using bash with the -c option comes in.
$ sudo bash -c 'cat /root/example.txt | gzip > /root/example.gz'
$ sudo ls /root/example.gz
/root/example.gz
We can see form the ls command’s output that the compressed file creation succeeded.
The second method is similar to the first in that we’re passing a command string to bash, but we’re doing it in a pipeline via sudo.
$ sudo rm /root/example.gz
$ echo "cat /root/example.txt | gzip > /root/example.gz" | sudo bash
$ sudo ls /root/example.gz
/root/example.gz