diff --git a/bashtools.sh b/bashtools.sh index 304bca7..a31052d 100755 --- a/bashtools.sh +++ b/bashtools.sh @@ -1,5 +1,17 @@ #!/usr/bin/env /bin/bash + +#################################### +# To include this in a script: +# +#. ${HOME}/code/bashtools/bashtools.sh +#if [[ -z $HAVE_BASHTOOLS ]]; then +# echo "ERROR: bashtools not installed download from https://git.nethack.net/rob/bashtools" >/dev/stderr +# exit 1 +#fi +# +#################################### + export BASHTOOLS_DIR="${HOME}/.bashtools" if [[ $1 == "install" ]]; then mkdir -p "$BASHTOOLS_DIR" @@ -213,292 +225,6 @@ function getsysstats() { DISKFREE_GB=$(echo "$DISKFREE_MB / 1024" | bc ) } -# menu [ -r RETVAR ] [-R NUMRETVAR] [-a] [-c num] [-l v|h ] [-x AB] [ -q "question text" ] [ -d default ] choice1 choice2 ... choiceN [ -D desc1 desc2 ... descN ] -function menu() { - local answer answernum autoselect bestval - local cols choice choiceformat cperrow cwidth default defaultnum desc - local idx layout mode n nchoices ndescs - local question retvar retvarnum thislen thisnum thistext - local x y repfrom repto - local found - - autoselect=0 - default="" - question="Select an option:" - mode="choices" - layout=v # 'v'ertical or 'h'orizontal - nchoices=0 - ndescs=0 - cwidth=0 - retvar=_SELECTION - cperrow=-1 - found=0 - - while [ $# -ge 1 ]; do - case $1 in - "-a") - autoselect=1 - ;; - "-c") - shift - cperrow="$1" - ;; - "-d") - shift - default="$1" - ;; - "-l") - shift - layout="$1" - ;; - "-r") - shift - retvar="$1" - ;; - "-R") - shift - retvarnum="$1" - ;; - "-x") - shift - repfrom="${1:0:1}" - repto="${1:1:1}" - ;; - "-q") - shift - question="$1" - ;; - "-D") - mode="descriptions" - ;; - *) - if [ "$mode" == "choices" ]; then - choice[${nchoices}]="$1" - if [[ ! -z $repfrom && ! -z $repto ]]; then - choice[${nchoices}]=`echo ${choice[$nchoices]} | tr "$repfrom" "$repto"` - fi - thislen=${#1} - if [ $thislen -gt $cwidth ]; then - cwidth=$thislen - fi - nchoices=$((nchoices + 1)) - else - desc[${ndescs}]="$1" - - # length of this description + length of matching choice - # + 3 for parantheses - thislen=$(( ${#1} + ${#choice[$ndescs]} + 3)) - - ndescs=$((ndescs + 1)) - - if [ $thislen -gt $cwidth ]; then - cwidth=$thislen - fi - fi - ;; - esac - - shift - done - - # Validate layout - if [[ ! $layout =~ v|h ]] ; then - cecho "$RED" "ERROR in ask(): invalid layout. Must be 'v'ertical or 'h'orizontal. [$question]" - die - fi - - # Validate descriptions - if [ $ndescs -gt $nchoices ]; then - cecho "$RED" "ERROR in ask(): Number of descriptions ($ndescs) is larger than number of choices ($nchoices)! [$question]" - die - fi - - # Validate default choice (if given) - if [ -n "$default" ]; then - defaultnum=-1 - for n in ${!choice[@]}; do - if [ "${choice[$n]}" == "$default" ]; then - defaultnum=$n - fi - done - if [ $defaultnum -eq -1 ]; then - cecho "$RED" "ERROR in ask(): given default '$default' does not match any choice. [$question]" - echo " choices are:" - for n in ${!choice[@]}; do - echo " ${choice[$n]}" - done - die - fi - fi - - # Determine choice width - longest choice + 6 chars - # to account for 'xxx) ' - cwidth=$((cwidth + 5)) - - # Determine maximum amount of choices per row - if [ $cperrow -eq -1 ]; then - if [ $nchoices -le 3 ]; then - cperrow=1 - else - cols=$(tput cols) - cperrow=$((cols / cwidth)) - fi - fi - - # Reduce choices per row to minimise (nchoices % cperrow). - # ie. try to make list of choices as close to a - # grid shape as possible. - # - bestval=$(( cperrow - (nchoices % cperrow) )) - n=$((cperrow - 1)) - while [ $n -ge 1 ]; do - thisval=$(( cperrow - (nchoices % n) )) - if [ $thisval -lt $bestval ]; then - bestval=$thisval - cperrow=$n - fi - n=$((n - 1)) - done - - # Determine how many rows we'll need for a - # vertical layout (ie. counting downwards instead - # of right). - if [ "$layout" == "v" ]; then - choicerows=$((nchoices / cperrow)) - if [ $((nchoices % cperrow)) -gt 0 ]; then - choicerows=$((choicerows + 1)) - fi - fi - - # printf format string - choiceformat="%3s) %-$((cwidth-5))s" - - answer="" - # Prompt for a choice until we get a valid answer. - while [ -z "$answer" ]; do - # Display menu - echo -e "$question" - - if [ "$layout" == "h" ]; then - for n in ${!choice[@]}; do - if [ -n "${desc[$n]}" ]; then - thistext="${choice[$n]} (${desc[$n]})" - else - thistext="${choice[$n]}" - fi - - thisnum=$((n + 1)) - if [ "${choice[$n]}" == "${default}" ]; then - thisnum="*$thisnum" - fi - - printf "$choiceformat" "$thisnum" "${thistext}" - if [ $(( (n + 1) % $cperrow )) -eq 0 ]; then - printf "\n" # newline - fi - done - else # ie. vertical - n=0 - for (( y=1; y<=$choicerows; y++)); do - for (( x=0; x<$cperrow; x++)); do - idx=$((n + (x * choicerows))) - if [ $idx -lt $nchoices ]; then - - if [ -n "${desc[$idx]}" ]; then - thistext="${choice[$idx]} (${desc[$idx]})" - else - thistext="${choice[$idx]}" - fi - - thisnum=$((idx + 1)) - if [ "${choice[$idx]}" == "${default}" ]; then - thisnum="*$thisnum" - fi - - printf "$choiceformat" "$thisnum" "${thistext}" - fi - done - n=$((n + 1)) - printf "\n" # newline - done - fi - - printf "\n" - if [ -n "$default" ]; then - printf " (* denotes default)\n" - fi - printf -- "-> " - - # If there's only one answer, just select it. Useful for - # cases where we are basing our list of choices on a dynamic variable. - if [ $autoselect -eq 1 -a $nchoices -eq 1 ]; then - answernum=1 - echo "1 (autoselect)" # make it look like we typed it - else - read answernum - fi - - if [ -z "$answernum" ]; then - if [ -n "$default" ]; then - answer=$default - info "[defaulting to $answer]" - - # populate 'answernum' correctly - for z in ${!choice[@]}; do - if [[ ${choice[$z]} == $answer ]]; then - answernum=$((z + 1)) - break - fi - done - - else - printf "Invalid response.\n\n" - fi - elif [[ ! $answernum =~ ^[0-9]*$ ]] ; then - # try to match based on name - found=0 - shopt -s nocasematch - for z in ${!choice[@]}; do - if [[ ${choice[$z]} =~ $answernum ]]; then - possanswernum[$found]=$((z + 1)) - possanswer[$found]=${choice[z]} - found=$((found + 1)) - fi - done - if [[ $found -eq 1 ]]; then - selectedposs=${possanswer[0]} - selectedpossnum=${possanswernum[0]} - answerhilite=${selectedposs/${answernum}/${BOLD}${answernum}${PLAIN}${CYAN}} - star="" - if [[ -z $BOLD ]]; then - star="*" - fi - printf "${CYAN}Matched '${star}${answerhilite}${star}'.${PLAIN}\n\n" - answernum="$selectedpossnum" - answer="$selectedposs" - elif [[ $found -gt 1 ]]; then - printf "${RED}Invalid response - '${BOLD}$answernum${PLAIN}${RED}' matches $found answers.${PLAIN}\n\n" - for z in ${!possanswer[@]}; do - echo -e " ${RED}${possanswernum[$z]}) ${possanswer[$z]}" - done - echo -e "${PLAIN}" - else - printf ${RED}"Invalid response.${PLAIN}\n\n" - fi - shopt -u nocasematch - elif [ $answernum -lt 1 -o $answernum -gt $nchoices ]; then - printf "Choice out of range.\n\n" - else - answer=${choice[$((answernum - 1))]} - fi - done - - answer="${answer//\'/\\\'}" - eval "$retvar=$'$answer'" - eval "$retvarnum='$answernum'" - return 0 -} - function ask() { # [-s == dont echo input] $1 = prompt $default_val $2 = return_variable_name local answer prompt default retvar readopts="" if [[ $1 == "-s" ]]; then @@ -514,6 +240,294 @@ function ask() { # [-s == dont echo input] $1 = prompt $default_val $2 = return_ eval "$retvar=\"$answer\"" } + +# ask [ -r RETVAR ] [-R NUMRETVAR] [-a] [-c num] [-l v|h ] [-x AB] [ -q "question text" ] [ -d default ] choice1 choice2 ... choiceN [ -D desc1 desc2 ... descN ] +function menu() { + local answer answernum autoselect bestval + local cols choice choiceformat cperrow cwidth default defaultnum desc + local idx layout mode n nchoices ndescs + local question retvar retvarnum thislen thisnum thistext + local x y repfrom repto + local found + + autoselect=0 + default="" + question="Select an option:" + mode="choices" + layout=v # 'v'ertical or 'h'orizontal + nchoices=0 + ndescs=0 + cwidth=0 + retvar=_SELECTION + cperrow=-1 + found=0 + + while [ $# -ge 1 ]; do + case $1 in + "-a") + autoselect=1 + ;; + "-c") + shift + cperrow="$1" + ;; + "-d") + shift + default="$1" + ;; + "-l") + shift + layout="$1" + ;; + "-r") + shift + retvar="$1" + ;; + "-R") + shift + retvarnum="$1" + ;; + "-x") + shift + repfrom="${1:0:1}" + repto="${1:1:1}" + ;; + "-q") + shift + question="$1" + ;; + "-D") + mode="descriptions" + ;; + *) + if [ "$mode" == "choices" ]; then + choice[${nchoices}]="$1" + if [[ ! -z $repfrom && ! -z $repto ]]; then + choice[${nchoices}]=`echo ${choice[$nchoices]} | tr "$repfrom" "$repto"` + fi + thislen=${#1} + if [ $thislen -gt $cwidth ]; then + cwidth=$thislen + fi + nchoices=$((nchoices + 1)) + else + desc[${ndescs}]="$1" + + # length of this description + length of matching choice + # + 3 for parantheses + thislen=$(( ${#1} + ${#choice[$ndescs]} + 3)) + + ndescs=$((ndescs + 1)) + + if [ $thislen -gt $cwidth ]; then + cwidth=$thislen + fi + fi + ;; + esac + + shift + done + + # Validate layout + if [[ ! $layout =~ v|h ]] ; then + error "error in myask(): invalid layout. Must be 'v'ertical or 'h'orizontal. [$question]" + die + fi + + # Validate descriptions + if [ $ndescs -gt $nchoices ]; then + error "error in myask(): Number of descriptions ($ndescs) is larger than number of choices ($nchoices)! [$question]" + die + fi + + # Validate default choice (if given) + if [ -n "$default" ]; then + defaultnum=-1 + for n in ${!choice[@]}; do + if [ "${choice[$n]}" == "$default" ]; then + defaultnum=$n + fi + done + if [ $defaultnum -eq -1 ]; then + error "error in myask(): given default '$default' does not match any choice. [$question]" + csecho "$RED" " choices are:" + for n in ${!choice[@]}; do + csecho "$RED" " ${choice[$n]}" + done + die + fi + fi + + # Determine choice width - longest choice + 6 chars + # to account for 'xxx) ' + cwidth=$((cwidth + 5)) + + # Determine maximum amount of choices per row + if [ $cperrow -eq -1 ]; then + if [ $nchoices -le 3 ]; then + cperrow=1 + else + cols=$(tput cols) + cperrow=$((cols / cwidth)) + fi + fi + + # Reduce choices per row to minimise (nchoices % cperrow). + # ie. try to make list of choices as close to a + # grid shape as possible. + # + bestval=$(( cperrow - (nchoices % cperrow) )) + n=$((cperrow - 1)) + while [ $n -ge 1 ]; do + thisval=$(( cperrow - (nchoices % n) )) + if [ $thisval -lt $bestval ]; then + bestval=$thisval + cperrow=$n + fi + n=$((n - 1)) + done + + # Determine how many rows we'll need for a + # vertical layout (ie. counting downwards instead + # of right). + if [ "$layout" == "v" ]; then + choicerows=$((nchoices / cperrow)) + if [ $((nchoices % cperrow)) -gt 0 ]; then + choicerows=$((choicerows + 1)) + fi + fi + + # printf format string + choiceformat="${GREEN}${BOLD}%3s${PLAIN}${GREEN}) %-$((cwidth-5))s$PLAIN" + + answer="" + # Prompt for a choice until we get a valid answer. + while [ -z "$answer" ]; do + # Display menu + csecho "$GREEN" "^b$question^p" + + if [ "$layout" == "h" ]; then + for n in ${!choice[@]}; do + if [ -n "${desc[$n]}" ]; then + thistext="${choice[$n]} (${desc[$n]})" + else + thistext="${choice[$n]}" + fi + + thisnum=$((n + 1)) + if [ "${choice[$n]}" == "${default}" ]; then + thisnum="*$thisnum" + fi + + printf "$choiceformat" "$thisnum" "${thistext}" + if [ $(( (n + 1) % $cperrow )) -eq 0 ]; then + printf "\n" # newline + fi + done + else # ie. vertical + n=0 + for (( y=1; y<=$choicerows; y++)); do + for (( x=0; x<$cperrow; x++)); do + idx=$((n + (x * choicerows))) + if [ $idx -lt $nchoices ]; then + + if [ -n "${desc[$idx]}" ]; then + thistext="${choice[$idx]} (${desc[$idx]})" + else + thistext="${choice[$idx]}" + fi + + thisnum=$((idx + 1)) + if [ "${choice[$idx]}" == "${default}" ]; then + thisnum="*$thisnum" + fi + + printf "$choiceformat" "$thisnum" "${thistext}" + fi + done + n=$((n + 1)) + printf "\n" # newline + done + fi + + printf "\n" + if [ -n "$default" ]; then + csecho "$GREEN" " ^i(* denotes default)^p" + fi + csecho -n "$GREEN" "^b-> ^p" + + # If there's only one answer, just select it. Useful for + # cases where we are basing our list of choices on a dynamic variable. + if [ $autoselect -eq 1 -a $nchoices -eq 1 ]; then + answernum=1 + csecho "$GREEN" "1 (autoselect)" # make it look like we typed it + else + echo -e -n "$GREEN" + read answernum + echo -e -n "$PLAIN" + fi + + if [ -z "$answernum" ]; then + if [ -n "$default" ]; then + answer=$default + info "[defaulting to $answer]" + + # populate 'answernum' correctly + for z in ${!choice[@]}; do + if [[ ${choice[$z]} == $answer ]]; then + answernum=$((z + 1)) + break + fi + done + + else + csecho "$RED" "Invalid response.\n" + fi + elif [[ ! $answernum =~ ^[0-9]*$ ]] ; then + # try to match based on name + found=0 + shopt -s nocasematch + for z in ${!choice[@]}; do + if [[ ${choice[$z]} =~ $answernum ]]; then + possanswernum[$found]=$((z + 1)) + possanswer[$found]=${choice[z]} + found=$((found + 1)) + fi + done + if [[ $found -eq 1 ]]; then + selectedposs=${possanswer[0]} + selectedpossnum=${possanswernum[0]} + answerhilite=${selectedposs/${answernum}/^b${answernum}^p} + star="" + if [[ -z $BOLD ]]; then + star="*" + fi + csecho "$CYAN" "Matched '${star}${answerhilite}${star}'.\n" + answernum="$selectedpossnum" + answer="$selectedposs" + elif [[ $found -gt 1 ]]; then + csecho "${RED}" "Invalid response - '^b$answernum^p' matches $found answers.\n" + for z in ${!possanswer[@]}; do + csecho "$RED" " ${possanswernum[$z]}) ${possanswer[$z]}" + done + else + csecho "${RED}" "Invalid response.\n" + fi + shopt -u nocasematch + elif [ $answernum -lt 1 -o $answernum -gt $nchoices ]; then + csecho "$RED" "Choice out of range.\n" + else + answer=${choice[$((answernum - 1))]} + fi + done + + answer="${answer//\'/\\\'}" + eval "$retvar=$'$answer'" + eval "$retvarnum='$answernum'" + return 0 +} + function checkreqs() { # appname cpus ram_in_gb disk_in_gb local rv=0 appname errs x appname="$1" @@ -599,6 +613,20 @@ function dnslookup() { # $1 = a_record_to_look_up return $rv } +function profile() { + local now str diff + [[ -z $__PROFILE || $__PROFILE -ne 1 ]] && return; + #now=$(date +%Y/%m/%d-%H:%M:%S) + now=$(printf "%0.f" "$(bc <<<"$(gdate +"%s.%N")*1000")") + str="^b${FUNCNAME[1]}()^p ===> ^b$*^p" + if [[ -n $__PROF_PREV ]]; then + diff=$(echo "scale=2; ($now - $__PROF_PREV)" | bc) + str="$str $GREEN($BOLD+$diff ms$PLAIN$GREEN)^p" + fi + cecho -s "${MAGENTA}" "$str" >&2 + __PROF_PREV=$now +} + #declare -fx $(bash -c "source $0 &> /dev/null; compgen -A function") declare -fx $(compgen -A function | grep -v ^_)