Last active
March 9, 2021 14:45
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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