1

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?

shellter
  • 36,525
  • 7
  • 83
  • 90
Noldorin
  • 144,213
  • 56
  • 264
  • 302

2 Answers2

4

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
rici
  • 234,347
  • 28
  • 237
  • 341
1

I found a shorter method to pass script arguments as a parameter to command with only restriction: a command should be evaluated.

In therms of your question, the following script will do what you want

#!/bin/bash

# Do whatever you want ...
# Just claim that 2nd quoted argument of script should be passed as
# a set of parameters of grep
eval grep $2

Then you can call this script with space-including parameters to be passed to grep

./script -f "-a -b \"foo bar\"" baz

or

./script -f '-a -b "foo bar"' baz

Voila! The grep will be called with correct parameters

grep -a -b "foo bar"

For example, call

./script -f '-i "system message" /etc/passwd' baz

then got correct output

dbus:x:81:81:System message bus:/:/sbin/nologin
MrCryo
  • 641
  • 7
  • 16
  • That clearly doesn't avoid the need for complicated metaquoting; read the bash FAQ in my answer. Also, you should think very carefully about the difference between `eval grep "$2"` and `eval grep $2`; you probably meant the first one, but both have corner cases. – rici Sep 30 '17 at 18:54
  • I agree. This is not a common solution. This just a simple fast solution for most often issue to pass, say, string with spaces as parameter. – MrCryo Oct 06 '17 at 16:26