Differences between revisions 8 and 25 (spanning 17 versions)
Revision 8 as of 2009-12-07 02:22:02
Size: 3587
Editor: ozgw
Comment:
Revision 25 as of 2020-02-04 18:35:41
Size: 2454
Editor: GreyCat
Comment: *sigh* put the common, simple versions first.
Deletions are marked like this. Additions are marked like this.
Line 2: Line 2:
== How do I process options in a bash script? ==

For example, how do I code my bash script to accept a bunch of options like
== Common utility functions (warn, die) ==
(If you were looking for option processing, see [[BashFAQ/035]].) Bash and sh don't offer a `die` builtin command like Perl does, but it's common to use a `die` function in scripts. You just have to write one yourself. Most people who write a `die` function like to keep it simple. There are two common varieties: one that only takes a message to print, and one that takes a message and an exit status value.
Line 7: Line 6:
foobar -a --include something # Usage: die message ...
die() {
  printf '%s\n' "$*" >&2
  exit 1
}
Line 10: Line 13:
First up, there are some [[http://www.gnu.org/software/libtool/manual/libc/Argument-Syntax.html|GNU and POSIX standards]] for how to do this. {{{
# Usage: die exit_status message ...
die() {
  rc=$1
  shift
  printf '%s\n' "$*" >&2
  exit "$rc"
}
}}}
Line 12: Line 23:
=== do-it-yourself === Some people like fancier functions. If you like fancier functions, here are some variants:
Line 15: Line 26:
while "$1"; do
    case "$1" in
        -a|--all) ALL=yes ;shift ;;
        -i|--include) INCLUDE="$2"; shift; shift ;;
        *) echo "$PROG: Bad option '$1', exiting." >&2; exit 1;;
    esac
done
##
# warn: Print a message to stderr.
# Usage: warn "format" ["arguments"...]
#
warn() {
  local fmt=$1
  shift
  printf "script_name: $fmt\n" "$@" >&2
}

###
### The following three "die" functions
### depend on the above "warn" function.
###

##
# die (simple version): Print a message to stderr
# and exit with the exit status of the most recent
# command.
# Usage: some_command || die "message" ["arguments"...]
#
die () {
  local st=$?
  warn "$@"
  exit "$st"
}

##
# die (explicit status version): Print a message to
# stderr and exit with the exit status given.
# Usage: if blah; then die status_code "message" ["arguments"...]; fi
#
die() {
  local st=$1
  shift
  warn "$@"
  exit "$st"
}

##
# die (optional status version): Print a message to
# stderr and exit with either the given status or
# that of the most recent command.
# Usage: some_command || die [status code] "message" ["arguments"...]
#
die() {
  local st=$?
  if [[ $1 != *[^0-9]* ]]; then
    st=$1
    shift
  fi
  warn "$@"
  exit "$st"
}
Line 24: Line 82:
This is all well and good but it's crufty and it doesn't honour the standards - for example, how do you handle concatenation of single-letter options? What if the user writes {{{-isomething}}} without a space or {{{--include=something}}}? What about '--' or inter-mixing options and arguments? It's harder than it looks!!

=== getopt(1) ===
Line 29: Line 84:
TEMP=`getopt -o ai: --long all,include: -n 'example.bash' -- "$@"` ##
# warn: Print a message to stderr.
# Usage: warn "message"
#
warn() {
  printf '%s\n' "$@" >&2
}
Line 31: Line 92:
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi ##
# die (optional status version): Print a message to
# stderr and exit with either the given status or
# that of the most recent command.
# Usage: some_command || die "message" [status code]
# some_command && die "message" [status code]
Line 33: Line 99:
eval set -- "$TEMP"

while true ; do
    case "$1" in
        -a|--all) echo "Option a" ; shift ;;
        -i|--include) echo "Option i = $2"; shift; shift ;;
        .../etc
die() {
  local st=$?
  case $2 in
    *[^0-9]*|'') :;;
    *) st=$2;;
  esac
  warn "$1"
  exit "$st"
}
Line 42: Line 110:
This is better as it obeys the standards and gives the user a fairly predictable user-interface. There is still the disadvantage that options are coded in at least 2, probably 3 places - in the call to getopt(1), in the case statement that processes them and presumably in the help message that you are going to get around to writing one of these days. This is a classic opportunity for errors to creep in as the code is written and maintained - often not discovered till much, much later. You also need to remember to "double shift" options that accept values (eg {{{--include}}} above). Finally, getopt(1) provides no way to print help for the calling script.

=== process-getopt(1) ===

This relies on a downloadable script that you can install somewhere - in this case, it's in /usr/bin:

{{{
PROG=$(basename $0)
VERSION='1.2'
USAGE="A tiny example using process-getopt(1)"

# call process-getopt functions to define some options:
source /usr/bin/process-getopt

SLOT=""
SLOT_func() { [ "${1:-""}" ] && SLOT="yes"; } # callback for SLOT option
add_opt SLOT "boolean option" s "" slot

TOKEN=""
TOKEN_func() { [ "${1:-""}" ] && TOKEN="$2"; } # callback for TOKEN option
add_opt TOKEN "this option takes a value" t n token number

add_std_opts # define the standard options --help etc:

TEMP=$(call_getopt "$@") || exit 1
eval set -- "$TEMP" # just as with getopt(1)

# remove the options from the command line
process_opts "$@" || shift "$?"

echo "SLOT=$SLOT"
echo "TOKEN=$TOKEN"
echo "args=$@"
}}}

Here, all information about each option is defined in one place making for much easier authoring and maintainence. A lot of the dirty work is handled automatically and standards are obeyed as in getopt(1) - because it calls getopt for you. As an added bonus you get a nicely formatted help page (for {{{ -h, --help }}} and a starter for a man page (using an easter-egg option {{{ --print-man-page }}} ).

[[http://sourceforge.net/projects/process-getopt/ | process-getopt ]] at sourceforge

[[http://bhepple.freeshell.org/oddmuse/wiki.cgi/process-getopt | author's website ]]

Full disclosure: bhepple at freeshell dot org wrote this entry and is the author of process-getopt(1). Constructive criticism to that email address is most welcome!!
Since it's your script, you get to decide how fancy you want to make it.

Common utility functions (warn, die)

(If you were looking for option processing, see BashFAQ/035.) Bash and sh don't offer a die builtin command like Perl does, but it's common to use a die function in scripts. You just have to write one yourself. Most people who write a die function like to keep it simple. There are two common varieties: one that only takes a message to print, and one that takes a message and an exit status value.

# Usage: die message ...
die() {
  printf '%s\n' "$*" >&2
  exit 1
}

# Usage: die exit_status message ...
die() {
  rc=$1
  shift
  printf '%s\n' "$*" >&2
  exit "$rc"
}

Some people like fancier functions. If you like fancier functions, here are some variants:

##
# warn: Print a message to stderr.
# Usage: warn "format" ["arguments"...]
#
warn() {
  local fmt=$1
  shift
  printf "script_name: $fmt\n" "$@" >&2
}

###
### The following three "die" functions
### depend on the above "warn" function.
###

##
# die (simple version): Print a message to stderr
# and exit with the exit status of the most recent
# command.
# Usage: some_command || die "message" ["arguments"...]
#
die () {
  local st=$?
  warn "$@"
  exit "$st"
}

##
# die (explicit status version): Print a message to
# stderr and exit with the exit status given.
# Usage: if blah; then die status_code "message" ["arguments"...]; fi
#
die() {
  local st=$1
  shift
  warn "$@"
  exit "$st"
}

##
# die (optional status version): Print a message to
# stderr and exit with either the given status or
# that of the most recent command.
# Usage: some_command || die [status code] "message" ["arguments"...]
#
die() {
  local st=$?
  if [[ $1 != *[^0-9]* ]]; then
    st=$1
    shift
  fi
  warn "$@"
  exit "$st"
}

##
# warn: Print a message to stderr.
# Usage: warn "message"
#
warn() {
  printf '%s\n' "$@" >&2
}

##
# die (optional status version): Print a message to
# stderr and exit with either the given status or
# that of the most recent command.
# Usage: some_command || die "message" [status code]
#        some_command && die "message" [status code]

die() {
  local st=$?
  case $2 in
    *[^0-9]*|'') :;;
    *) st=$2;;
  esac
  warn "$1"
  exit "$st"
} 

Since it's your script, you get to decide how fancy you want to make it.


CategoryShell

BashFAQ/101 (last edited 2020-02-04 18:35:41 by GreyCat)