Is mixing getopts with positional parameters possible?

BashShellParameters

Bash Problem Overview


I want to design a shell script as a wrapper for a couple of scripts. I would like to specify parameters for myshell.sh using getopts and pass the remaining parameters in the same order to the script specified.

If myshell.sh is executed like:

myshell.sh -h hostname -s test.sh -d waittime param1 param2 param3

myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh

myshell.sh param1 -h hostname -d waittime -s test.sh param2 param3

All of the above should be able to call as

test.sh param1 param2 param3

Is it possible to utilize the options parameters in the myshell.sh and post remaining parameters to underlying script?

Bash Solutions


Solution 1 - Bash

I wanted to do something similar to the OP, and I found the relevant information I required here and here

Essentially if you want to do something like:

script.sh [options] ARG1 ARG2

Then get your options like this:

while getopts "h:u:p:d:" flag; do
case "$flag" in
    h) HOSTNAME=$OPTARG;;
    u) USERNAME=$OPTARG;;
    p) PASSWORD=$OPTARG;;
    d) DATABASE=$OPTARG;;
esac
done

And then you can get your positional arguments like this:

ARG1=${@:$OPTIND:1}
ARG2=${@:$OPTIND+1:1}

More information and details are available through the link above.

Hope that helps!!

Solution 2 - Bash

Mix opts and args :

ARGS=""
echo "options :"
while [ $# -gt 0 ]
do
    unset OPTIND
    unset OPTARG
    while getopts as:c:  options
    do
    case $options in
            a)  echo "option a  no optarg"
                    ;;
            s)  serveur="$OPTARG"
                    echo "option s = $serveur"
                    ;;
            c)  cible="$OPTARG"
                    echo "option c = $cible"
                    ;;
        esac
   done
   shift $((OPTIND-1))
   ARGS="${ARGS} $1 "
   shift
done

echo "ARGS : $ARGS"
exit 1

Result:

bash test.sh  -a  arg1 arg2 -s serveur -c cible  arg3
options :
option a  no optarg
option s = serveur
option c = cible
ARGS :  arg1  arg2  arg3

Solution 3 - Bash

myshell.sh:

#!/bin/bash

script_args=()
while [ $OPTIND -le "$#" ]
do
    if getopts h:d:s: option
    then
        case $option
        in
            h) host_name="$OPTARG";;
            d) wait_time="$OPTARG";;
            s) script="$OPTARG";;
        esac
    else
        script_args+=("${!OPTIND}")
        ((OPTIND++))
    fi
done

"$script" "${script_args[@]}"

test.sh:

#!/bin/bash
echo "$0 $@"

Testing the OP's cases:

$ PATH+=:.  # Use the cases as written without prepending ./ to the scripts
$ myshell.sh -h hostname -s test.sh -d waittime param1 param2 param3
./test.sh param1 param2 param3
$ myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh
./test.sh param1 param2 param3
$ myshell.sh param1 -h hostname -d waittime -s test.sh param2 param3
./test.sh param1 param2 param3

What's going on:

getopts will fail if it encounters a positional parameter. If it's used as a loop condition, the loop would break prematurely whenever positional parameters appear before options, as they do in two of the test cases.

So instead, this loop breaks only once all parameters have been processed. If getopts doesn't recognize something, we just assume it's a positional parameter, and stuff it into an array while manually incrementing getopts's counter.

Possible improvements:

As written, the child script can't accept options (only positional parameters), since getopts in the wrapper script will eat those and print an error message, while treating any argument like a positional parameter:

$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./myshell.sh: illegal option -- a
./test.sh param1 param2 opt1 param3

If we know the child script can only accept positional parameters, then myshell.sh should probably halt on an unrecognized option. That could be as simple as adding a default last case at the end of the case block:

            \?) exit 1;;
$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./myshell.sh: illegal option -- a

If the child script needs to accept options (as long as they don't collide with the options in myshell.sh), we could switch getopts to silent error reporting by prepending a colon to the option string:

    if getopts :h:d:s: option

Then we'd use the default last case to stuff any unrecognized option into script_args:

            \?) script_args+=("-$OPTARG");;
$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./test.sh param1 param2 -a opt1 param3

Solution 4 - Bash

getopts won't parse the mix of param1 and -n options.

It is much better to put param1-3 into options like others.

Furthermore you can use already existing libraries such as shflags. It is pretty smart and it is easy to use.

And the last way is to write your own function to parse params without getopts, just iterating all params through case construction. It is the hardest way but it is the only way to match your expectations exactly.

Solution 5 - Bash

I thought up one way that getopts can be extended to truly mix options and positional parameters. The idea is to alternate between calling getopts and assigning any positional parameters found to n1, n2, n3, etc.:

parse_args() {
    _parse_args 1 "$@"
}

_parse_args() {
    local n="$1"
    shift

    local options_func="$1"
    shift

    local OPTIND
    "$options_func" "$@"
    shift $(( OPTIND - 1 ))

    if [ $# -gt 0 ]; then
        eval test -n \${n$n+x}
        if [ $? -eq 0 ]; then
            eval n$n="\$1"
        fi

        shift
        _parse_args $(( n + 1 )) "$options_func" "$@"
    fi
}

Then in the OP's case, you could use it like:

main() {
    local n1='' n2='' n3=''
    local duration hostname script

    parse_args parse_main_options "$@"

    echo "n1 = $n1"
    echo "n2 = $n2"
    echo "n3 = $n3"
    echo "duration = $duration"
    echo "hostname = $hostname"
    echo "script   = $script"
}

parse_main_options() {
    while getopts d:h:s: opt; do
        case "$opt" in
            d) duration="$OPTARG" ;;
            h) hostname="$OPTARG" ;;
            s) script="$OPTARG"   ;;
        esac
    done
}

main "$@"

Running it shows the output:

$ myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh
n1 = param1
n2 = param2
n3 = param3
duration = waittime
hostname = hostname
script   = test.sh

Just a proof of concept, but maybe it's useful to someone.

Note: there's a gotcha if one function that uses parse_args calls another function that uses parse_args and the outer function declares e.g. local n4='', but the inner one doesn't and 4 or more positional parameters are passed to the inner function

Solution 6 - Bash

Just mashed up a quickie, which easily handles a mixture of options and positional-parameters (leaving only positional-params in $@):

#!/bin/bash
while [ ${#} -gt 0 ];do OPTERR=0;OPTIND=1;getopts "p:o:hvu" arg;case "$arg" in
        p) echo "Path:   [$OPTARG]" ;;
        o) echo "Output: [$OPTARG]" ;;
        h) echo "Help"              ;;
        v) echo "Version"           ;;
    \?) SET+=("$1")                                           ;;
    *) echo "Coding error: '-$arg' is not handled by case">&2 ;;
esac;shift;[ "" != "$OPTARG" ] && shift;done
[ ${#SET[@]} -gt 0 ] && set "" "${SET[@]}" && shift

echo -e "=========\nLeftover (positional) parameters (count=$#) are:"
for i in `seq $#`;do echo -e "\t$i> [${!i}]";done

Sample output:

[root@hots:~]$ ./test.sh 'aa bb' -h -v -u -q 'cc dd' -p 'ee ff' 'gg hh' -o ooo
Help
Version
Coding error: '-u' is not handled by case
Path:   [ee ff]
Output: [ooo]
=========
Leftover (positional) parameters (count=4) are:
        1> [aa bb]
        2> [-q]
        3> [cc dd]
        4> [gg hh]
[root@hots:~]$

Solution 7 - Bash

Instead of using getopts, you can directly implement your own bash argument parser. Take this as a working example. It can handle simultaneously name and position arguments.

#!/bin/bash

function parse_command_line() {
    local named_options;
    local parsed_positional_arguments;

    yes_to_all_questions="";
    parsed_positional_arguments=0;

    named_options=(
            "-y" "--yes"
            "-n" "--no"
            "-h" "--help"
            "-s" "--skip"
            "-v" "--version"
        );

    function validateduplicateoptions() {
        local item;
        local variabletoset;
        local namedargument;
        local argumentvalue;

        variabletoset="${1}";
        namedargument="${2}";
        argumentvalue="${3}";

        if [[ -z "${namedargument}" ]]; then
            printf "Error: Missing command line option for named argument '%s', got '%s'...\\n" "${variabletoset}" "${argumentvalue}";
            exit 1;
        fi;

        for item in "${named_options[@]}";
        do
            if [[ "${item}" == "${argumentvalue}" ]]; then
                printf "Warning: Named argument '%s' got possible invalid option '%s'...\\n" "${namedargument}" "${argumentvalue}";
                exit 1;
            fi;
        done;

        if [[ -n "${!variabletoset}" ]]; then
            printf "Warning: Overriding the named argument '%s=%s' with '%s'...\\n" "${namedargument}" "${!variabletoset}" "${argumentvalue}";
        else
            printf "Setting '%s' named argument '%s=%s'...\\n" "${thing_name}" "${namedargument}" "${argumentvalue}";
        fi;
        eval "${variabletoset}='${argumentvalue}'";
    }

    # https://stackoverflow.com/questions/2210349/test-whether-string-is-a-valid-integer
    function validateintegeroption() {
        local namedargument;
        local argumentvalue;

        namedargument="${1}";
        argumentvalue="${2}";

        if [[ -z "${2}" ]];
        then
            argumentvalue="${1}";
        fi;

        if [[ -n "$(printf "%s" "${argumentvalue}" | sed s/[0-9]//g)" ]];
        then
            if [[ -z "${2}" ]];
            then
                printf "Error: The %s positional argument requires a integer, but it got '%s'...\\n" "${parsed_positional_arguments}" "${argumentvalue}";
            else
                printf "Error: The named argument '%s' requires a integer, but it got '%s'...\\n" "${namedargument}" "${argumentvalue}";
            fi;
            exit 1;
        fi;
    }

    function validateposisionaloption() {
        local variabletoset;
        local argumentvalue;

        variabletoset="${1}";
        argumentvalue="${2}";

        if [[ -n "${!variabletoset}" ]]; then
            printf "Warning: Overriding the %s positional argument '%s=%s' with '%s'...\\n" "${parsed_positional_arguments}" "${variabletoset}" "${!variabletoset}" "${argumentvalue}";
        else
            printf "Setting the %s positional argument '%s=%s'...\\n" "${parsed_positional_arguments}" "${variabletoset}" "${argumentvalue}";
        fi;
        eval "${variabletoset}='${argumentvalue}'";
    }

    while [[ "${#}" -gt 0 ]];
    do
        case ${1} in
            -y|--yes)
                yes_to_all_questions="${1}";
                printf "Named argument '%s' for yes to all questions was triggered.\\n" "${1}";
                ;;

            -n|--no)
                yes_to_all_questions="${1}";
                printf "Named argument '%s' for no to all questions was triggered.\\n" "${1}";
                ;;

            -h|--help)
                printf "Print help here\\n";
                exit 0;
                ;;

            -s|--skip)
                validateintegeroption "${1}" "${2}";
                validateduplicateoptions g_installation_model_skip_commands "${1}" "${2}";
                shift;
                ;;

            -v|--version)
                validateduplicateoptions branch_or_tag "${1}" "${2}";
                shift;
                ;;

            *)
                parsed_positional_arguments=$((parsed_positional_arguments+1));

                case ${parsed_positional_arguments} in
                    1)
                        validateposisionaloption branch_or_tag "${1}";
                        ;;

                    2)
                        validateintegeroption "${1}";
                        validateposisionaloption g_installation_model_skip_commands "${1}";
                        ;;

                    *)
                        printf "ERROR: Extra positional command line argument '%s' found.\\n" "${1}";
                        exit 1;
                        ;;
                esac;
                ;;
        esac;
        shift;
    done;

    if [[ -z "${g_installation_model_skip_commands}" ]];
    then
        g_installation_model_skip_commands="0";
    fi;
}

You would call this function as:

#!/bin/bash
source ./function_file.sh;
parse_command_line "${@}";

Usage example:

./test.sh as 22 -s 3
Setting the 1 positional argument 'branch_or_tag=as'...
Setting the 2 positional argument 'skip_commands=22'...
Warning: Overriding the named argument '-s=22' with '3'...

References:

  1. example_installation_model.sh.md
  2. https://stackoverflow.com/questions/4341630/checking-for-the-correct-number-of-arguments
  3. https://unix.stackexchange.com/questions/129391/passing-named-arguments-to-shell-scripts
  4. https://stackoverflow.com/questions/16483119/an-example-of-how-to-use-getopts-in-bash

Solution 8 - Bash

There are some standards for unix option processing, and in shell programming, getopts is the best way of enforcing them. Almost any modern language (perl, python) has a variant on getopts.

This is just a quick example:

command [ options ] [--] [ words ]
  1. Each option must start with a dash, -, and must consist of a single character.

  2. The GNU project introduced Long Options, starting with two dashes --, followed by a whole word, --long_option. The AST KSH project has a getopts that also supports long options, and long options starting with a single dash, -, as in find(1) .

  3. Options may or may not expect arguments.

  4. Any word not starting with a dash, -, will end option processing.

  5. The string -- must be skipped and will end option processing.

  6. Any remaining arguments are left as positional parameters.

The Open Group has a section on Utility Argument Syntax

Eric Raymond's The Art of Unix Programming has a chapter on traditional unix choices for option letters and their meaning.

Solution 9 - Bash

You can try this trick: after while loop with optargs, just use this snippet

#shift away all the options so that only positional agruments
#remain in $@

for (( i=0; i<OPTIND-1; i++)); do
    shift
done

POSITIONAL="$@"

However, this approach has a bug:

    all the options after the first positional argument are ingored by getopts and are considered as positional arguments - event those that are correct (see sample output: -m and -c are among positional arguments)

Maybe it has even more bugs...

Look at the whole example:

while getopts :abc opt; do
    case $opt in
        a)
        echo found: -a
        ;;
        b)
        echo found: -b
        ;;
        c)
        echo found: -c
        ;;
        \?) echo found bad option: -$OPTARG
        ;;
    esac
done

#OPTIND-1 now points to the first arguments not beginning with -

#shift away all the options so that only positional agruments
#remain in $@

for (( i=0; i<OPTIND-1; i++)); do
    shift
done

POSITIONAL="$@"

echo "positional: $POSITIONAL"

Output:

[root@host ~]# ./abc.sh -abc -de -fgh -bca haha blabla -m -c
found: -a
found: -b
found: -c
found bad option: -d
found bad option: -e
found bad option: -f
found bad option: -g
found bad option: -h
found: -b
found: -c
found: -a
positional: haha blabla -m -c

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBharat SinhaView Question on Stackoverflow
Solution 1 - BashDRendarView Answer on Stackoverflow
Solution 2 - BashchichiView Answer on Stackoverflow
Solution 3 - BashChris PearsonView Answer on Stackoverflow
Solution 4 - BashrushView Answer on Stackoverflow
Solution 5 - BashMichael KropatView Answer on Stackoverflow
Solution 6 - BashVladView Answer on Stackoverflow
Solution 7 - BashuserView Answer on Stackoverflow
Solution 8 - BashHenk LangeveldView Answer on Stackoverflow
Solution 9 - BashYuri PozniakView Answer on Stackoverflow