Skip to content

Instantly share code, notes, and snippets.

@jlinoff
Last active March 9, 2021 14:45
Show Gist options
  • Save jlinoff/1876972c0b37259c82367d51c8313171 to your computer and use it in GitHub Desktop.
Save jlinoff/1876972c0b37259c82367d51c8313171 to your computer and use it in GitHub Desktop.
Bash argument parser idiom that accepts short form, long form and long form = options.
#!/bin/bash
#
# Copyright (c) 2017 by Joe Linoff
# MIT Open Source License.
#
# This script shows how to implement an argument parser with
# 4 options. Two of the options are simply flags, one of
# of them has a single argument and the other has 2 arguments.
#
# It is meant to show bash can support reasonably complex
# argument parsing idioms that will make shell scripts
# more user friendly without using getopts. It is useful
# for cases where getopts is not available.
#
# The options demonstrated are:
#
# 1. -h or --help
# 2. -v or --verbose
# 3. -f ARG or --file ARG or --file=ARG
# 4. -c ARG1 ARG2 or --compare ARG1 ARG2
# 5. -l a,b,c,d or --list a,b,c,d
#
# The options parsing allows the following specifications.
#
# 1. -h
# 2. --help
# 3. -v
# 4. --verbose
# 5. -vv
# 6. -f ARG1
# 7. --file ARG1
# 8. --file=ARG1
# 9. -c ARG1 ARG2
# 10. --compare ARG1 ARG2
# 11. --list a,b,c,d
#
# This example does not show how to implement best match which would
# mean accepting an option like "--com" (because it is the best unique
# match to --compare). That could be added but i am not convinced
# that it is worth the overhead.
#
# The options parser is global in this example because it is setting
# global (script wide) variables.
# ========================================================================
# Functions
# ========================================================================
# Simple error function that prints the line number of the caller and
# highlights the message in red.
function _err() {
echo -e "\033[31;1mERROR:\033[0;31m:${BASH_LINENO[0]} $*\033[0m"
exit 1
}
# ========================================================================
# Main
# ========================================================================
CARGS=()
FILE=''
HELP=0
LIST=()
VERBOSE=0
# The OPT_CACHE is to cache short form options.
OPT_CACHE=()
while (( $# )) || (( ${#OPT_CACHE[@]} )) ; do
if (( ${#OPT_CACHE[@]} > 0 )) ; then
OPT="${OPT_CACHE[0]}"
if (( ${#OPT_CACHE[@]} > 1 )) ; then
OPT_CACHE=(${OPT_CACHE[@]:1})
else
OPT_CACHE=()
fi
else
OPT="$1"
shift
fi
case "$OPT" in
# Handle the case of multiple short arguments in a single
# string:
# -abc ==> -a -b -c
-[!-][a-zA-Z0-9\-_]*)
for (( i=1; i<${#OPT}; i++ )) ; do
# Note that the leading dash is added here.
CHAR=${OPT:$i:1}
OPT_CACHE+=("-$CHAR")
done
;;
-h|--help)
(( HELP++ ))
;;
-v|--verbose)
# Increase the verbosity.
# Can accept: -v -v OR -vv.
(( VERBOSE++ ))
;;
-l|--list|--list=*)
# Can be specified multiple times but we only accept the
# last one.
# Can accept: --file foo and --file=foo
if [ -z "${OPT##*=*}" ] ; then
ITEMS="${OPT#*=}"
else
ITEMS="$1"
shift
fi
[[ -z "$ITEMS" ]] && _err "Missing argument for '$OPT'."
LIST+=(${ITEMS//,/ })
;;
-f|--file|--file=*)
# Can be specified multiple times but we only accept the
# last one.
# Can accept: --file foo and --file=foo
if [ -z "${OPT##*=*}" ] ; then
FILE="${OPT#*=}"
else
FILE="$1"
shift
fi
[[ -z "$FILE" ]] && _err "Missing argument for '$OPT'."
;;
-c|--compare)
# Can be specified multiple times but we only accept the
# last one.
# Can accept:
# --compare ARG1 ARG2
# Cannot accept:
# --compare=*
# The reason for not accepting the '=' sign is to reduce
# complexity because of the ambiguity of separators. If
# you decide that you will always use a comma as the
# separator, that is fine until one of the arguments
# contains a comma.
CARG1="$1"
CARG2="$2"
shift 2
[[ -z "$CARG1" ]] && _err "Missing both arguments for '$OPT'."
[[ -z "$CARG2" ]] && _err "Missing second argument for '$OPT'."
CARGS=()
CARGS+=("$CARG1")
CARGS+=("$CARG2")
;;
-*)
_err "Unrecognized option '$OPT'."
;;
*)
_err "Unrecognized argument '$OPT'."
;;
esac
done
echo "COMPARE : ${CARGS[@]}"
echo "FILE : ${FILE}"
echo "HELP : ${HELP}"
echo "LIST : ${#LIST[@]} (${LIST[@]})"
echo "VERBOSE : ${VERBOSE}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment