bashbash4

Pass command line arguments and command line argument


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?


Solution

  • 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