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" != "" ] &开发者_如何学Camp;& 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:
- You run a command like
s chown matt file\ with\ spaces
- bash parses this into a list of words: "s" "chown" "matt" "file with spaces". Note that at this point the escapes you typed have been removed (although they had their intended effect: making bash treat those spaces as part of a parameter, rather than separators between parameters).
- When bash parses the
printf "%q " "$@"
command in the script, it replaces"$@"
with the arguments to the script, with parameter breaks intact. It's equivalent toprintf "%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).- This is then passed to sudo as a parameter (since there are double-quotes around the
$()
construct, it'll be treated as a single parameter to sudo). This is equivalent to runningsudo su -c "chown matt file\ with\ spaces "
. sudo
runssu
, 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.- The shell executes the command it got as an argument to
-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. - The shell runs
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.
精彩评论