Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bash parameter variables in script problem

Tags:

variables

bash

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 $@ ?

like image 394
Matt Avatar asked Apr 27 '11 19:04

Matt


2 Answers

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:

  1. You run a command like s chown matt file\ with\ spaces
  2. 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).
  3. 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 to printf "%q " "chown" "matt" "file with spaces".
  4. 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).
  5. 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 running sudo su -c "chown matt file\ with\ spaces ".
  6. sudo runs su, and passes along the rest of the parameter list it got including the fully escaped command.
  7. su runs a shell, and it also passes along the rest of its parameter list.
  8. 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.
  9. The shell runs chown, with the parameters "matt" and "file with spaces". This does what you expected.

Isn't bash parsing a hoot?

like image 157
Gordon Davisson Avatar answered Oct 16 '22 22:10

Gordon Davisson


"$*" 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.

like image 31
Gilles 'SO- stop being evil' Avatar answered Oct 16 '22 22:10

Gilles 'SO- stop being evil'