neechbear/blip

View on GitHub
blip.bash.in

Summary

Maintainability
Test Coverage
#!/bin/bash
#
# blip - Bash Library for Indolent Programmers
#
# Please see the man page blip.bash(3) or bash.bash.3.md for full documentation.
#
# This library is written for, and requires the bash shell. It is not expected
# to, nor intended to work with the bourne shell or any other shell. Great care
# has been taken to use internal built-in functions instead of forking external
# commands wherever possible, in order to offer the best performance possible.
#
# https://nicolaw.uk/blip
# https://github.com/neechbear/blip/
# https://github.com/neechbear/blip/blob/master/blip.bash.3.md
#
# MIT License
#
# Copyright (c) 2016, 2017, 2018 Nicola Worthington <nicolaw@tfb.net>.
# With contributions from Sergej Alikov <sergej.alikov@gmail.com>, 2016.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Command line argument parsing functionality within this library is is a forked
# adaptation of the cmdarg.sh library (https://github.com/akesterson/cmdarg),
# which is written by and
# Copyright (c) 2013 Andrew Kesterson <andrew@aklabs.net>
# See LICENSE and CONTRIBUTORS for details.
#
# Some other inspirational sources:
#   https://nicolaw.uk/bash
#   http://mywiki.wooledge.org/BashFAQ
#   https://code.google.com/archive/p/bsfl/downloads
#   https://bash.cyberciti.biz/guide/Shell_functions_library
#   http://www.bashinator.org/
#   https://dberkholz.com/2011/04/07/bash-shell-scripting-libraries/
#   https://github.com/Dieterbe/libui-sh/blob/master/libui.sh
#
# Get a nice list of bash built-ins without forking crap for formatting:
#     while read -r _ cmd ; do echo $cmd ; done < <(enable -a)
#
# Preferred function naming conventions:
#   is_*    An evaluation test that returns boolean true or false only.
#           No STDOUT should be emitted.
#
#   to_*    Data manipulation that returns results through STDOUT.
#
#   get_*   Gathers some information which is returned through STDOUT.
#           Newline characters should be omitted from output when only
#           a single line of output is ever expected.

# Action to take on fatal conditions. Test is written in old bourne compatible
# syntax to account for old limited and buggy conditionals.
if [ "x${BLIP_INTERNAL_FATAL_ACTION:-}" = "x" ] ; then
  BLIP_INTERNAL_FATAL_ACTION="exit 2"
fi

# Try and bail out early if we detect that we are probably not running
# from inside a bash shell interpreter. You may disable the exit on
# non-Bash shell functionality by setting BLIP_ALLOW_FOREIGN_SHELLS=1.
if [ "x$BASH" = "x" ] || [ "x$BASH_VERSION" = "x" ] ; then
  case "x$BLIP_ALLOW_FOREIGN_SHELLS" in
    x1|xyes|xtrue|xon|xenable|xenabled) true ;;
    *)
      echo "blip.bash detected a foreign shell interpreter is running;" \
           "exiting!" >&2
      $BLIP_INTERNAL_FATAL_ACTION
  esac
fi

# Bomb out if we're being run on Bash version 3 or older.
if (( BASH_VERSINFO[0] < 4 )); then
  echo "blip.bash is designed to work with Bash version 4 or newer;" \
       "exiting!" >&2
  $BLIP_INTERNAL_FATAL_ACTION
fi

# Set variable for convenience variable substitution conditional logic.
declare -x __BLIP_BASH_V4=""
declare -x __BLIP_BASH_V42=""
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]] ; then
  __BLIP_BASH_V4=true
  if [[ "${BASH_VERSINFO[2]}" -ge 2 ]] ; then
    __BLIP_BASH_V42=true
  fi
fi

if [[ -z "${BLIP_VERSION:+defined}" ]] ; then
  declare ${__BLIP_BASH_V42:+-g} -a BLIP_VERSINFO=("@VERSION_MAJOR@" "@VERSION_MINOR@" "@VERSION_PATCH@" "@VERSION_TAG@")
  declare ${__BLIP_BASH_V42:+-g} BLIP_VERSION="${BLIP_VERSINFO[0]}.${BLIP_VERSINFO[1]}.${BLIP_VERSINFO[2]}-${BLIP_VERSINFO[3]}"
else
  echo "blip.bash version $BLIP_VERSION is already loaded." >&2
  if ! [[ "$BLIP_VERSION" = "@VERSION_MAJOR@.@VERSION_MINOR@.@VERSION_PATCH@-@VERSION_TAG@" ]] ; then
    echo "Reloading conflicting versions of blip.bash over each" \
        "other may result in unpredictable behaviour!" >&2
  fi
fi

# Evaulates if single argument input is an integer.
is_int () { [[ "${1:-}" =~ ^-?[0-9]+$ ]]; }
is_integer () { is_int "$@"; }

# This is only used internally if you want to debug something. It allows
# printing of some useful messages in the more complex functions like
# trap handlers.
declare ${__BLIP_BASH_V42:+-g} -i -x BLIP_DEBUG_LOGLEVEL=${BLIP_DEBUG_LOGLEVEL:-0}
if ! is_int "$BLIP_DEBUG_LOGLEVEL" ; then
  BLIP_DEBUG_LOGLEVEL=0
fi

if [[ -n "${BLIP_REQUIRE_VERSION:-}" ]] ; then
  declare -ax BLIP_REQUIRE_VERSINFO=(${BLIP_REQUIRE_VERSION//[-.]/ })
  if   [[ ${BLIP_REQUIRE_VERSINFO[0]:-} -gt ${BLIP_VERSINFO[0]} ]] \
    || [[ ${BLIP_REQUIRE_VERSINFO[1]:-} -gt ${BLIP_VERSINFO[1]} ]] \
    || [[ ${BLIP_REQUIRE_VERSINFO[2]:-} -gt ${BLIP_VERSINFO[2]} ]] ; then
    echo "blip.bash version $BLIP_VERSION does not satisfy minimum" \
         "required version $BLIP_REQUIRE_VERSION; exiting!" >&2
    $BLIP_INTERNAL_FATAL_ACTION
  fi
  unset BLIP_REQUIRE_VERSINFO
fi

# Assign command names to run from $PATH unless otherwise already defined.
declare ${__BLIP_BASH_V42:+-g} -x BLIP_EXTERNAL_CMD_FLOCK="${BLIP_EXTERNAL_CMD_FLOCK:-flock}"
declare ${__BLIP_BASH_V42:+-g} -x BLIP_EXTERNAL_CMD_STAT="${BLIP_EXTERNAL_CMD_STAT:-stat}"
declare ${__BLIP_BASH_V42:+-g} -x BLIP_EXTERNAL_CMD_BC="${BLIP_EXTERNAL_CMD_BC:-bc}"
declare ${__BLIP_BASH_V42:+-g} -x BLIP_EXTERNAL_CMD_CURL="${BLIP_EXTERNAL_CMD_CURL:-curl}"
declare ${__BLIP_BASH_V42:+-g} -x BLIP_EXTERNAL_CMD_DATE="${BLIP_EXTERNAL_CMD_DATE:-date}"
declare ${__BLIP_BASH_V42:+-g} -x BLIP_EXTERNAL_CMD_GREP="${BLIP_EXTERNAL_CMD_GREP:-grep}"

# TOOD(nicolaw): Decide if individually exported environment variables are best
#                or if an associative array is more convenient. The associative
#                array is certianly cleaner, but it cannot be exported to sub-
#                shells, which makes it less flexible.
#while read -r _ _blip_vtype _blip_vname ; do
#    if [[ ! "$_blip_vtype" =~ A ]] && \
#       [[ "$_blip_vname" = "BLIP_EXTERNAL_CMD" ]] ; then
#        unset BLIP_EXTERNAL_CMD
#    fi
#done < <(typeset -p BLIP_EXTERNAL_CMD 2>/dev/null||:)
#declare -p BLIP_EXTERNAL_CMD >/dev/null 2>&1 || declare -Agx BLIP_EXTERNAL_CMD=()
#for _ in flock stat bc curl date grep ; do
#    BLIP_EXTERNAL_CMD[$_]="$_"
#done

# Trap handler stack.
declare ${__BLIP_BASH_V42:+-g} -x -a BLIP_TRAP_STACK=()
declare ${__BLIP_BASH_V42:+-g} -x -A BLIP_TRAP_MAP=() # Maps BLIP_TRAP_STACK indexes to signals

# The following may or may not offer a better solution. I should read it in
# detail to find out if I should rewrite what I've already done or not. At
# first glance it looks concise, but there's a fair few evals and it doesn't
# look set -u friendly.
# Either way, I like the idea of being able to prepend as well as just being
# able to append (push) handlers on and off the stack. I need to implement
# that functionality too! I may need to rethink the names of my functions
# though.
# http://stackoverflow.com/questions/16115144/bash-save-and-restore-trap-state-easy-way-to-manage-multiple-handlers-for-trap

append_trap () {
  declare action="${1:-}"; shift
  [[ -z "$action" ]] && return
  declare sig
  for sig in "$@" ; do
    trap -- "$(
          _get_existing_action() { printf "%s${3+\n}" "${3:-}"; }
          eval "_get_existing_action $(trap -p "$sig")"
          printf '%s\n' "$action"
        )" "$sig"
  done
}
declare -ft append_trap

execute_trap_stack () {
  declare sig
  for sig in "$@" ; do
    if [[ -n "${BLIP_TRAP_MAP[$sig]:-}" ]] ; then
      declare -i idx
      for idx in ${BLIP_TRAP_MAP[$sig]} ; do
        eval "${BLIP_TRAP_STACK[$idx]}"
      done
    fi
  done
}

push_trap_stack () {
  declare action="${1:-}"; shift
  [[ -z "$action" ]] && return

  declare sig
  for sig in "$@" ; do
    declare -i idx="${#BLIP_TRAP_STACK[@]}"
    declare -i i
    for ((i = 0; i < ${#BLIP_TRAP_STACK[@]}; i++)) ; do
      if [[ -z "${BLIP_TRAP_STACK[$i]:-}" ]] ; then
        idx=$i
        break
      fi
    done

    BLIP_TRAP_STACK[$idx]=$action
    if [[ -n "${BLIP_TRAP_MAP[$sig]:+defined}" ]] ; then
      BLIP_TRAP_MAP[$sig]+=" $idx"
    else
      BLIP_TRAP_MAP[$sig]="$idx"
      if ! [[ "$(trap -p "$sig")" =~ execute_trap_stack\ $sig ]] ; then
        append_trap "execute_trap_stack $sig" "$sig"
      fi
    fi

    if [[ $BLIP_DEBUG_LOGLEVEL -ge 1 ]] ; then
      for ((i = 0; i < ${#BLIP_TRAP_STACK[@]}; i++)) ; do
        echo "\$BLIP_TRAP_STACK[$i]=${BLIP_TRAP_STACK[$i]:-}"
      done
      echo "\$BLIP_TRAP_MAP[$sig]=${BLIP_TRAP_MAP[$sig]:-}"
    fi
    if [[ $BLIP_DEBUG_LOGLEVEL -ge 3 ]] ; then
      trap -p "$sig" || true
    fi
  done
}

pop_trap_stack () {
  declare sig
  for sig in "$@" ; do
    if [[ -n "${BLIP_TRAP_MAP[$sig]:-}" ]] ; then
      declare -a map=(${BLIP_TRAP_MAP[$sig]})
      declare -i idx=${map[-1]}
      BLIP_TRAP_STACK[$idx]=""
      unset map[${#map[@]}-1]
      BLIP_TRAP_MAP[$sig]="${map[*]:-}"
    fi

    if [[ $BLIP_DEBUG_LOGLEVEL -ge 1 ]] ; then
      echo "\$BLIP_TRAP_MAP[$sig]=${BLIP_TRAP_MAP[$sig]:-}"
      declare -i i
      for ((i = 0; i < ${#BLIP_TRAP_STACK[@]}; i++)) ; do
        echo "\$BLIP_TRAP_STACK[$i]=${BLIP_TRAP_STACK[$i]:-}"
      done
    fi
    if [[ $BLIP_DEBUG_LOGLEVEL -ge 3 ]] ; then
      trap -p "$sig" || true
    fi
  done
}

set_trap_stack () {
  declare action="${1:-}"; shift
  [[ -z "$action" ]] && return
  declare sig
  for sig in "$@" ; do
    unset_trap_stack "$sig"
    push_trap_stack "$action" "$sig"
  done
}

unset_trap_stack () {
  declare sig
  for sig in "$@" ; do
    if [[ -n "${BLIP_TRAP_MAP[$sig]:-}" ]] ; then
      declare -i idx
      for idx in ${BLIP_TRAP_MAP[$sig]} ; do
        BLIP_TRAP_STACK[$idx]=""
      done
      unset BLIP_TRAP_MAP["$sig"]
    fi
  done
}

get_trap_stack () {
  declare sig
  for sig in "$@" ; do
    if [[ -n "${BLIP_TRAP_MAP[$sig]:-}" ]] ; then
      declare -i i
      for i in ${BLIP_TRAP_MAP[$sig]} ; do
        if [[ -n "${BLIP_TRAP_STACK[$i]:-}" ]] ; then
          printf '%s\n' "${BLIP_TRAP_STACK[$i]:-}"
        fi
      done
    fi
    return 0
  done
}

get_variable_type () {
  declare vtype=""; vtype="$(declare -p "$1" 2>/dev/null)"
  vtype="${vtype#* -}"
  printf '%s' "${vtype%% *}"
}

as_json_value () {
  if [[ ! -n "${1+defined}" ]] ; then
    # null.
    printf 'null'
  elif [[ "$1" = "true" || "$1" = "false" ]] ; then
    # Boolean.
    printf '%s' "$1"
  elif [[ "$1" =~ ^-?([1-9][0-9]*|0)(\.[0-9]+)?$ ]] ; then
    # TODO(nicolaw): Allow support for exponential E notation.
    # Number.
    printf '%s' "$1"
  else
    # String.
    printf '"%s"' "$(as_json_string "$1")"
  fi
}

as_json_string () {
  declare str="$1"
  # shellcheck disable=SC1003
  declare -a shell=(        '\'  '"'  $'\b' $'\f' $'\n' $'\r' $'\t' )
  # shellcheck disable=SC1003
  declare -a json_escaped=( '\\' '\"'  '\b'  '\f'  '\n'  '\r'  '\t' )
  declare -i i
  for i in "${!shell[@]}"; do
    str="${str//"${shell[${i}]}"/${json_escaped[${i}]}}"
  done
  printf '%s' "$str"
}

# FIXME: This doesn't work for associative arrays yet, and is still a little
#        sketchy with regular arrays.
# vars_as_json $(compgen -v BASH)
vars_as_json () {
  declare format='"%s": %s'
  printf '{'
  while [[ $# -ge 1 ]] ; do
    declare vtype=""; vtype="$(get_variable_type "$1")"

    if [[ $vtype == *"a"* ]] ; then
      # Array.
      declare tmp_indirection="${1}[@]"
      declare -a tmp_array=( "${!tmp_indirection}" )
      # shellcheck disable=SC2059
      printf "$format" "$1" '['
      declare -i i
      for i in "${!tmp_array[@]}" ; do
        # TODO: Double check that this is a valid thing to disable here.
        #       I think we want to preserve null values over empty strings, so
        #       this is actually what we want.
        # shellcheck disable=2086
        printf '%s' "$(as_json_value ${tmp_array[$i]+"${tmp_array[$i]}"})"
        if [[ $i -lt ${#tmp_array} ]] ; then
          printf ', '
        fi
      done
      printf ']'

    elif [[ $vtype == *"A"* ]] ; then
      # Associative array / object.
      declare tmp_indirection=""; tmp_indirection="$(declare -p "$1")"
      unset __blip_tmp_dict
      # Even though this declare works exactly as-is on the command line, even
      # including all the indirection, it doesn't appear to properly work here.
      # We appear to be able to get the values out of the dict, but not the
      # keys, and not print it's declare statement etc.
      eval "declare -A __blip_tmp_dict=${tmp_indirection#*=}"
      if [[ $BLIP_DEBUG_LOGLEVEL -ge 3 ]] ; then
        declare -p "__blip_tmp_dict" >&2 || :
        # We define this through an eval statement above, so shellcheck cannot
        # detect that is it defined properly.
        # shellcheck disable=SC2154
        echo "__blip_tmp_dict keys=${!__blip_tmp_dict[*]}" >&2
        echo "__blip_tmp_dict values=${__blip_tmp_dict[*]}" >&2
      fi
      # shellcheck disable=SC2059
      printf "$format" "$1" '{'
      declare -i i=0
      declare k
      for k in "${!__blip_tmp_dict[@]}" ; do
        # shellcheck disable=SC2059,SC2086
        printf "$format" "$i" "$(as_json_value ${__blip_tmp_dict[$i]+"${__blip_tmp_dict[$i]}"})"
        if [[ $k -lt ${#__blip_tmp_dict[@]} ]] ; then
          printf ', '
        fi
        let i++
      done
      printf '}'
      unset __blip_tmp_dict

    else
      # Number, string, boolean, null.
      # shellcheck disable=SC2059,SC2086
      printf "$format" "$1" "$(as_json_value ${!1:+"${!1}"})"
    fi

    if [[ $# -gt 1 ]] ; then
      printf ', '
    fi
    shift
  done
  printf '}\n'
}

is_newer_version () {
  declare lhs_version="${1:-}"
  declare rhs_version="${2:-}"
  declare -a lhs=( ${lhs_version//./ } )
  declare -a rhs=( ${rhs_version//./ } )
  declare -i i
  for i in "${!lhs[@]}" ; do
    if ! [[ ${rhs[$i]:-} -ge ${lhs[$i]} ]] ; then
      return 1
    fi
  done
  return 0
}

required_command_version () {
  declare command="$1"
  declare check_version="$2"
  declare version_command="${3:-${command} --version}"
  is_newer_version "$check_version" "$($version_command 2>&1 | grep -Eow '[0-9]+\.[0-9\.]+' | tail -n1)"
}

get_pid_lock_filename () {
  declare lock_path="${1:-}"
  declare base_name="${2:-$0}"
  declare tmp_dir="${TMPDIR:-/tmp}"

  if [[ -z "$lock_path" ]] ; then
    if [[ -w /var/run ]] ; then
      lock_path="/var/run"
    elif [[ -n "${tmp_dir:-}" ]] && [[ -w "$tmp_dir" ]] ; then
      lock_path="$tmp_dir"
    else
      lock_path="${PWD:-./}"
    fi
  fi

  base_name="${base_name##*/}"
  if [[ "$base_name" =~ ([a-zA-Z0-9][a-zA-Z0-9_-]*) ]] ; then
    echo -n "${lock_path%/}/${BASH_REMATCH[1]}.pid"
  else
    echo -n "${lock_path%/}/${base_name}.pid"
  fi
}

get_exclusive_execution_lock () {
  declare pid_file="${1:get_pid_lock_filename}"
  # Use prefered flock mechanism (probably under Linux).
  if is_in_path "$BLIP_EXTERNAL_CMD_FLOCK" ; then
    : "${pid_file}"
  # Otherwise make do with mkdir method.
  else
    : "${pid_file}"
  fi
}

read_config_file () {
  declare config_file="${1:-}"; shift
  _safe_read_vars () {
    declare input_file="$1"; shift
    (
      source "$input_file" || :
      declare var=""
      for var in "$@" ; do
        printf '%q' "${var}=${!var:-}"
        echo
      done
    ) || :
  }
  while read -r line ; do
    if [[ -z "${__BLIP_BASH_V42:-}" ]] ; then
      export "$(eval "echo ${line}")" || :
    else
      declare -g "$(eval "echo ${line}")" || :
    fi
  done < <(_safe_read_vars "$config_file" "$@")
}

# Return the length of the longest argument.
get_max_length () {
  declare -i max=0
  for arg in "$@" ; do
    if [[ ${#arg} -gt $max ]] ; then
      max="${#arg}"
    fi
  done
  echo -n "$max"
}

trim () {
  declare string="${1:-}"
  string="${string#"${string%%[![:space:]]*}"}"
  string="${string%"${string##*[![:space:]]}"}"
  echo -n "$string"
}

get_string_characters () {
  declare string="${1:-}"
  declare -i i
  for (( i=0; i<${#string}; i++ )); do
    echo "${string:$i:1}"
  done
}

# Functionality to add:
#    - Add get_user_input() - multi character user input without defaults
#    - Add process locking functions
#    - Add background daemonisation functions (ewww - ppl should use systemd)
#    - Add standard logging functions
#    - Add syslogging functionality of all process STDOUT + STDERR
#    - Add console colour output options

# Ask the user for confirmation, expecting a single character y or n reponse.
# Returns 0 when selecting y, 1 when selecting n.
get_user_confirmation () {
  declare question="${1:-Are you sure?}"
  declare default_response="${2:-}"
  get_user_selection "$question" "$default_response" "y" "n"
}

# See also: bash's "select" built-in.
get_user_selection () {
  declare question="${1:-Make a selection }"; shift
  declare default_response="${1:-}"; shift
  declare -i max_response_length; max_response_length="$(get_max_length "$@")"

  # Replace with a standard argument validation routine.
  # http://tldp.org/LDP/abs/html/exitcodes.html
  if [[ $max_response_length -ne 1 ]] ; then
    >&2 echo "get_user_selection() <question_prompt> <default_response> <valid_responseN>..."
    >&2 echo "No valid_reponse arguments were passed, or 1 or more valid_response arguments were not exactly 1 character in length."
    return 126
  fi

  declare prompt=""
  declare arg
  for arg in "$@" ; do
    if [[ "$arg" = "$default_response" ]] ; then
      arg="*$arg"
    fi
    prompt="${prompt:+$prompt|}$arg"
  done

  declare input=""
  while read -n 1 -e -r -p "${question}${prompt:+ [$prompt]: }" input ; do
    if [[ -z "$input" ]] ; then
      input="$default_response"
    fi

    declare -i rc=0
    for valid_response in "$@" ; do
      if [[ "$input" = "$valid_response" ]] ; then
        return $rc
      fi
      rc=$((rc+1))
    done
  done
}

# Store the time that bash started (or a close enough aproximation assuming
# that nobody has modified $SECONDS if we're using an older version of bash).
if ! [[ -n "${BLIP_START_UNIXTIME+defined}" ]] ; then
  declare ${__BLIP_BASH_V42:+-g} -x -i BLIP_START_UNIXTIME=""
  if [[ ${BASH_VERSINFO[0]} -ge 4 && ${BASH_VERSINFO[1]} -ge 2 ]] ; then
    BLIP_START_UNIXTIME="$(printf "%(%s)T" -2)"
  else
    BLIP_START_UNIXTIME="$(( $(date +"%s") - SECONDS ))"
  fi
fi

# https://en.wikipedia.org/wiki/ISO_8601
get_iso8601_date () { get_date "%Y-%m-%d" "$@"; }

# Return the time since the epoch in seconds.
get_unixtime () { get_date "%s" "$@"; }

# This is pretty pointless (just use $SECONDS right?).
#if [[ ${BASH_VERSINFO[0]} -ge 4 && ${BASH_VERSINFO[1]} -ge 2 ]] ; then
#get_runtime_seconds () {
#    echo -n $(( $(get_unixtime -1) - $(get_unixtime -2) ))
#}
#fi

get_date () {
  declare format="${1:-%a %b %d %H:%M:%S %Z %Y}"
  declare when="${2:--1}"
  if [[ ${BASH_VERSINFO[0]} -ge 4 && ${BASH_VERSINFO[1]} -ge 2 ]] ; then
    printf "%($format)T\n" "$when"
  else
    if [[ "$when" = "-1" ]] ; then
      when=""
    elif [[ "$when" = "-2" ]] ; then
      when="@${BLIP_START_UNIXTIME}"
    fi
    $BLIP_EXTERNAL_CMD_DATE ${when:+-d "$when"} +%s
  fi
}

url_http_header () {
  $BLIP_EXTERNAL_CMD_CURL -k -L -s -I "$1"
}

# 200 OK
# Returns "200"
url_http_response_code () {
  declare url="$1"
  declare response=""; response="$(url_http_response "$url")"
  if [[ "$response" =~ ([0-9]+) ]] ; then
    echo -n "${BASH_REMATCH[1]}"
  fi
}

# HTTP/1.1 200 OK
# Returns "200 OK"
url_http_response () {
  declare url="$1"
  declare header=""
  declare response=""
  while read -r header ; do
    if [[ "$header" =~ ^HTTP(/[0-9]*\.?[0-9]+)?\ +([[:print:]]+) ]] ; then
      response="${BASH_REMATCH[2]}"
    fi
  done < <(url_http_header "$url")
  [[ -n "$response" ]] && echo -n "$response"
}

# TODO(nicolaw): Make less broken; what about non-http:// and file:// URLs?
url_exists () {
  declare url="$1"
  if [[ "$url" =~ ^file:// ]] ; then
    $BLIP_EXTERNAL_CMD_CURL -k -s -L -I "$url" -o /dev/null 2>/dev/null
    return $?
  else
    declare response=""; response="$(url_http_response_code "$url")"
    if   is_int "$response" \
      && [[ $response -ge 200 ]] \
      && [[ $response -lt 300 ]] ; then
        return 0
      fi
  fi
  return 1
}

is_in_path () {
  declare cmd
  for cmd in "$@" ; do
    if ! type -P "$cmd" >/dev/null 2>&1 ; then
       return 1
    fi
  done
  return 0
}

# MAC-48 and EUI-48 are syntactically indistinguishable, so for the sake of
# consistency this is named is_eui48_address to match the eui64 function.
is_eui48_address () {
  declare addr="${1:-}"
  addr="${addr,,}"
  if   [[ $addr =~ ^[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}$ ]] ; then
    return 0
  elif [[ $addr =~ ^[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}$ ]] ; then
    return 0
  elif [[ $addr =~ ^[a-f0-9]{4}\.[a-f0-9]{4}\.[a-f0-9]{4}$ ]] ; then
    return 0
  fi
  return 1
}

is_eui64_address () {
  declare addr="${1:-}"
  addr="${addr,,}"
  if   [[ $addr =~ ^[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}$ ]] ; then
    return 0
  elif [[ $addr =~ ^[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}-[a-f0-9]{2}$ ]] ; then
    return 0
  fi
  return 1
}

# IEEE 802 standard format for MAC-48 and EUI-48  addresses in most
# common human friendly transmission order.
is_mac_address () { is_eui48_address "$@"; }

# TODO(nicolaw): Try to get this working using bash's own regex engine.
is_ipv4_address () {
  declare regex='(?<![0-9])(?:(?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])[.](?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5]))(?![0-9])'
  $BLIP_EXTERNAL_CMD_GREP -Pq "^$regex$" <<< "${1:-}"
}

is_ipv4_prefix () {
  declare ip="${1%%/*}"
  declare prefix="${1##*/}"
  if is_ipv4_address "$ip" && is_int "$prefix" &&
    [[ $prefix -ge 0 ]] && [[ $prefix -le 32 ]] ; then
    return 0
  fi
  return 1
}

# TODO(nicolaw): Try to get this working using bash's own regex engine.
is_ipv6_address () {
  declare regex='((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?'
  $BLIP_EXTERNAL_CMD_GREP -Pq "^$regex$" <<< "${1:-}"
}

is_ipv6_prefix () {
  declare ip="${1%%/*}"
  declare prefix="${1##*/}"
  if is_ipv6_address "$ip" && is_int "$prefix" &&
    [[ $prefix -ge 0 ]] && [[ $prefix -le 128 ]] ; then
    return 0
  fi
  return 1
}

get_free_disk_space () {
  while read -r _ blocks _ ; do
    if is_int "$blocks" ; then
      echo -n "$(( blocks * 1024 ))"
    fi
  done < <(df -kP "$1")
}

get_username () {
  declare user="${USER:-$LOGNAME}"
  user="${user:-$(id -un)}"
  echo -n "${user:-$(whoami)}"
}

get_user_shell () {
  while read -r username _ _ _ _ _ shell ; do
    if [[ -n "${username:-}" && "${username:-}" == "${1:-}" ]] ; then
      echo -n "${shell:-}"
      break
    fi
  done < <(getent passwd "${1:-}")
}

get_gecos_name () {
  get_gecos_info "name" "$@"
}

# https://en.wikipedia.org/wiki/Gecos_field
get_gecos_info () {
  declare key="${1:-}"
  declare user="${2:-$(get_username)}"
  #while IFS=: read username passwd uid gid gecos home shell ; do
  while IFS=: read -r username _ _ _ gecos _ _ ; do
    if [[ "$user" = "$username" ]] ; then
      if [[ -n "$key" ]] && [[ "$gecos" =~ ([,;]) ]] ; then
        IFS="${BASH_REMATCH[1]}" read -r name addr office home email <<< "$gecos"
        case "$key" in
          *name) echo -n "$name" ;;
          building|room|addr*) echo -n "$addr" ;;
          office*) echo -n "$office" ;;
          home*) echo -n "$home" ;;
          *) echo -n "$email" ;;
        esac
      elif [[ -z "$key" ]] || [[ "$key" = "name" ]] ; then
        echo -n "$gecos"
      fi
      break
    fi
  done < <(getent passwd "$user")
}

# English language boolean true or false.
is_true () { [[ "${1:-}" =~ ^yes|on|enabled?|true|1$ ]]; }
is_false () { [[ "${1:-}" =~ ^no|off|disabled?|false|0$ ]]; }
is_boolean () { is_true "$@" || is_false "$@"; }

# Evaluates if single argument input is an absolute integer.
is_abs_int () { [[ "${1:-}" =~ ^[0-9]+$ ]]; }
is_absolute_integer () { is_abs_int "$@"; }

is_zero () { [[ "${1:-}" =~ ^[-\+]?0*\.?0+$ ]]; }
is_negative () { [[ "${1:-}" =~ ^-[0-9]*\.?[0-9]+$ ]] && [[ "${1:-}" =~ [1-9] ]]; }
is_positive () { [[ "${1:-}" =~ ^\+?[0-9]*\.?[0-9]+$ ]] && [[ "${1:-}" =~ [1-9] ]]; }

is_float () { [[ "${1:-}" =~ ^[-\+]?[0-9]*\.[0-9]+$ ]]; }

# Converts single argument input to an absolute value.
abs () {
  declare val="${1:-}"
  if is_positive "$val" || is_zero "$val" ; then
    echo -n "$val"
  elif is_int "$val" ; then
    echo -n $(( val * -1 ))
  elif is_float "$val" ; then
    $BLIP_EXTERNAL_CMD_BC <<< "$val * -1"
  else
    return 2
  fi
}
absolute () { abs "$@"; }

# Convert one or more words to uppercase without explicit variable substition.
# (Not meant as a replacement for tr in a pipeline).
to_upper () {
  for word in "$@" ; do
    echo "${word^^}"
  done
}

# Convert one or more words to lowercase without explicit variable substition.
# (Not meant as a replacement for tr in a pipeline).
to_lower () {
  for word in "$@" ; do
    echo "${word,,}"
  done
}

# Evaluates if argument2 is present as distinct word in argument1.
# Equivalent of grep -w.
# TODO(nicolaw): Should this be extended to have is_word_in_strings, and/or
#                is/are_words_in_string variants? Would that be overkill?
is_word_in_string () {
  declare str="${1:-}"
  declare re="\\b${2:-}\\b"
  [[ "$str" =~ $re ]] && return 0
  return 1
}

# Append a list of word(s) to argument1 if they are not already present as
# distinct words.
append_if_not_present () {
  declare base_str="${1:-}"; shift
  for add_str in "$@" ; do
    if ! matches_word "$base_str" "$add_str" ; then
      base_str="${base_str} ${add_str}"
    fi
  done
  echo "${base_str## }"
}

# Returns all mount points, optionally filtered by device.
get_fs_mounts () {
  declare device
  [[ -z "${1:-}" ]] || device=$(readlink -f "${1}")
  while IFS=" " read -r source target rest; do
    # Need echo -e to unescape source/target.
    if [[ -z "${device:-}" || "$(echo -e "${source}")" = "${device}" ]] ; then
      echo -e "${target}"
    fi
  done < /proc/mounts
}

# FIXME: Check out what is failing in unit tests that call this function when we
#        are checking the age of a file that does not exist. Is it the unittest
#        that is broken, or blip itslef?
# tests/../blip.bash: line 852: 1517188838 -  : syntax error: operand expected (error token is "-  ")
# test #78 "echo 1" failed:
#   expected "0"
#     got "1"
#    1 of 79 050_documentation.sh tests failed in 0.160s.

# %w %W  time of file birth; - or 0 if unknown (creation)
# %x %X  time of last access, human-readable (read)
# %y %Y  time of last modification, human-readable (content)
# %z %Z  time of last change, human-readable (meta data)
get_file_age () {
  echo -n $(( $(get_unixtime -1) - $($BLIP_EXTERNAL_CMD_STAT -c %Y "${1:-}") ))
}

# Define ANSI colour code variables.
# https://en.wikipedia.org/wiki/ANSI_escape_code
if is_true "${BLIP_ANSI_VARIABLES:-}" && [[ -z "${ANSI[*]+defined}" ]] ; then
  declare -rx ANSI_RESET=""          #

  declare -rx ANSI_BLINK_SLOW=""     #
  declare -rx ANSI_BLINK_FAST=""     #
  declare -rx ANSI_BLINK_OFF=""     #

  declare -rx ANSI_HIDDEN_ON=""      #
  declare -rx ANSI_HIDDEN_OFF=""    #

  declare -rx ANSI_STRIKE_ON=""      #
  declare -rx ANSI_STRIKE_OFF=""    #
  declare -rx ANSI_ITALIC_ON=""      #
  declare -rx ANSI_ITALIC_OFF=""    #

  declare -rx ANSI_UNDERLINE_ON=""   #
  declare -rx ANSI_UNDERLINE_OFF="" #
  declare -rx ANSI_OVERLINE_ON=""   #
  declare -rx ANSI_OVERLINE_OFF=""  #

  declare -rx ANSI_FRAME_ON=""      #
  declare -rx ANSI_FRAME_OFF=""     #
  declare -rx ANSI_ENCIRCLE_ON=""   #
  declare -rx ANSI_ENCIRCLE_OFF=""  #

  declare -rx ANSI_BOLD_ON=""        #
  declare -rx ANSI_BOLD_OFF=""      #
  declare -rx ANSI_FAINT_ON=""       #
  declare -rx ANSI_FAINT_OFF=""     #

  declare -rx ANSI_INVERSE_ON=""     #
  declare -rx ANSI_INVERSE_OFF=""   #

  declare -rx ANSI_FG_BLACK=""      #
  declare -rx ANSI_FG_RED=""        #
  declare -rx ANSI_FG_GREEN=""      #
  declare -rx ANSI_FG_YELLOW=""     #
  declare -rx ANSI_FG_BLUE=""       #
  declare -rx ANSI_FG_MAGENTA=""    #
  declare -rx ANSI_FG_CYAN=""       #
  declare -rx ANSI_FG_WHITE=""      #
  declare -rx ANSI_FG_DEFAULT=""    #

  declare -rx ANSI_BG_BLACK=""      #
  declare -rx ANSI_BG_RED=""        #
  declare -rx ANSI_BG_GREEN=""      #
  declare -rx ANSI_BG_YELLOW=""     #
  declare -rx ANSI_BG_BLUE=""       #
  declare -rx ANSI_BG_MAGENTA=""    #
  declare -rx ANSI_BG_CYAN=""       #
  declare -rx ANSI_BG_WHITE=""      #
  declare -rx ANSI_BG_DEFAULT=""    #

  declare -rxA ANSI=(
      [reset]="$ANSI_RESET"
      [blink]="$ANSI_BLINK_SLOW"
      [blink_slow]="$ANSI_BLINK_SLOW"
      [blink_fast]="$ANSI_BLINK_FAST"
      [blink_slow_on]="$ANSI_BLINK_SLOW"
      [blink_fast_on]="$ANSI_BLINK_FAST"
      [blink_off]="$ANSI_BLINK_OFF"
      [hidden]="$ANSI_HIDDEN_ON"
      [hidden_on]="$ANSI_HIDDEN_ON"
      [hidden_off]="$ANSI_HIDDEN_OFF"
      [strike]="$ANSI_STRIKE_ON"
      [strike_on]="$ANSI_STRIKE_ON"
      [strike_off]="$ANSI_STRIKE_OFF"
      [italic]="$ANSI_ITALIC_ON"
      [italic_on]="$ANSI_ITALIC_ON"
      [italic_off]="$ANSI_ITALIC_OFF"
      [underline]="$ANSI_UNDERLINE_ON"
      [underline_on]="$ANSI_UNDERLINE_ON"
      [underline_off]="$ANSI_UNDERLINE_OFF"
      [overline]="$ANSI_OVERLINE_ON"
      [overline_on]="$ANSI_OVERLINE_ON"
      [overline_off]="$ANSI_OVERLINE_OFF"
      [frame]="$ANSI_FRAME_ON"
      [frame_on]="$ANSI_FRAME_ON"
      [frame_off]="$ANSI_FRAME_OFF"
      [encircle]="$ANSI_ENCIRCLE_ON"
      [encircle_on]="$ANSI_ENCIRCLE_ON"
      [encircle_off]="$ANSI_ENCIRCLE_OFF"
      [bold]="$ANSI_BOLD_ON"
      [bold_on]="$ANSI_BOLD_ON"
      [bold_off]="$ANSI_BOLD_OFF"
      [faint]="$ANSI_FAINT_ON"
      [faint_on]="$ANSI_FAINT_ON"
      [faint_off]="$ANSI_FAINT_OFF"
      [inverse]="$ANSI_INVERSE_ON"
      [inverse_on]="$ANSI_INVERSE_ON"
      [inverse_off]="$ANSI_INVERSE_OFF"
      [black]="$ANSI_FG_BLACK"
      [fg_black]="$ANSI_FG_BLACK"
      [bg_black]="$ANSI_BG_BLACK"
      [red]="$ANSI_FG_RED"
      [fg_red]="$ANSI_FG_RED"
      [bg_red]="$ANSI_BG_RED"
      [green]="$ANSI_FG_GREEN"
      [fg_green]="$ANSI_FG_GREEN"
      [bg_green]="$ANSI_BG_GREEN"
      [yellow]="$ANSI_FG_YELLOW"
      [fg_yellow]="$ANSI_FG_YELLOW"
      [bg_yellow]="$ANSI_BG_YELLOW"
      [blue]="$ANSI_FG_BLUE"
      [fg_blue]="$ANSI_FG_BLUE"
      [bg_blue]="$ANSI_BG_BLUE"
      [magenta]="$ANSI_FG_MAGENTA"
      [fg_magenta]="$ANSI_FG_MAGENTA"
      [bg_magenta]="$ANSI_BG_MAGENTA"
      [cyan]="$ANSI_FG_CYAN"
      [fg_cyan]="$ANSI_FG_CYAN"
      [bg_cyan]="$ANSI_BG_CYAN"
      [white]="$ANSI_FG_WHITE"
      [fg_white]="$ANSI_FG_WHITE"
      [bg_white]="$ANSI_BG_WHITE"
      [fg_default]="$ANSI_FG_DEFAULT"
      [bg_default]="$ANSI_BG_DEFAULT"
      )
fi



if (( BASH_VERSINFO[0] < 4 )); then
  echo "cmdarg is incompatible with bash versions < 4, please upgrade bash" >&2
  exit 1
fi

CMDARG_ERROR_BEHAVIOR=return

CMDARG_FLAG_NOARG=0
CMDARG_FLAG_REQARG=2 # : identifier
CMDARG_FLAG_OPTARG=4 # ? identifier

CMDARG_TYPE_ARRAY=1  # [] identifier
CMDARG_TYPE_HASH=2   # {} identifier
CMDARG_TYPE_STRING=3
CMDARG_TYPE_BOOLEAN=4

cmdarg () {
  # cmdarg <option> <key> <description> [default value] [validator function]
  #
  # option : The short name (single letter) of the option
  # key : The long key that should be placed into cmdarg_cfg[] for this option
  # description : The text description for this option to be used in 
  #               cmdarg_usage
  #
  # default value : The default value, if any, for the argument
  # validator : This is passed through eval(), with $1 equal to the current
  #             value of the argument in question, and must return non-zero if
  #             the argument value is invalid. Can be straight bash, but it
  #             really should be the name of a function. This may be enforced
  #             in future versions of the library.
  #

  # FIXME: Support optionally specifying ONLY a short arg, or ONLY a long arg.
  #        cmdarg.sh: line 38: CMDARG_FLAGS: bad array subscript
  #        cmdarg.sh: line 78: CMDARG_FLAGS[$shortopt]: bad array subscript
  #        cmdarg.sh: line 38: CMDARG_TYPES: bad array subscript
  #        cmdarg.sh: line 79: CMDARG_TYPES[$key]: bad array subscript
  #        cmdarg "" "long" "Long only (no short)"
  #        cmdarg "s" "" "Short only (no long)"

  declare shortopt="${1:-}"
  declare key="${2:-}"
  if [[ -z "$shortopt" ]]; then
    >&2 echo "Missing mandatory short flag argument."
    $CMDARG_ERROR_BEHAVIOR 1
  fi
  if [[ -z "$key" ]]; then
    >&2 echo "Missing mandatory long option argument."
    $CMDARG_ERROR_BEHAVIOR 1
  fi

  shortopt="${1:0:1}"

  if [[ -n "${CMDARG_FLAGS[$shortopt]:-}" || -n "${CMDARG_TYPES[$key]:-}" ]] \
     || declare -F "cmdarg_$key" > /dev/null 2>&1 ; then
    printf "Command line flag -%s,--%s is reserved or already defined!\n" \
      "$shortopt" "$key" >&2
    $CMDARG_ERROR_BEHAVIOR 1
  fi

  declare -A argtypemap
  argtypemap[':']=$CMDARG_FLAG_REQARG
  argtypemap['?']=$CMDARG_FLAG_OPTARG
  declare argtype="${1:1:1}"

  if [[ "$argtype" =~ ^[\[{]$ ]]; then
    echo "Flags required [:?] when specifying Hash or Array arguments" \
         "(${argtype})!" >&2
    $CMDARG_ERROR_BEHAVIOR 1

  elif [[ -n "$argtype" ]]; then
    CMDARG_FLAGS[$shortopt]=${argtypemap[$argtype]:-}

    if [[ "${1:2:4}" == "[]" ]]; then
      if ! declare -p "$key" > /dev/null 2>&1 ; then
        echo "Array variable \"${key}\" does not exist; array arguments must" \
             "declare variable first." >&2
        echo "Try \"declare -a $key=()\" before calling cmdarg()." >&2
        $CMDARG_ERROR_BEHAVIOR 1
      fi
      CMDARG_TYPES[$key]=$CMDARG_TYPE_ARRAY

    elif [[ "${1:2:4}" == "{}" ]]; then
      if ! declare -p "$key" > /dev/null 2>&1 ; then
        echo "Associative array variable \"${key}\" does not exist; hash arguments must" \
             "declare variable first." >&2
        echo "Try \"declare -A $key=()\" before calling cmdarg()." >&2
        $CMDARG_ERROR_BEHAVIOR 1
      fi
      CMDARG_TYPES[$key]=$CMDARG_TYPE_HASH
    else
      CMDARG_TYPES[$key]=$CMDARG_TYPE_STRING
    fi

  else
    CMDARG_FLAGS[$shortopt]=$CMDARG_FLAG_NOARG
    CMDARG_TYPES[$key]=$CMDARG_TYPE_BOOLEAN
    cmdarg_cfg[$key]=false
  fi

  CMDARG[$shortopt]="${2:-}"
  CMDARG_REV[${2:-}]="$shortopt"
  CMDARG_DESC[$shortopt]="${3:-}"
  CMDARG_DEFAULT[$shortopt]="${4:-}"

  if [[ ${CMDARG_FLAGS[$shortopt]:-} -eq $CMDARG_FLAG_REQARG && -z "${4:-}" ]]
  then
    CMDARG_REQUIRED+=("$shortopt")
  else
    CMDARG_OPTIONAL+=("$shortopt")
  fi

  cmdarg_cfg[${2:-}]="${4:-}"
  declare validatorfunc=""
  validatorfunc="${5:-}"

  if [[ -n "$validatorfunc" ]] \
      && ! declare -F "$validatorfunc" > /dev/null 2>&1 ; then
    echo "Validators must be bash functions accepting 1 argument (not" 
         "'$validatorfunc')" >&2
    $CMDARG_ERROR_BEHAVIOR 1
  fi
  CMDARG_VALIDATORS[$shortopt]="$validatorfunc"
}

cmdarg_info () {
  # cmdarg_info <flag> <value>
  #
  # Sets various flags about your script that are printed during cmdarg_usage
  #

  declare flag="${1:-}"; shift || true
  case "$flag" in
    header|copyright|footer|author|version)
      {
        local IFS=$'\n'
        CMDARG_INFO[$flag]="$*"
      }
      ;;
    *)
      echo "cmdarg_info <flag> <value>" >&2
      echo "Where <flag> is 'header', 'version', 'author', 'copyright' or" \
           "'footer'" >&2
      $CMDARG_ERROR_BEHAVIOR 1
      ;;
  esac
}

cmdarg_describe () {
  # cmdarg_describe <shortopt>
  #
  # Prints a description of an argument definition.
  #

  if [[ -z "${cmdarg_helpers[describe]:-}" ]] ; then
    return
  fi

  declare opt="${1:-}"
  declare longopt="${CMDARG[$opt]:-}"
  declare argtype="${CMDARG_TYPES[$longopt]:-}"
  declare default="${CMDARG_DEFAULT[$opt]:-}"
  declare description="${CMDARG_DESC[$opt]:-}"
  declare flags="${CMDARG_FLAGS[$opt]:-}"
  declare validator="${CMDARG_VALIDATORS[$opt]:-}"

  "${cmdarg_helpers[describe]}" "$longopt" "$opt" "$argtype" "$default" \
    "$description" "$flags" "$validator"
}

cmdarg_describe_default () {
  # cmdarg_describe_default
  #
  # Default cmdarg_helper for cmdarg_describe; prints description of argument.
  #

  declare longopt="${1:-}"
  declare opt="${2:-}"
  declare argtype="${3:-}"
  declare default="${4:-}"
  declare description="${5:-}"
  declare flags="${6:-}"
  declare validator="${7:-}"

  if [[ -n "$default" ]]; then
    default=" (Default \"$default\")"
  fi
  description="${description%.}."

  case "$argtype" in
    $CMDARG_TYPE_STRING)
      printf " -%s, --%s=VALUE : String. %s%s\n" \
        "$opt" "$longopt" "$description" "$default"
      ;;
    $CMDARG_TYPE_BOOLEAN)
      printf " -%s, --%s : Boolean. %s%s\n" \
        "$opt" "$longopt" "$description" "$default"
      ;;
    $CMDARG_TYPE_ARRAY)
      printf " -%s, --%s=VALUE : Array. %s %s%s\n" \
        "$opt" "$longopt" "$description" \
        "(See note)" \
        "$default"
      ;;
    $CMDARG_TYPE_HASH)
      printf " -%s, --%s KEY=VALUE : Hash. %s %s%s\n" \
        "$opt" "$longopt" "$description" \
        "(See note)" \
        "$default"
      ;;
    *)
      printf "Unable to return description for -%s,--%s; unknown type (%s)\n" \
        "$opt" "$longopt" "$argtype" >&2
      $CMDARG_ERROR_BEHAVIOR 1
      ;;
    esac
}

cmdarg_usage () {
  # cmdarg_usage
  #
  # Prints a helpful usage message about the current program.
  #

  declare version="${CMDARG_INFO[version]:-}"
  if [[ -z "$version" && -n "${VERSION:-}" ]]; then
    version="$VERSION"
  fi
  printf "%s%s\n" \
    "${0##*/}" "${version:+ version $version}"

  declare _tmp="${CMDARG_INFO[copyright]:-}"
  if [[ -n "${CMDARG_INFO[copyright]:-}" && "${CMDARG_INFO[author]:-}" ]]; then
    _tmp+=" : "
  fi
  _tmp+="${CMDARG_INFO[author]:-}"
  if [[ -n "$_tmp" ]]; then
    echo "$_tmp"
  fi

  if [[ -n "${CMDARG_INFO[header]:-}" ]]; then
    printf "\n%s\n" "${CMDARG_INFO[header]:-}"
  fi

  declare -a argtype_footnotes=()

  declare key=""
  if [[ "${#CMDARG_REQUIRED[@]}" -ne 0 ]]; then
    echo
    echo "Required arguments:"
    for key in "${CMDARG_REQUIRED[@]}"
    do
      cmdarg_describe "$key"
      declare longopt="${CMDARG[$key]}"
      argtype_footnotes[${CMDARG_TYPES[$longopt]:-0}]=true
    done
  fi

  if [[ "${#CMDARG_OPTIONAL[@]}" -ne 0 ]]; then
    echo
    echo "Optional arguments:"
    for key in "${CMDARG_OPTIONAL[@]}"
    do
      cmdarg_describe "$key"
      declare longopt="${CMDARG[$key]}"
      argtype_footnotes[${CMDARG_TYPES[$longopt]:-0}]=true
    done
  fi

  if [[ -n "${argtype_footnotes[$CMDARG_TYPE_ARRAY]:-}" || \
        -n "${argtype_footnotes[$CMDARG_TYPE_HASH]:-}" ]]; then
    echo
    echo "Note: arguments of Array & Hash types may be specified multiple" \
         "times."
  fi

  if [[ -n "${CMDARG_INFO[footer]:-}" ]]; then
    echo
    echo "${CMDARG_INFO[footer]:-}"
  fi
}

cmdarg_validate () {
  declare longopt="${1:-}"
  declare value="${2:-}"
  declare hashkey="${3:-}"
  declare shortopt="${CMDARG_REV[$longopt]:-}"

  if [[ -n "${CMDARG_VALIDATORS[$shortopt]:-}" ]]; then
    if ! ( "${CMDARG_VALIDATORS[$shortopt]}" "$value" "$hashkey" ) ; then
      printf "Invalid value for -%s,--%s : %s\n" \
        "$shortopt" "$longopt" "$value" >&2
      $CMDARG_ERROR_BEHAVIOR 1
    fi
  fi
  return 0
}

cmdarg_set_opt () {
  declare key="${1:-}"
  declare arg="${2:-}"

  case "${CMDARG_TYPES[$key]:-}" in
    $CMDARG_TYPE_STRING)
      cmdarg_cfg[$key]="$arg"
      cmdarg_validate "$key" "$arg" || $CMDARG_ERROR_BEHAVIOR 1
      ;;
    $CMDARG_TYPE_BOOLEAN)
      cmdarg_cfg[$key]=true
      cmdarg_validate "$key" "$arg" || $CMDARG_ERROR_BEHAVIOR 1
      ;;
    $CMDARG_TYPE_ARRAY)
      # shellcheck disable=SC2016
      declare str='${#'"$key"'[@]}'
      declare prevlen=""; prevlen="$(eval "echo \"$str\"")"
      eval "${key}[$(( prevlen + 1 ))]=\$arg"
      cmdarg_validate "$key" "$arg" || $CMDARG_ERROR_BEHAVIOR 1
      ;;
    $CMDARG_TYPE_HASH)
      declare k="${arg%%=*}"
      declare v="${arg#*=}"
      #if [[ "$k" == "$arg" && "$v" == "$arg" && "$k" == "$v" ]]; then
      if [[ "$k" == "$arg" ]]; then
        echo "Malformed hash argument: $arg" >&2
        $CMDARG_ERROR_BEHAVIOR 1
      fi
      eval "$key[\$k]=\$v"
      cmdarg_validate "$key" "$v" "$k" || $CMDARG_ERROR_BEHAVIOR 1
      ;;
    *)
      echo "Unable to return string description for ${key}; unknown type" \
           "${CMDARG_TYPES[$key]:-}" >&2
      $CMDARG_ERROR_BEHAVIOR 1
      ;;
  esac

  return 0
}

cmdarg_check_empty () {
  # cmdarg_check_empty <shortopt>
  #
  # Called by cmdarg_parse to validate that a required argument has a value
  # defined or not.
  #

  # FIXME: Optimise this so that we return a boolean true or false, instead of
  #        capturing a string and testing against "" in a process substition!

  declare key="${1:-}"
  declare longopt="${CMDARG[$key]:-}"
  declare type="${CMDARG_TYPES[$longopt]:-}"

  case "$type" in
    $CMDARG_TYPE_STRING)
      echo "${cmdarg_cfg[$longopt]:-}"
      ;;
    $CMDARG_TYPE_BOOLEAN)
      echo "${cmdarg_cfg[$longopt]:-}"
      ;;
    $CMDARG_TYPE_ARRAY)
      # shellcheck disable=SC2016
      declare lval='${!'"${longopt}"'[@]}'
      eval "echo \"$lval\""
      ;;
    $CMDARG_TYPE_HASH)
      # shellcheck disable=SC2016
      declare lval='${!'"${longopt}"'[@]}'
      eval "echo \"$lval\""
      ;;
    *)
      echo "${cmdarg_cfg[$longopt]:-}"
      ;;
  esac
}

cmdarg_parse () {
  # cmdarg_parse "$@"
  #
  # Parse command line "$@" arguments. This function only knows about the
  # argument definitions that were previously supplied and defined by cmdarg()
  # calls.
  #

  declare usage=0
  declare failed=0
  declare parsing=0

  while [[ $# -ne 0 ]]; do
    declare optarg=""
    declare opt=""
    declare longopt=""
    declare fullopt="${1:-}"
    declare is_equals_arg=1

    shift || true
    if [[ "$fullopt"  =~ ^(--[a-zA-Z0-9_\-]+|^-[a-zA-Z0-9])= ]]; then
      declare tmpopt=$fullopt
      fullopt=${tmpopt%%=*}
      # FIXME: Are we eating too much here? Should it be ${tmpopt#*=} instead?
      optarg=${tmpopt##*=}
      is_equals_arg=0
    fi

    if [[ "$fullopt" == "--" && $parsing -eq 0 ]]; then
      cmdarg_argv+=("$@")
      break
    elif [[ "${fullopt:0:2}" == "--" ]]; then
      longopt="${fullopt:2}"
      opt="${CMDARG_REV[$longopt]:-}"
    elif [[ "${fullopt:0:1}" == "-" && ${#fullopt} -eq 2 ]]; then
      opt="${fullopt:1}"
      longopt="${CMDARG[$opt]:-}"
    elif [[ "${fullopt:0:1}" != "-" ]]; then
      cmdarg_argv+=("$fullopt")
      continue
    else
      echo "Malformed argument: $fullopt" >&2
      echo "While parsing: $*" >&2
      failed+=1
    fi

    if [[ "$opt" == "h" || "$longopt" == "help" ]]; then
      usage=1
    fi

    if [[ $is_equals_arg -eq 1 ]]; then
      if   [[ -n "$opt" ]] \
        && [[ ${CMDARG_FLAGS[$opt]:-} -eq ${CMDARG_FLAG_REQARG}
           || ${CMDARG_FLAGS[$opt]:-} -eq ${CMDARG_FLAG_OPTARG} ]]; then
        optarg="${1:-}"
        shift || true
      fi
    fi

    if [[ -n "$opt" && -n "${CMDARG[$opt]:+abc}" ]]; then
      cmdarg_set_opt "${CMDARG[$opt]:-}" "$optarg"
      declare rc=$?
      failed=$(( failed + rc ))
    else
      declare -a invalid=()
      if [[ -n "$opt" ]]; then invalid+=("-$opt"); fi
      if [[ -n "$longopt" ]]; then invalid+=("--$longopt"); fi
      (IFS=" | "; echo "Unknown argument or invalid value : ${invalid[*]}" >&2)
      failed+=1
    fi
  done

  # Don't call $CMDARG_ERROR_BEHAVIOR during early validation; tell the user
  # everything they did wrong first.

  declare -a missing=()

  declare key=""
  if [[ "${#CMDARG_REQUIRED[@]}" -ne 0 ]] ; then
    for key in "${CMDARG_REQUIRED[@]}"
    do
      # FIXME: We should check a return code, not do a process substitution.
      if [[ -z "$(cmdarg_check_empty "$key")" ]]; then
        missing+=("-${key}")
        failed=$(( failed + 1 ))
      fi
    done
  fi

  if [[ $failed -gt 0 ]]; then
    if [[ ${#missing[@]} -ge 1 ]]; then
      echo "Missing ${#missing[@]} mandatory argument(s) : ${missing[*]}" >&2
    fi
    echo >&2
  fi

  if [[ -n "${cmdarg_helpers[usage]:-}" ]] ; then
    if [[ $usage -ge 1 ]]; then
      "${cmdarg_helpers[usage]}"
    elif [[ $failed -ge 1 ]]; then
      "${cmdarg_helpers[usage]}" >&2
      $CMDARG_ERROR_BEHAVIOR 1
    fi
  fi
}

cmdarg_purge () {
  declare arr=""
  for arr in cmdarg_cfg CMDARG CMDARG_REV CMDARG_OPTIONAL CMDARG_REQUIRED \
        CMDARG_DESC CMDARG_DEFAULT CMDARG_VALIDATORS CMDARG_INFO \
        CMDARG_FLAGS CMDARG_TYPES cmdarg_argv cmdarg_helpers
  do
    eval "$arr=()"
  done

  cmdarg_helpers[describe]="cmdarg_describe_default"
  cmdarg_helpers[usage]="cmdarg_usage"
  cmdarg "h" "help" "Show this help"
}

# Holds the final map of configuration options
declare -xA cmdarg_cfg=()
# Maps (short arg) -> (long arg)
declare -xA CMDARG=()
# Maps (long arg) -> (short arg)
declare -xA CMDARG_REV=()
# A list of optional arguments (e.g., no :)
declare -xa CMDARG_OPTIONAL=()
# A list of required arguments (e.g., :)
declare -xa CMDARG_REQUIRED=()
# Maps (short arg) -> (description)
declare -xA CMDARG_DESC=()
# Maps (short arg) -> default
declare -xA CMDARG_DEFAULT=()
# Maps (short arg) -> validator
declare -xA CMDARG_VALIDATORS=()
# Miscellanious info about this script
declare -xA CMDARG_INFO=()
# Map of (short arg) -> flags
declare -xA CMDARG_FLAGS=()
# Map of (short arg) -> type (string, array, hash)
declare -xA CMDARG_TYPES=()
# Array of all elements found after --
declare -xa cmdarg_argv=()
# Hash of functions that are used for user-extensible functionality
declare -xA cmdarg_helpers=()

cmdarg_helpers[describe]="cmdarg_describe_default"
cmdarg_helpers[usage]="cmdarg_usage"
cmdarg "h" "help" "Show this help"

# trap 'declare rc=$?; >&2 echo "Unexpected error executing $BASH_COMMAND at ${BASH_SOURCE[0]} line $LINENO"; __blip_stacktrace__ >&2; exit $?' ERR
__blip_stacktrace__() {
  declare -i frame=0 argv_offset=0
  if shopt -q extdebug ; then
    while : ; do
      declare -a caller_info=( $(caller $frame) ) argv=('')
      declare -i argc=0 frame_argc=0
      [[ $frame -lt ${#BASH_ARGC[@]} ]] && frame_argc=${BASH_ARGC[frame]}
      for ((frame_argc--,argc=0; frame_argc >= 0; argc++,frame_argc--)); do
        argv[argc]=${BASH_ARGV[argv_offset + frame_argc]}
        case "${argv[argc]}" in
          *[[:space:]]*) argv[argc]="'${argv[argc]}'" ;;
        esac
      done
      if [[ ${#caller_info[@]} -gt 0 ]] ; then
        printf "%$(( frame * 2 ))s%s:%s %s:: %s %s\n" \
          "" "${caller_info[2]}" "${caller_info[0]}" "${caller_info[1]}" \
          "${FUNCNAME[frame]}" "${argv[*]}"
      else
        printf "%$(( frame * 2 ))s%s %s %s %s\n" \
          "" "$BASH" "-$-" "$0" "${argv[*]}"
        break
      fi
      argv_offset=$((argv_offset+BASH_ARGC[frame]))
      frame=$((frame+1))
    done
  else
    # Pauper's stack trace without extdebug's provision of BASH_ARG*
    for fn in "${FUNCNAME[@]}"; do
      ((frame)) && printf "%$(( frame * 2 ))s%s:%s %s %s\n" \
        "" "${BASH_SOURCE[$frame]}" "${BASH_LINENO[$frame]}" "$fn"
      frame=$((frame+1))
    done
  fi
}

#
# That strange feeling you're experiencing right now... I should apologise for
# that. It's called cognitive dissonance.
# https://en.wikipedia.org/wiki/Cognitive_dissonance
#
# Sorry if you consider yourself harmed as a result of reading or using this
# software. If it makes any difference to you, I now have very puffy eyes from
# writing this.
#
# "...Hello Doctor, I've been having trouble with my eyes. They're swollen..."
#