Simpler processing of shell script options

I’m looking for way to process shell script arguments that is cleaner and more “self documenting” than getopt/getopts.

It would need to provide…

  • Full support of long options with or without a value after ‘=’ or ‘ ‘(space).
  • Proper handling of hyphenated option names (i.e. –ignore-case)
  • Proper handling of quoted option values (i.e. –text “A text string”)

I would like to eliminate the overhead of the big loop with embedded case statement that getopt/getopts requires and reduce option processing to something like…

option=argumentparse "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="230763">[email protected]</a>"
[[ option == ""           ]] && helpShow
[[ option =~ -h|--help    ]] && helpShow
[[ option =~ -v|--version ]] && versionShow
[[ option =~ -G|--GUI     ]] && GUI=$TRUE
[[ option =~ --title      ]] && TITLE=${option["--title"]}

Here, an argumentparse() function resolves the various syntax possibilities into a consistent format, perhaps an associative array.

There must be something coded out there somewhere. Any ideas?

(updated and retitled)

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

Since this question has been viewed so much (for me at least) but no answers were submitted, passing on the solution adopted…

NOTE
Some functions, like the multi-interface output functions ifHelpShow() and uiShow() are used but not included here as their calls contain relevant information but their implementations do not.

###############################################################################
# FUNCTIONS (bash 4.1.0)
###############################################################################

function isOption () {
  # isOption "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="715531">[email protected]</a>"
  # Return true (0) if argument has 1 or more leading hyphens.
  # Example:
  #     isOption "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="133753">[email protected]</a>"  && ...
  # Note:
  #   Cannot use ifHelpShow() here since cannot distinguish 'isOption --help'
  #   from 'isOption "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d3f793">[email protected]</a>"' where first argument in "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="113551">[email protected]</a>" is '--help'
  # Revised:
  #     20140117 docsalvage
  # 
  # support both short and long options
  [[ "${1:0:1}" == "-" ]]  && return 0
  return 1
}

function optionArg () {
  ifHelpShow "$1" 'optionArg  --option "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="507410">[email protected]</a>"
    Echo argument to option if any. Within "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e2c6a2">[email protected]</a>", option and argument may be separated
    by space or "=". Quoted strings are preserved. If no argument, nothing echoed.
    Return true (0) if option is in argument list, whether an option-argument supplied
    or not. Return false (1) if option not in argument list. See also option().
    Examples:
        FILE=$(optionArg --file "$1")
        if $(optionArg -f "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="391d79">[email protected]</a>"); then ...
        optionArg --file "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="8fabcf">[email protected]</a>"   && ...
    Revised:
        20140117 docsalvage'  && return
  #
  # --option to find (without '=argument' if any)
  local FINDOPT="$1"; shift
  local OPTION=""
  local ARG=
  local o=
  local re="^$FINDOPT="
  #
  # echo "option start: FINDOPT=$FINDOPT, o=$o, OPTION=$OPTION, ARG=$ARG, @<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="81bca5c1">[email protected]</a>" >&2
  #
  # let "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="012541">[email protected]</a>" split commandline, respecting quoted strings
  for o in "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="163256">[email protected]</a>"
  do
    # echo "FINDOPT=$FINDOPT, o=$o, OPTION=$OPTION, ARG=$ARG" >&2
    # echo " o=$o"  >&2
    # echo "re=$re" >&2
    #
    # detect --option and handle --option=argument
    [[ $o =~ $re ]]  && { OPTION=$FINDOPT; ARG="${o/$FINDOPT=/}"; break; }
    #
    # $OPTION will be non-null if --option was detected in last pass through loop
    [[ ! $OPTION ]]  && [[ "$o" != $FINDOPT ]]   && {              continue; } # is a positional arg (no previous --option)
    [[ ! $OPTION ]]  && [[ "$o" == $FINDOPT ]]   && { OPTION="$o"; continue; } # is the arg to last --option
    [[   $OPTION ]]  &&   isOption "$o"          && {                 break; } # no more arguments
    [[   $OPTION ]]  && ! isOption "$o"          && { ARG="$o";       break; } # only allow 1 argument
  done
  #
  # echo "option  final: FINDOPT=$FINDOPT, o=$o, OPTION=$OPTION, ARG=$ARG, @<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="cbf6ef8b">[email protected]</a>" >&2
  #
  # use '-n' to remove any blank lines
  echo -n "$ARG"
  [[ "$OPTION" == "$FINDOPT" ]]   && return 0
  return 1
}

###############################################################################
# MAIN  (bash 4.1.0) (excerpt of relevant lines)
###############################################################################

# options
[[ "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="674327">[email protected]</a>" == ""            ]]   && { zimdialog --help           ; exit 0; }
[[ "$1" == "--help"      ]]   && { zimdialog --help           ; exit 0; }
[[ "$1" == "--version"   ]]   && { uiShow "version $VERSIONn"; exit 0; }

# options with arguments
TITLE="$(optionArg --title  "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dcf89c">[email protected]</a>")"
TIP="$(  optionArg --tip    "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="eaceaa">[email protected]</a>")"
FILE="$( optionArg --file   "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="91b5d1">[email protected]</a>")"

Method 2

There’s a great answer here but OP clearly asked for simpler processing of command line options. I think there can not be simpler way of parsing complex shell options than using getopt from util-linux:

$ cat opts.sh 
#!/bin/bash

# Print usage and exit
usage() {
    exec >&2 # Write everything to STDERR (consistent with getopt errors)
    (($#)) && echo "[email protected]"
    echo "Usage: $0 [opts...] [args...]"
    exit 1
}

# Use getopt to validate options and reorder them. On error print usage
OPTS=$(getopt -s bash -o 'ab:c' -l 'longopt,longopt2,longwitharg:' -- "[email protected]") || usage

# Replace our arguments with the reordered version
eval set -- "$OPTS"

# At this point everything up to "--" is options
declare opt_a opt_c longopt longopt2 longwitharg
declare -a opt_b  # Array to accumulate -b arguments
while (($#))
do
    case $1 in
        -a) opt_a=1;;
        -b) opt_b+=("$2"); shift;;
        -c) ((++opt_c));;
        --longopt) longopt=1;;
        --longopt2) ((++longopt2));;
        --longwitharg) longwitharg=$2; shift;;
        --) shift; break;; # We're done with options, shift over "--" and move on...
        *) usage "Unknown argument: $1" # Should not happen unless getopt errors are ignored.
    esac
    # Always shift once (for options with arguments we already shifted in the case so it's the 2nd shift)
    shift
done

echo "Remaining arguments after parsing options: $#"
# Now you can work directly with "[email protected]" or slurp it in an array
args=("[email protected]")

# Here's what we're left with:
declare -p opt_a opt_b opt_c longopt longopt2 longwitharg

# This is how you iterate over an array which may contain spaces and other field separators
for file in "${args[@]}"
do
    echo "File arg: $file"
done

getopt will do its validation and return error (unless you tell it not to, then you can catch them yourself). For example:

$ ./opts.sh --badopt
getopt: unrecognized option '--badopt'

Everything else is ordered and quoted properly:

$ ./opts.sh -ab3 -b8 file1 -ccc --longopt --longwitharg=abc --longwitharg "abc def" "file with spaces" 
Remaining arguments after parsing options: 2
declare -- opt_a="1"
declare -a opt_b=([0]="3" [1]="8")
declare -- opt_c="3"
declare -- longopt="1"
declare -- longopt2
declare -- longwitharg="abc def"
File arg: file1
File arg: file with spaces

The arguments here:

  • -a is a flag – set to 1 if present
  • -b and be repeated, each instance is added to the opt_b array
  • -c is a counter, counts the 3 occurrences
  • --longopt is a flag like -a
  • --longopt2 is a counter like -c (not used in this example, so unset; it will still be treated as 0 in Arithmetic Expansion).
  • --longwitharg is a normal option with argument. In this example we override its value later on the command line
  • file1 and file with spaces are the remaining arguments, and although they’re specified at random places they both end up at the end after -- once getopt reorders the command line.

Note that quoting variables is very important to properly handle arguments with spaces, although I’ve voluntarily avoided quotes where it’s not needed such as:

  • Direct variable assignment: longwitharg=$2
  • Switch case: case $1 in

If unsure, extra quotes generally won’t hurt.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x