#!/usr/bin/env bash # # Simple option parsing library # Copyright (c) Petr Baudis, 2005; GPL # # See the bottom for example code. _cg_cmd=${0##*/} die() { echo "$_cg_cmd: $@" >&2 exit 1 } # Usage: path_lookup COMMAND VARNAME [CMDTEST] # Lookup COMMAND in $PATH and save the full path to VARNAME (optionally, # only if CMDTEST on the command succeeds). # This would have been type -P but we want to be bash2 compatible. path_lookup() { local exename="$1" varname="$2" cmdtest="$3" # We do our own $PATH iteration as it's faster than the fork() # of $(which), and this happens many times every time we # execute some cg tool. # Cut'n'pasted to the 'cg' source. local save_IFS dir cmd save_IFS="$IFS"; IFS=: for dir in $PATH; do IFS="$save_IFS" cmd="$dir/$exename" if [ -x "$cmd" ] && { [ -z "$cmdtest" ] || eval "$cmdtest"; }; then export $varname="$cmd" break fi done IFS="$save_IFS" } # Usage: width="$(...single column... | column_width MINUSPREFIX MAXWIDTH)" column_width() { local line= minusprefix="$1" maxwidth="$2" [ "$maxwidth" ] || maxwidth=35 while read line; do line=${line#$1}; echo ${#line} done | sort -nr | head -n 1 | ( read maxlen; [ ${maxlen:-0} -le $maxwidth ] || maxlen=$maxwidth; echo ${maxlen:-0} ) } # Usage: columns_print COL1 WIDTH COL2 - COL3 tWIDTH COL4 - ... columns_print() { local fmt= cols= cols=() while [ $# -gt 0 ]; do local col="$1"; shift local width="$1"; shift local tab= local trim= if [ x"${width:0:1}" = x"t" ]; then tab=1; width="${width:1}" fi if [ x"${width:0:1}" = x"m" ]; then trim=1; width="${width:1}" fi if [ x"$width" = x"-" ]; then fmt="$fmt%s" else fmt="$fmt%-${width}s" if [ -n "$trim" ] && [ ${#col} -gt "$width" ]; then width=$((width - 3)) col="${col:0:$width}..." fi fi cols[${#cols[@]}]="$col" [ -z "$tab" ] || fmt="$fmt\t"; done printf "$fmt\n" "${cols[@]}" } print_help() { path_lookup "cg-$2" "_cg_cmd" [ -n "$_cg_cmd" ] || exit 1 sed -n '/^USAGE=/,0s/.*"\(.*\)"/Usage: \1/p' < "$_cg_cmd" if [ x"$1" = xlong ]; then echo # TODO: Reduce this to just one sed if possible. sed -n '3,/^$/s/^# *//p' < "$_cg_cmd" | sed 's/^\(-.*\)::.*/\1::/' exit fi sed -n '3s/^# *//p' < "$_cg_cmd" echo echo "Options:" maxlen="$(sed -n 's/^# \(-.*\)::[^A-Za-z0-9].*/\1/p' < "$_cg_cmd" | column_width)" [ $maxlen -ge 11 ] || maxlen=11 # --long-help _cg_fmt=" %-20s %s\n" sed -n 's/# \(-.*\)::[^A-Za-z0-9]\(.*\)/\1\n\2/p' < "$_cg_cmd" | while read line; do case "$line" in -*) _cg_option="$line" ;; *) columns_print " " - "$_cg_option" "$maxlen" " $line" - ;; esac done columns_print " " - "-h, --help" "$maxlen" " Print usage summary" - columns_print " " - "--long-help" "$maxlen" " Print user manual" - columns_print " " - "--version" "$maxlen" " Print version" - exit } for option in "$@"; do [ x"$option" != x-- ] || break if [ x"$option" = x"-h" ] || [ x"$option" = x"--help" ]; then print_help short "${_cg_cmd##cg-}" elif [ x"$option" = x"--long-help" ]; then print_help long "${_cg_cmd##cg-}" elif [ x"$option" = x"--version" ]; then exec "$(dirname "$0")"/cg-version fi done ARGS=("$@") ARGPOS=0 set '' # clear positional parameters - use $ARGS[] instead if [ -z "$CG_NORC" -a -t 1 -a -e "$HOME/.cgrc" ]; then _cg_name="${_cg_cmd#cg-}" # We hope that there are no weird (regex-sensitive) characters # in Cogito command names. _cg_defaults1="$(sed -n "/^$_cg_cmd/s/^$_cg_cmd //p" < "$HOME/.cgrc")" _cg_defaults2="$(sed -n "/^$_cg_name/s/^$_cg_name //p" < "$HOME/.cgrc")" # And here we explicitly do not quote, allowing multiple arguments # to be specified - default word splitting will do its work here. ARGS=($_cg_defaults1 $_cg_defaults2 "${ARGS[@]}") fi optshift() { unset ARGS[$ARGPOS] ARGS=("${ARGS[@]}") [ -z "$1" -o -n "${ARGS[$ARGPOS]}" ] || die "option $1 requires an argument" } optfail() { die "unrecognized option ${ARGS[$ARGPOS]}" } optconflict() { die "conflicting options $CUROPT and $1" } optparse() { unset OPTARG if [ -z "$1" ]; then case "${ARGS[$ARGPOS]}" in --) optshift; return 1 ;; -*) return 0 ;; *) while (( ++ARGPOS < ${#ARGS[@]} )); do [[ "${ARGS[$ARGPOS]}" != -- ]] || return 1 [[ "${ARGS[$ARGPOS]}" != -* ]] || return 0 done; return 1 ;; esac fi CUROPT="${ARGS[$ARGPOS]}" local match="${1%=}" minmatch="${2:-1}" opt="$CUROPT" o="$CUROPT" val [[ "$1" != *= ]] || val="$match" case "$match" in --*) [ -z "$val" ] || o="${o%%=*}" [ ${#o} -ge $((2 + $minmatch)) -a \ "${match:0:${#o}}" = "$o" ] || return 1 if [[ -n "$val" && "$opt" == *=?* ]]; then ARGS[$ARGPOS]="${opt#*=}" else optshift "$val" fi ;; -?) [[ "$o" == $match* ]] || return 1 [[ "$o" != -?-* || -n "$val" ]] || optfail ARGS[$ARGPOS]=${o#$match} if [ -n "${ARGS[$ARGPOS]}" ]; then [ -n "$val" ] || ARGS[$ARGPOS]=-"${ARGS[$ARGPOS]}"; else optshift "$val" fi ;; *) die "optparse cannot handle $1" ;; esac if [ "$val" ]; then OPTARG="${ARGS[$ARGPOS]}" optshift fi } ### Example: