Question

I'm creating a bash script which involves parsing arguments. The usage would be:

$ ./my_script.sh -a ARG_1 -b ARG_2 [-c LIST_OF_ARGS...]

Using getopts I'm able to parse -a and -b and get their respective values ARG_1 and ARG_2. If and only if user places -c as last argument, then I'm also able to get -c and create a list with all values in LIST_OF_ARGS....

But I would not like to force user to insert -c as the last flag. For instance, it would be great if the script can be invoked this way:

$ ./my_script.sh -b ARG_2 -c V1 V2 V3 -a ARG_1

Here is my current code:

while getopts a:b:c opt
do
    case $opt in
        a)
            A_FLAG=$OPTARG
            ;;
        b)
            B_FLAG=$OPTARG
            ;;
        c)
            # Handle values as regular expressions
            args=("$@")
            C_LIST=()
            for (( i=$OPTIND-1 ; i <= $#-1 ; i++ ))
            do
                C_LIST=("${C_LIST[@]}" ${args[$i]})
            done
            ;;
        ?)
            usage
            ;;
    esac
done
Was it helpful?

Solution

On my system, I haven a /usr/share/doc/util-linux/examples/getopt-parse.bash file. It puts the result of getopt into a variable, and set the positional parameters to that variable. Then uses a switch similar to yours, but uses shift to remove arguments when found.

You could do something similar, but for your -c option use shift until you get an option or run out of arguments.


Or it might be enough for you to use your current solution, but remember to set the OPTIND variable after the loop.

OTHER TIPS

You need to separate your detection of the -c flag with the processing associated with it. For example, something like:

while getopts a:b:c opt
do
    case $opt in
        a)
            A_FLAG=$OPTARG
            ;;
        b)
            B_FLAG=$OPTARG
            ;;
        c)
            C_FLAG=1
            ;;
        ?)
            usage
            ;;
    esac
done

# discard all of our options.
shift `expr $OPTIND - 1`

if [ "$C_FLAG" = 1 ]; then
            # Handle values as regular expressions
            args=("$@")
            C_LIST=()
            for (( i=0 ; i <= $#-1 ; i++ ))
            do
                C_LIST=("${C_LIST[@]}" ${args[$i]})
            done
fi

This script doesn't collect all the non-option arguments until after processing all the command line options.

Here's a question: why have a -c option at all?

If the full usage involves a list of values, why not just have no -c option and allow the -a and -b options only while the rest are regular args as in ./myscript.sh -a ARG_1 -b ARG_2 [argument ...], where any arguments are optional (like the -c option and its arguments are in your usage example?

Then your question becomes "how do I intersperse program options and arguments", to which I would respond: "You shouldn't do this, but to achieve this anyway, parse the command line yourself; getopts won't work the way you want it to otherwise."

Of course, parsing is the hard way. Another possibility involves adding the values after -c to a list, so long as you don't encounter another option or the end of the options:

C_LIST=()
while getopts a:b:c: opt; do
    #Skipping code...
    c)
        C_LIST+="$OPTARG"
        shift $(expr $OPTIND - 1)
        while [ -n "$1" ] && [ $(printf "%s" "$1" | grep -- '^[^-]') ]; do
            C_LIST+="$1"
            shift
        done
        OPTIND=1
        ;;

The behaviour of getopts is mimicked: even if OPTARG begins with a '-' character, it is still kept, but after OPTARG, any string starting with the '-' character may simply be an invalid option such as -n. I used printf instead of echo because some versions of echo, such as the one that bash has built-in, have a -e option that may or may not allow the loop to continue, which isn't desired. The grep expression should prevent this, but who knows if that version of echo allows for -e'hello', which would cause grep to succeed because it sees "hello"? While possibly unnecessary, why take chances?

Personally, I'd avoid this behaviour if you can, but I also don't understand why you're asking for this behaviour in the first place. If I were to recommend anything, I'd suggest the more common /path/to/script -a ARG_1 -b ARG_2 [argument ...] style above any other possible choice of implementation.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top