I have a script I wrote for switching to root or running a command as root without a password. I edited my /etc/sudoers file so that my user [matt] has permission to run /bin/su with no password. This is my script "s" contents:
matt: ~ $ cat ~/bin/s
#!/bin/bash
[ "$1" != "" ] && c='-c'
sudo su $c "$*"
If there are no parameters [simply s
], it basically calls sudo su
which goes to root with no password. But if I put paramaters, the $c variable equals "-c", which makes su execute a single command.
It works good, except for when I need to use spaces. For example:
matt: ~ $ touch file\ with\ spaces
matt: ~ $ s chown matt file\ with\ spaces
chown: cannot access 'file': No such file or directory
chown: cannot access 'with': No such file or directory
chown: cannot access 'spaces': No such file or directory
matt: ~ $ s chown matt 'file with spaces'
chown: cannot access 'file': No such file or directory
chown: cannot access 'with': No such file or directory
chown: cannot access 'spaces': No such file or directory
matt: ~ $ s chown matt 'file\ with\ spaces'
matt: ~ $
How can I fix this?
Also, what's the difference between $* and $@ ?
Ah, fun with quoting. Usually, the approach @John suggests will work for this: use "$@"
, and it won't try to interpret (and get confused by) the spaces and other funny characters in your parameters. In this case, however, that won't work because su's -c option expects the entire command to be passed as a single parameter, and then it'll start a new shell which parses the command (including getting confused by spaces and such). In order to avoid this, you actually need to re-quote the parameters within the string you're going to pass to su -c
. bash's printf
builtin can do this:
#!/bin/bash
if [ $# -gt 0 ]; then
sudo su -c "$(printf "%q " "$@")"
else
sudo su
fi
Let me go over what's happening here:
s chown matt file\ with\ spaces
printf "%q " "$@"
command in the script, it replaces "$@"
with the arguments to the script, with parameter breaks intact. It's equivalent to printf "%q " "chown" "matt" "file with spaces"
.printf
interprets the format string "%q " to mean "print each remaining parameter in quoted form, with a space after it". It prints: "chown matt file\ with\ spaces ", essentially reconstructing the original command line (it has an extra space on the end, but this turns out not to be a problem).$()
construct, it'll be treated as a single parameter to sudo). This is equivalent to running sudo su -c "chown matt file\ with\ spaces "
.sudo
runs su
, and passes along the rest of the parameter list it got including the fully escaped command.su
runs a shell, and it also passes along the rest of its parameter list.-c
: chown matt file\ with\ spaces
. In the normal course of parsing it, it'll interpret the unescaped spaces as separators between parameters, and the escaped spaces as part of a parameter, and it'll ignore the extra space at the end.chown
, with the parameters "matt" and "file with spaces". This does what you expected.Isn't bash parsing a hoot?
"$*"
collects all the positional parameters ($1
, $2
, …) into a single word, separated by one space (more generally, the first character of $IFS
). Note that in shell terminology, a word can include any character including spaces: "foo bar"
or foo\ bar
parses to a single word. For example, if there are three arguments, then "$*"
is equivalent to "$1 $2 $3"
. If there is no argument, then "$*"
is equivalent to ""
(an empty word).
"$@"
is a special syntax that expands to the list of positional parameters, each in its own word. For example, if there are three arguments, then "$@"
is equivalent to "$1" "$2" "$3"
. If there is no argument, then "$@"
is equivalent to nothing (an empty list, not a list with one word that is empty).
"$@"
is almost always what you want to use, as opposed to "$*"
, or unquoted $*
or $@
(the last two are exactly equivalent and perform filename generation (a.k.a. globbing) and word splitting on all the positional parameters).
There's an additional problem, which is that su
except a single shell command as the argument of -c
, and you're passing it multiple words. You've had a detailed explanation of getting the quoting right, but let me add advice on how to do it right, which sidesteps the double quoting issues. You may also want to refer to https://unix.stackexchange.com/questions/3063/how-do-i-run-a-command-as-the-system-administrator-root for more background on sudo
and su
.
sudo
already runs a command as root, so there's no need to invoke su
. In case your script has no argument , you can just run a shell directly; unless your version of sudo
is very old, there's an option for that: sudo -s
. So your script can be:
#!/bin/sh
if [ $# -eq 0 ]; then set -- -s; else set -- -- "$@"; fi
exec sudo "$@"
(The else part is to handle the rare case of a command that begins with -
.)
I wouldn't bother with such a short script though. Running a command as root is unusual and risky enough that typing the three extra characters shouldn't be a problem. Running a shell as root is even more unusual and risky and surely deserves six more characters.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With