Prabhu Konchada
Prabhu Konchada

Reputation: 548

how to separate optionals from arguments in bash?

My script needs two arguments to run.

It can have optionals if needed so I want to ensure these properties so I have written the following script

 if [ $# -lt 3 ]
    then
     echo "This command need 3 arguments"
    else
     echo $1 $2 $3
  fi

observed that "$#" counts total number of arguments passed including the optionals

OUTPUT :
sh my_script.sh -o optional1 -r.

It works but not as intended as my check fails.

Expected Output :(two arguments are compulsory)
sh my_script.sh arg1 arg2 --> should work
sh my_script.sh -o optional1 -r optional2 --> throw error that it requires two arguments
sh my_script.sh -o optional1 -r optional2 arg1 arg2 --> should work

Upvotes: 1

Views: 981

Answers (3)

David C. Rankin
David C. Rankin

Reputation: 84561

Note, $# reports the number of arguments (positional parameters). It doesn't care what they are. Anything on the command line is considered an argument. (e.g. somevar or -o). So to distinguish between -foo and foo, you will need to either write an interpreter, or use getopts as provided by others (as that interpreter)

While getopts is fine, I generally want my own interpreter. To provide a custom interpreter in sh, you will generally use a while loop, shift and a case statement to check each positional parameter and respond accordingly. While string manipulation is somewhat limited in sh compared to bash it is sufficient.

An example that fits your requirements could be:

#!/bin/sh

if [ $# -lt 2 ]; then
    printf "This command need 2 arguments\n"
    exit 1
fi

let nargs=0

while [ "$1" != "" ]; do
    printf " processing: %s\n" "$1"
    case "$1" in
        -r  )   shift
                [ "$1" = "" -o  `expr index "$1" "-"` -eq 1 ] && \
                { printf "error: -r requires an option.\n"; exit 1; }
                r_var="$1"
                printf "   -r %s\n" "$r_var"
                let nargs=$nargs+1
                ;;
        -*  )   ;;  ## don't count '-x' arguments as options (change as needed)
        *   )   let nargs=$nargs+1
                ;;
    esac
    shift
done

if [ $nargs -lt 2 ]; then
    printf "\n Two compulsory arguments not given.\n\n"
    exit 1
else
    printf "\n required arguments satisfied.\n\n"
fi


exit 0

Example Use/Output

$ sh params.sh arg1 arg2
 processing: arg1
 processing: arg2

 required arguments satisfied.


$ sh params.sh -o optional -r
 processing: -o
 processing: optional
 processing: -r
error: -r requires an option.


$ sh params.sh -o optional -r arg1 arg2
 processing: -o
 processing: optional
 processing: -r
   -r arg1
 processing: arg2

 required arguments satisfied.


$ sh params.sh -o arg2
 processing: -o
 processing: arg2

 Two compulsory arguments not given.

Checking for Duplicate Arguments

A nested for loop checking the positional parameters for duplicates is probably the best way to check for duplicate arguments if you don't want to try and cover everything in the case statement. One approach would be to check at the beginning of your script:

#!/bin/sh

let args=2

## check duplicate arguments
for i ; do
    for j in $(seq $args $#); do
        printf "i: %s  j: %s compare %s %s\n" $i $j $i ${!j}  ## delete - just for info
        if [ $i = ${!j} ]; then
            printf "error: duplicate args [ %s = %s ], exiting.\n" $i ${!j}
            exit 1;
        fi
    done
    let args=$args+1
done

exit 0

Examples/Output

$ sh pdupes.sh a b c d e
i: a  j: 2 compare a b
i: a  j: 3 compare a c
i: a  j: 4 compare a d
i: a  j: 5 compare a e
i: b  j: 3 compare b c
i: b  j: 4 compare b d
i: b  j: 5 compare b e
i: c  j: 4 compare c d
i: c  j: 5 compare c e
i: d  j: 5 compare d e

$ sh pdupes.sh a b c d b e
i: a  j: 2 compare a b
i: a  j: 3 compare a c
i: a  j: 4 compare a d
i: a  j: 5 compare a b
i: a  j: 6 compare a e
i: b  j: 3 compare b c
i: b  j: 4 compare b d
i: b  j: 5 compare b b
error: duplicate args [ b = b ], exiting.

Preventing Accepting Argument Twice

In addition to the loop check above which will catch any duplicate, you can also add an option to the case statement that will prevent accepting an argument twice. It is up to you whether you just want to skip the duplicate or flag it and exit. In the case of your -o with optional argument, I would try something like the following which accepts an optional argument, tests it is not another -x type and checks whether it was already set. If it passes all the tests, `o_var is set and by being set, it can't be set again:

    -o  )   shift
            [ "$1" = "" -o  `expr index "$1" "-"` -eq 1 -o ! -z $o_var ] && \
            { printf "duplicate value for '-o' received, exiting\n"; exit 1; } || \
            o_var="$1" && let nargs=$nargs+1
            printf "   -o %s\n" "$o_var"
            ;;

Slight Variations Controlling Response to Duplicate

There are two additional ways of handling multiple attempts to set -o. The first is to just exit on any second appearance of -o in the argument list (regardless of whether there is an attempt to set the optional variable associated with -o):

    -o  )   shift
            ## throw error on any second instance of '-o' 
            if [ -z "$o_var" ]; then
                if [ ! `expr index "$1" "-"` -eq 1 -a "$1" != "" ]; then
                    o_var="$1"
                    let nargs=$nargs+1
                    printf "   -o %s\n" "$o_var"
                fi
            else
                    printf "error: multiple attempts to set '-o', exiting.\n" "$o_var" "$1"
                    exit 1                
            fi
            ;;

Examples

Both attempts to set -o a second time are treated the same:

$ sh params.sh -o foo -r arg1 arg2 -o bar
$ sh params.sh -o foo -r arg1 arg2 -o
 processing: -o
   -o foo
 processing: -r
   -r arg1
 processing: arg2
 processing: -o
error: multiple attempts to set '-o', exiting.

The other possibility is to only throw an error only if there is a second attempt to set the optional variable associated with -o. (e.g. if the second -o is harmless, ignore it):

    -o  )   shift
            ## throw error only if second instance of '-o' has optional argument
            if [ "$1" != "" -a ! `expr index "$1" "-"` -eq 1 ]; then
                if [ -z "$o_var" ]; then
                    o_var="$1"
                    let nargs=$nargs+1
                    printf "   -o %s\n" "$o_var"
                else
                    printf "error: multiple attempts to set '-o'. (%s, now '%s')\n" "$o_var" "$1"
                    exit 1
                fi
            fi
            ;;

Examples

$ sh params.sh -o foo -r arg1 arg2 -o bar
 processing: -o
   -o foo
 processing: -r
   -r arg1
 processing: arg2
 processing: -o
error: multiple attempts to set '-o'. (foo, now 'bar')

$ sh params.sh -o foo -r arg1 arg2 -o
 processing: -o
   -o foo
 processing: -r
   -r arg1
 processing: arg2
 processing: -o

 required arguments satisfied.

Upvotes: 3

123
123

Reputation: 11216

Without getopts, but pretty much the same thing, i prefer this way because it is easier to add long args.

#!/bin/bash


while true;do

        case $1 in

        '-o'|'--optional')
                optional1=$2
                shift 2
                ;;
        '-r')
                optional2=$2
                shift 2
                ;;
        *)
                break
                ;;
        esac
done

if [[ $# -lt 2 ]];then
    echo Too few args.
    exit 1
fi

echo $optional1
echo $optional2

Obviously it could be more robust as this will take multiple of the same flags.

Upvotes: 2

Jonathan Leffler
Jonathan Leffler

Reputation: 753765

  1. Process the options with getopts.

    r_flag=0
    o_flag="default value"
    while getopts o:r arg
    do
        case "$arg" in
        (o) o_flag="$OPTARG";;
        (r) r_flag=1;;
        (*) echo "Unrecognized option $arg" >&2; exit 1;;
        esac
    done
    
  2. After the loop doing that, use shift $(($OPTIND - 1)) to get rid of the processed options.

  3. Now you can check that there are at least 2 more arguments:

    if [ $# -lt 2 ]
    then
        echo "Usage: $(basename $0 .sh) [-o option][-r] arg1 arg2 ..." >&2
        exit 1
    fi
    

Upvotes: 2

Related Questions