I want my script to be called like:
./script -f "-a -b "foo bar"" baz
where -a -b "foo bar"
are command line arguments to pass to a command (e.g. grep
) internally executed by my script.
The problem here does of course concern quoting: the naive approach (expanding the command-line argument to script
) treats foo
and bar
as separate arguments, which is of course undesirable.
What's the best way to accomplish this?
There is no really great way of accomplishing that, only workarounds. Almost all common solutions involve passing individual arguments as individual arguments, because the alternative -- passing a list of arguments as a single argument -- makes you jump through hoops of fire every time you want to pass an even slightly complicated argument (which, it will turn out, will be common; your example is just the start of the metaquoting morass you're about to sink into. See this Bash FAQ entry for more insights).
Probably the most common solution is to put the arguments to pass through at the end of the argument list. That means that you need to know the end of the arguments which are actually for your script, which more or less implies that there are a fixed number of positional parameters. Typically, the fixed number would be 1. An example of this strategy is pretty well any script interpreter (bash
itself, python
, etc.) which take exactly one positional argument, the name of the script. That would make your invocation:
./script baz -a -b "foo bar"
That's not always convenient. The find
command, for example, has an -exec
option which is followed by an actual command in the following arguments. To do that, you have to know where the words to be passed through end; find
solves that by using a specific delimiter argument: a single semicolon. (That was an arbitrary choice, but it is very rare as a script argument so it usually works out.) In that case, your invocation would look like:
./script -f -a -b "foo bar" \; baz
The ;
needs to be quoted, obviously, because otherwise it would terminate the command, but that is not a complicated quoting problem.
That could be extended by allowing the user to explicitly specify a delimiter word:
./script --arguments=--end -a -b "foo bar" --end baz
Here's some sample code for the find
-like suggestion:
# Use an array to accumulate the arguments to be passed through
declare -a passthrough=()
while getopts f:xyz opt; do
case "$opt" in
f)
passthrough+=("$OPTARG")
while [[ ${!OPTIND} != ';' ]]; do
if ((OPTIND > $#)); then
echo "Unterminated -f argument" >>/dev/stderr
exit 1
fi
passthrough+=("${!OPTIND}")
((++OPTIND))
done
((++OPTIND))
;;
# Handle other options
esac
done
# Handle positional arguments
# Use the passthrough arguments
grep "${passthrough[@]}" # Other grep arguments