------------------------------------------------------------------------------- Shell Script Option Handling ------------------------------------------------------------------------------- # List all options in quotes echo >&2 "Input Arguments:" "${0@Q}" "${@@Q}" # List all options (quote as needed) ( printf "Options: "; printf '%q ' "$@"; echo; ) >&2 ------------------------------------------------------------------------------- My General Option Handling Loop.... =======8<--------CUT HERE---------- #!/bin/bash # # script [options] args... # # The 'Usage()' and 'Help()' functions uses $PROGDIR to find this actual # script, then reads and output these comments (short usage or full set of # comments) as the documentation for this script. That is the script # commands and usage documention are in the same file, making it self # documenting, via options. # # This options loop can handle short and long options and just about ANY # unusual option such as numbers, 'geometry',etcetera. # # The only thing this does not handle is groups of single letter options! # Though there are solutions for that (see below) # # Options # -q|--quiet Be wery wery Quiet... We're hunting Wabbits # -d|--debug Debugging Run # -v+--verbose Verbose Output # -n Dry Run - Do Noth'n # --help This summery # --doc Extended documentation # ### # # Extended and Verbose Documentation and Examples # ##### # # Programmers only docs, version notes, and anything else that should be # recorded but is not part of normal user documentation. # ##### # Discover where the shell script resides (output above docs) PROGNAME="${BASH_SOURCE##*/}" # script name (basename) PROGDIR="${BASH_SOURCE%/*}" # directory (dirname - may be relative) # Fully qualify directory path (remove relative components and symlinks) #PROGDIR=$(cd "$PROGDIR" && pwd -P || echo "$PROGDIR") #ORIGDIR=$(pwd -P) # original directory #Debugging #printf "Running:"; printf " %q" "${@:0}"; printf '\n' Usage() { # Report Error and Synopsis line only echo >&2 "$PROGNAME:" "$@" sed >&2 -n '1,2d; /^###/q; /^#/!q; /^#$/q; s/^# */Usage: /p;' \ "$PROGDIR/$PROGNAME" echo >&2 "For help use $PROGNAME --help" exit 10; } Help() { # Output documentation sed >&2 -n '1d; /^###/q; /^#/!q; s/^#*//; s/^ //; p' \ "$PROGDIR/$PROGNAME" exit 10; } Doc() { # Output Extended documentation sed >&2 -n '1d; /^#####/q; /^#/!q; s/^#*//; s/^ //; p' \ "$PROGDIR/$PROGNAME" exit 10; } Error() { # Just output an error condition and exit (no usage) echo >&2 "$PROGNAME:" "$@" exit 2 } # Add short option pre-processor here (see below)? # minimal option handling while [ $# -gt 0 ]; do case "$1" in -\?|-help|--help) Help ;; # Standard help options. -doc|--doc) Doc ;; -q|--quiet) QUIET='quiet' ;; # be very very quiet, I'm hunting wabbit! -d|--debug) DEBUG='debug' ;; # Output programming debug reports -v|--verbose) VERBOSE='verbose' ;; # bang the drums, and blow the whistles -n|--name) shift; NAME="$1" ;; # provide a name argument (seperate) -) break ;; # output to stdout, end of options --) shift; break ;; # forced end of user options -*) Usage "Unknown option \"$1\"" ;; # unknown command line option *) break ;; # unforced end of user options esac shift # next option done (( $# > 0 )) && { ARG=$1; shift; } (( $# > 0 )) && Usage "Too Many Arguments" [ "$QUIET" ] || echo "Sing and Shout" [ -z "$QUIET" ] && echo "Normal output" [ "$DEBUG" ] && echo "$PROGNAME DEBUG: End of options" [ "$VERBOSE" ] && echo "I am hooting my horn and making a racket" [ "$NAME" ] && echo "Using name $NAME" =======8<--------CUT HERE---------- ------------------------------------------------------------------------------- Special Option Handling Examples (for above) =======8<-------- # Simple option and argument EG: -n name -n) shift; name="$1" ;; # Joined OR unjoined Argument EG: -Nname or -N name -N*) name="${1:2}"; [[ -z $name ]] && { shift; name="$1"; } ;; # Joined or unjoined number EG: -b123 -b 123 -b*) bits="${1:2}"; [[ -z $bits ]] && { shift; bits="$1"; } [[ $bits != *[^0-9]* ]] || Usage "Bits (-b) is not a integer" ;; # Number with type check EG: -{size} -[0-9]*) size="${1:2}" [[ $size =~ ^[^1-9][0-9]*$ ]] || Usage "Bad Number Option" Width=$size; Height=$size ;; # Coordinates EG: -c {x],{y} positive or negative -c) if [[ "$2" =~ ([-+]?[0-9]+),([-+]?[0-9]+) ]]; then CENTER_X="${BASH_REMATCH[1]}" CENTER_Y="${BASH_REMATCH[2]}" else Usage "Bad Geometry" fi ; shift ;; # Direct Geometry style rectangle argument (no checks) [0-9]*x[0-9]*) CROP="$1" ;; # WxH [+-][0-9]*[+-][0-9]*) CROP="$1" ;; # +X+Y [0-9]*x[0-9]*[+-][0-9]*[+-][0-9]*) CROP="$1" ;; # WxH+X+Y # Generalised Argument Save EG: -Gvalue or -G value => $opt_G -[a-zA-Z]*) # what flags are we looking for var="${1:1:1}" arg="${1:2}" [[ -z $arg ]] && { shift; arg="$1"; } read opt_$var <<< "$arg" # may loose newline in argument ;; # Generalized Long Option (with argument, '=' optional) => $opt_long # EG: --style long --name=JohnDoe --?* ) var="${1:2}" var="${var%%=*} arg=${1#*=} [[ -z $arg ]] && { shift; arg="$1"; } # space seperated read opt_$var <<< "$arg" ;; # --- # Post option handling... # Argument Count Check (must have 2 args) [ $# -lt 2 ] && Usage "Too Few Arguments" [ $# -gt 2 ] && Usage "Too Many Arguments" # Handle a file input argument... FILE="${1:-/dev/stdin}" [ ! -f $FILE ] && Error "Input file \"$FILE\": does not exist" [ ! -r $FILE ] && Error "Input file \"$FILE\": is not readable" # URL argument check URL="$2" [[ "$URL" =~ https?://.* ]] || Error "Required URL argument missing" # set HTTP_CMD to use... # The := will set variable, if not already set # Note this may then be set using a environment variable. cmd_found curl && : ${HTTP_CMD:=curl} cmd_found wget && : ${HTTP_CMD:=wget} cmd_found lynx && : ${HTTP_CMD:=lynx} # debug echo function if [ "$DEBUG" ]; then debug() { echo >&2 "$@"; } else debug() { :; } fi debug "WARNING: Debugging has been enabled" =======8<-------- ------------------------------------------------------------------------------- Alternative Option handling... See https://stackoverflow.com/questions/192249/ --- Use of a pre-processor to 'fix' options to a more regular form... EG: map "--name=value" to "--name" "value", and "-xyz" to "-x" "-y" "-z" WARNING: This does not work for options like -oOPTION or -123 Also does not handle tar-like options. EG: -bf 100 file => -b 100 -f file # Pre-process options (seperate -xyz => -x -y -z) ARGV=() while [[ $# -gt 0 ]]; do arg="$1"; shift case "${arg}" in --) ARGV+=("$arg"); break; ;; # end of options --*=*) ARGV+=("${arg%%=*}" "${arg#*=}") ;; # long option (with =) --*) ARGV+=("$arg" "$2"); shift ;; # long option (sep args) -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;; *) ARGV+=("$arg") ;; # non-option (before '--') esac done set -- "${ARGV[@]}" "$@" # redefine options to program #( printf "Processed opts: "; printf '%q ' "$@"; echo; ) >&2 # Now parse options as before... =======8<-------- --- "getopt" can be used as a pre-processor to simplify arguments. But the command is not recommended. =======8<-------- !/bin/bash # Allow a command to fail with !’s side effect on errexit # use return value from ${PIPESTATUS[0]}, because ! hosed $? getopt --test > /dev/null if (( $? != 4 )); then echo 'I’m sorry, "getopt" is not available.' exit 1 fi # option --output/-o requires 1 argument LONGOPTS=debug,force,output:,verbose OPTIONS=dfo:v # temporarily store output to be able to check for errors PARSED=$( getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@" ) if [[ ${PIPESTATUS[0]} -ne 0 ]]; then # getopt has complained about wrong arguments to stdout exit 2 fi # Now use getopt output as the arguments. eval set -- "$PARSED" # Normal option processing here =======8<-------- --- "getopts" is a bash builtin, (there is also a seperate GNU verion) =======8<-------- OPTIND=1 # Reset in case getopts has been used previously in the shell. OPTERR=0 # don't have getopts report errors while getopts "h?vf:" opt; do case "$opt" in h|\?) show_help; exit 0 ;; v) verbose=1 ;; f) output_file="$OPTARG" ;; *) echo >&2 "Unknown option $opt"; exit 10 ;; esac done shift $((OPTIND-1)) [ "${1:-}" = "--" ] && shift =======8<-------- ------------------------------------------------------------------------------- Dependency Program Testing # What secondary program does this script rely on... DEPENDENCIES="sed awk grep egrep tr bc magick" # Check Dependencies a script requires is available for i in $DEPENDENCIES; do type $i >/dev/null 2>&1 || Usage "Required program dependency \"$i\" missing" done ------------------------------------------------------------------------------- Positional Parameter Argument Handling... If you care for the possibility that there aren't any arguments. "$@" -> "" (one empty argument) ${1+"$@"} -> nothing at all This is not a problem with newer modern shells (bash, zsh, ksh), only very old Bourne shells. You can index command line arguments using substring expansion. set -$- a b c d e f Note @ isn't a full bash array echo "$3" c echo "${@[3]}" -bash: ${@[3]}: bad substitution But you can 'slice' it as an array... echo "${@:3:1}" # Get argment 3 i=3; echo "${@:$i:1}" # Get argment 3 indirectly by index echo "${@:$i}" # Get argment 3 and later echo "${@: -1}" # Get the last argument (NOTE THE SPACE!) echo "${@:(-1)}" # get the last argument alternative echo "${@:${#@}}" # get last argument alternative echo "${@:1:($#-1)}' # All but the last argument set -$- "${@:1:$(($#-1))}" # remove the last argument from argument list ARGS=( "$@" ) # Convert arguments into a full bash array printf "'%s' " "${@:0}" # If a offset of 0 is given "$0" is included # EG: output the full command and arguments Indexing (using a indirect referance variable) set -- a b c d e f OPTIND=3 echo "index 3 = ${!OPTIND}" # => index 3 = c This is different to 'arrays' (see later) for historical reasons. But slices can be used with arrays -- see below. -------------------------------------------------------------------------------