d3pd
d3pd

Reputation: 8315

Bash manual input and shift of positional parameters

(Briskly, this question concerns mainly how to apply information to getopts and how to apply a shift to a manually-defined array.

I'm writing a procedure for the start of some Bash functions that parses parameters for a function in the usual getopts way while also parsing unexpected parameters (by looping over getopts processing multiple times).

In the code below can be seen a little function process_arguments() which essentially contains the getopts processing and a concluding shift operation which acts on the positional parameters. Because this overall procedure for parameter parsing is destined for functions as opposed to scripts, I'd like to move the processing contained in the function process_arguments to the main while loop in the script. This involves a modification to the input and output information from the procedure in the function.

The input information for the getopts processing can be passed to getopts in the array args:

getopts optstring name [args]

However, I'm not sure of how the output of the procedure can be changed. At present, it shifts the positional parameters and then it prints the positional parameters (echo "${@}").

So, questions...

Thanks for your help. I welcome any other guidance. The overall goal is to remove the function process_arguments() and incorporate its functionality into the main ``while``` loop.

#!/bin/bash
################################################################################
#
# This is a test of command line parameter parsing that is explicitly non-POSIX.
# This approach handles
#     - options (for getopts)
#     - arguments of options (for getopts)
#     - positional arguments (for getopts) and
#     - non-option arguments (command line parameters not expected by getopts).
# All non-option arguments are placed in the string non_option_parameters. The
# silent error reporting mode of getopts is required. Following command line
# parameter parsing, input information can be accepted based on internal
# priority assumptions.
#
# example usage:
#     ./script.sh 
#         non-option parameters:
#     ./script.sh  -a Dirac
#         -a triggered with parameter Dirac
#         non-option parameters: 
#     ./script.sh -a Dirac -b
#         -a triggered with parameter Dirac
#         -b triggered
#         non-option parameters: 
#     ./script.sh -a Dirac Feynman -b
#         -a triggered with parameter Dirac
#         -b triggered
#         non-option parameters: Feynman 
#     ./script.sh -a Dirac Feynman Born -b
#         -a triggered with parameter Dirac
#         -b triggered
#         non-option parameters: Feynman Born 
#     ./script.sh -a Dirac Feynman Born Born -b
#         -a triggered with parameter Dirac
#         -b triggered
#         non-option parameters: Feynman Born
#     ./script.sh -a Dirac Feynman Born Born -b
#         -a triggered with parameter Dirac
#         -b triggered
#         non-option parameters: Feynman Born Born
#
################################################################################

process_arguments(){
    OPTIND=1
    while getopts "${option_string}" option; do
        case ${option} in
            a)
                echo "-a triggered with parameter "${OPTARG}"" >&2 # output to STDERR
                ;;
            b)
                echo "-b triggered" >&2 # output to STDERR
                ;;
            \?)
                echo "invalid option: -"${OPTARG}"" >&2 # output to STDERR
            ;;
        esac
    done
    shift $(expr ${OPTIND} - 1)
    echo "${@}"
}

option_string=":a:b"
non_option_parameters=""
parameters="${@}"
while [ ! -z "${parameters}" ]; do
    parameters="$(process_arguments ${parameters})"
    non_option_parameters="${non_option_parameters} "$(awk -F '[ \t]*-' '{print $1}' <<< "${parameters}")
    parameters=$(awk -F '[ \t]*-' 'NF > 1{print substr($0, index($0, "-"))}' <<< "${parameters}")
done
echo "non-option parameters:${non_option_parameters}"

Upvotes: 2

Views: 1614

Answers (2)

glenn jackman
glenn jackman

Reputation: 246827

I didn't spend a lot of time reading your code. I agree with you: do your options processing at the script's "main level" so you can work directly with the script's position parameters.

Here's a rewrite:

#!/bin/bash

while getopts ":a:b" option; do
    case $option in
        a)
            echo "-a triggered with parameter $OPTARG" >&2
            ;;
        b)
            echo "-b triggered" >&2
            ;;
        :)
            echo "missing argument for option -${OPTARG}" >&2
            ;;
        \?)
            echo "invalid option: -${OPTARG}" >&2
            ;;
    esac
done
shift $(( OPTIND - 1 ))

echo "remaining arguments:"
for (( i=1; i<=$#; i++ )); do
    printf "%d: '%s'\n" $i "${!i}"
done

And in action:

$ bash ./opts.sh -a A -b B -c -d "foo bar" baz
-a triggered with parameter A
-b triggered
remaining arguments:
1: 'B'
2: '-c'
3: '-d'
4: 'foo bar'
5: 'baz'

You don't see any "invalid option" errors for "-c" and "-d" because the while getopts loop ended when it hit the first non-option argument "B".


Update: calling getopts multiple times:

extra_args=()

while (( $# > 0 )); do
    OPTIND=1
    while getopts ":a:b" option; do
        case $option in
            a) echo "-a triggered with parameter $OPTARG" >&2 ;;
            b) echo "-b triggered" >&2 # output to STDERR ;;
            :) echo "missing argument for option -$OPTARG" >&2 ;;
            ?) echo "invalid option: -$OPTARG" >&2 ;;
            esac
    done
    shift $(( OPTIND - 1 ))
    if (( $# > 0 )); then 
        extra_args+=( "$1" )
        shift
    fi
done

echo "non-option arguments:"
printf "%s\n" "${extra_args[@]}"
$ bash ~/tmp/opts.sh   -a A B -c -d "foo bar" baz -b
-a triggered with parameter A
invalid option: -c
invalid option: -d
-b triggered
non-option arguments:
B
foo bar
baz

Upvotes: 1

rici
rici

Reputation: 241741

The shiftless solution follows.

I use bash arrays to preserve positional (non-flag) parameters, because it's the only effective way to handle parameters with whitespace. In particular, your echo "${@}" effectively wipes flattens arguments, despite the quoting, because echo doesn't preserve quoting.

I also retain process_argument, although you could easily put it in the loop. It makes very little difference (and directly calling exit 1 rather than return 1 is arguably simpler.)

# process_argument option
# Do whatever needs to be done to handle provided option. Fail on error.
OPTIONS=:a:b
process_argument() {
  case "$1" in
    a)
        echo "-a triggered with parameter $OPTARG" >&2
        ;;
    b)
        echo "-b triggered" >&2 # output to STDERR
        ;;
    :)
        echo "missing argument for option -${OPTARG}" >&2
        return 1
        ;;
    \?)
        echo "invalid option: -${OPTARG}" >&2
        return 1
        ;;
  esac
}

# Loop to handle all parameters
non_option_parameters=()
while true; do
  while getopts "$OPTIONS" option; do
    if ! process_argument "$option"; then exit 1; fi
  done
  if ((OPTIND > $#)); then break; fi
  non_option_parameters+=(${!OPTIND})
  ((OPTIND++))
done

printf "Non-option parameters: "
printf "<%s> " "${non_option_parameters[@]}"; echo

Upvotes: 1

Related Questions