views:

393

answers:

2

I'm storing the arguments to a command in a variable. The final command I want is:

mock -r myconfig --define "debug_package %{nil}" --resultdir results --rebuild mypackage.src.rpm

Here's my attempt:

set -x    # for debugging

RESULTDIR=results
MOCK_CONFIG="myconfig"
MOCK_ARGS="-r $MOCK_CONFIG --define \"debug_package %{nil}\" --resultdir $RESULTDIR"
cmd="mock $MOCK_ARGS --rebuild mypackage.src.rpm"
$cmd

The results are:

+ RESULTDIR=results
+ MOCK_CONFIG=myconfig
+ MOCK_ARGS='-r myconfig --define "debug_package %{nil}" --resultdir results'
+ cmd='mock -r myconfig --define "debug_package %{nil}" --resultdir results --rebuild mypackage.src.rpm'
+ mock -r myconfig --define '"debug_package' '%{nil}"' --resultdir results --rebuild mypackage.src.rpm
ERROR: Bad option for '--define' ("debug_package).  Use --define 'macro expr'

As you can see, the arguments to the --define parameter are not being quoted properly. --define thinks I'm passing it only debug_package, which is incomplete.

I have tried various variations in the quotes when defining MOCK_ARGS, even trying to escape the space between debug_package and %{nil}.

What combination of quotes and/or escapes allows me to build this argument list and execute the command from this script?

EDIT:

The reason I'm storing the resulting command in a variable is because it ends up being passed into a function which does some logging, then executes the command.

Also, I have come across this FAQ which suggests I should use arrays instead of a variable. I've begun experimenting with arrays but so far don't have a working solution.

+1  A: 

Shell scripting can be miserable. In many cases, cmd="blah $blah";$cmd differs from blah $blah. You can try eval $cmd` instead of $cmd.

Try calling this perl script instead of mock:

#!/usr/bin/perl
print join("\n",@ARGV),"\n";

You may be surprised to see what the argument list actually is:

-r
myconfig
--define
"debug_package
%{nil}"
--resultdir
results
--rebuild
mypackage.src.rpm

Although I guess the "set -x" output is trying to show you this with the single quotes.

One nice trick I recently learned is that

function f {
  echo "$@"
}

Actually properly forwards the positional arguments to f ($* doesn't).

It looks like "eval" gives what you probably intend:

set -x    # for debugging
RESULTDIR=results
MOCK_CONFIG="myconfig"
MOCK_ARGS="-r $MOCK_CONFIG "'--define "debug_package %{nil}" --resultdir '"$RESULTDIR"
cmd="mock $MOCK_ARGS --rebuild mypackage.src.rpm"
echo $cmd
eval $cmd

Output:

+ echo mock -r myconfig --define '"debug_package' '%{nil}"' --resultdir results --rebuild mypackage.src.rpm
mock -r myconfig --define "debug_package %{nil}" --resultdir results --rebuild mypackage.src.rpm
+ eval mock -r myconfig --define '"debug_package' '%{nil}"' --resultdir results --rebuild mypackage.src.rpm
++ mock -r myconfig --define 'debug_package %{nil}' --resultdir results --rebuild mypackage.src.rpm
-r
myconfig
--define
debug_package %{nil}
--resultdir
results
--rebuild
mypackage.src.rpm
wrang-wrang
+1  A: 

Arrays are the way to go for things like this. Here's what I came up with:

log_and_run() {
    echo "$(date): running command: $*"
    "$@"
    echo "$1 completed with status $?"
}

RESULTDIR=results
MOCK_CONFIG="myconfig"
MOCK_ARGS=(-r "$MOCK_CONFIG" --define "debug_package %{nil}" --resultdir "$RESULTDIR")
cmd=(mock "${MOCK_ARGS[@]}" --rebuild mypackage.src.rpm)
log_and_run "${cmd[@]}"
# could also use: log_and_run mock "${MOCK_ARGS[@]}" --rebuild mypackage.src.rpm

Note that $* expands to all parameters seperated by spaces (suitable for passing to a log command); while "$@" expands to all parameters as separate words (as used in log_and_run, the first will be treated as the command to execute, the rest as parameters to it); similarly, "${MOCK_ARGS[@]}" expands to all of the elements of MOCK_ARGS as separate words, whether or not they contain spaces (hence each element of MOCK_ARGS becomes an element of cmd, with no quote-parsing confusion).

Also, I've quoted the expansions of MOCK_CONFIG and RESULTDIR; this isn't necessary here because they don't contain spaces, but is good habit to get in in case they ever do happen to contain spaces (e.g. is it passed into your script from outside? Then you should assume it might contain spaces).

BTW, if you need to pass additional parameters to the function (I'll use the example of the filename to log to), you can use array slicing to split off only the later parameters to use as the command to log/execute:

log_and_run() {
    echo "$(date): running command ${*:2}" >>"$1" 
    "${@:2}"
    echo "$2 completed with status $?" >>"$1"
}
#...
log_and_run test.log "${cmd[@]}"

Addendum: if you want to quote space-containing parameters in the log, you can "manually" construct the log string and do whatever quoting you want. For instance:

log_and_run() {
    local log_cmd=""
    for arg in "$@"; do
        if [[ "$arg" == *" "* || -z "$arg" ]]; then
            log_cmd="$log_cmd \"$arg\""
        else
            log_cmd="$log_cmd $arg"
        fi
    done
    echo "$(date): running command:$log_cmd"
    ...

Note that this will handle spaces in parameters and blank parameters, but not (for instance) double-quotes inside parameters. Doing this "right" can get arbitrarily complicated...

Also, the way I construct the log_cmd string, it winds up with a leading space; I handled this above by omitting the space before it in the echo command. If you need to actually trim the space, use "${log_cmd# }".

Gordon Davisson
Thanks, this solves my problem! The stroke of genius was to make the complete `mock` command an array. This led to refactoring of my `log_and_run()` function which naively used `$1` to log and run the command instead of `$*` and/or `$@`. I couldn't get the quotes around `"debug_package %{nil}"` to show up in the logs, but that's a minor detail I can live with.
Mike Mazur