From dea1b2881c4baec1316092a1d2b9d68ffb4e4ddc Mon Sep 17 00:00:00 2001 From: Rob Pearce Date: Sun, 13 Mar 2022 10:04:40 +1100 Subject: [PATCH] Initial code checkin --- README.md | 14 +- bashtools.sh | 570 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 577 insertions(+), 7 deletions(-) create mode 100755 bashtools.sh diff --git a/README.md b/README.md index 0b50cef..d063502 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # bashtools -Collections of bash functions covering: - -* Coloured messages -* Spinner -* Text input -* Selection from a list -* DNS lookups +Collections of bash functions covering: + + * Coloured messages + * Spinner + * Text input + * Selection from a list + * DNS lookups diff --git a/bashtools.sh b/bashtools.sh new file mode 100755 index 0000000..9744159 --- /dev/null +++ b/bashtools.sh @@ -0,0 +1,570 @@ +#!/bin/bash +$(return 2>/dev/null) +rv="$?" +if [[ $rv -ne 0 ]]; then + echo "ERROR: This script should be sourced ('. $0') rather than run directly." + exit 1; +fi + +if [[ $1 != "reload" && -n $HAVE_BASHTOOLS ]]; then + return +fi +HAVE_BASHTOOLS=1 + +BASHTOOLS_DIR="${HOME}/.bashtools" + +SPINNERFRAMES='|/-\' +SPINNERFILE="$BASHTOOLS_DIR/spinner.pid" + +[[ ! -e $BASHTOOLS_DIR ]] && mkdir "$BASHTOOLS_DIR" +[[ -e $SPINNERFILE ]] && rm -f "${SPINNERFILE}" + +BOLD="\033[1m" +PLAIN="\033[0m" +ITALIC="\033[3m" +STRIKE="\033[9m" +UNDERLINE="\033[4m" +RED="\033[31m" +YELLOW="\033[33m" +GREEN="\033[32m" +BLUE="\033[34m" +MAGENTA="\033[35m" +CYAN="\033[36m" +ORANGE="${PLAIN}\033[38;2;255;165;0m" +ORANGEBOLD="${BOLD}\033[38;2;255;220;0m" + +MAGENTARGB="${PLAIN}\033[38;2;208;65;126m" +MAGENTARGBBOLD="${BOLD}\033[38;2;255;135;196m" + +INFORMCOL="$ORANGE" +INFORMCOLB="$ORANGEBOLD" +NOTIFYCOL="$MAGENTARGB" +NOTIFYCOLB="$MAGENTARGBBOLD" + +GREY="\033[38;2;110;110;110m" +LINK="$BLUE$UNDERLINE" + +UNDERLINE_PRINTED=$(echo -en "$UNDERLINE") +UNDERLINE_LENGTH=${#UNDERLINE_PRINTED} + +REGEXP_IP='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$' + +function noansi() { + local allcols c + allcols=$(set | egrep "\\033.*m\'" | sed 's/=.*//') + for c in $allcols; do + eval "$c=''" + done +} + +# provide specific "bold" colour +function ccecho() { # [-n] [-s] col boldcol "str" + local col bcol str minusn="" autobold=0 + + while [[ ${1:0:1} == "-" ]]; do + if [[ $1 == "-n" ]]; then + minusn="-n" + fi + if [[ $1 == "-s" ]]; then + autobold=1 + fi + shift + done + col="$1" + shift + if [[ $autobold -eq 1 ]]; then + bcol="$col$BOLD" + else + bcol="$1" + shift + fi + str="$*" + str=${str//\^b/${bcol}} + str=${str//\^p/${PLAIN}${col}} + str=${str//\^i/${col}${ITALIC}} + str=${str//\^u/${col}${UNDERLINE}} + str=${str//\^s/${col}${STRIKE}} + + echo $minusn -e "${col}${str}${PLAIN}" +} + +# use bold version of supplied colour +function cecho() { # [-n] col "str" + ccecho -s $* +} + +alias try=notify +function notify() { + #echo -en "${NOTIFYCOLB}* ${NOTIFYCOL}$*...${PLAIN} " + cecho -n "$NOTIFYCOL" "$NOTIFYCOLB" "^b* ^p$*... " + if [[ $NOSPINNER -ne 1 ]]; then + start_spinner & + tput civis + fi + innotify=1 +} + +function inform() { + cecho "$INFORMCOL" "$INFORMCOLB" "^b* ^p$* " +} + +function ok() { + local msg=${*:-ok} + stop_spinner + [[ $innotify -eq 0 ]] && return 1 + innotify=0 + echo -e "$GREEN$msg$PLAIN" +} + +function fail() { + local msg=${*:-failed} + stop_spinner + [[ $innotify -eq 0 ]] && return 1 + innotify=0 + echo -e "$RED$msg$PLAIN" +} + +function partial() { + local msg=${*:-failed} + stop_spinner + [[ $innotify -eq 0 ]] && return 1 + innotify=0 + echo -e "$YELLOW$msg$PLAIN" +} + +function warn() { + cecho -s "$YELLOW" "^bWarning: ^p$*" +} + +function error() { + cecho -s "$RED" "^bERROR: ^p$*" +} + +function start_spinner() { + local idx=0 len spid + + ( + len=${#SPINNERFRAMES} + echo -n " " + while [ 1 ] ; do + echo -en "\b${SPINNERFRAMES:$idx:1}" + idx=$((idx + 1)) + [[ $idx -ge $len ]] && idx=0 + sleep 0.1 + done + ) 2>/dev/null & + spid=$! + echo $spid >>"$SPINNERFILE" +} + +function stop_spinner() { + local f pid + + [[ $NOSPINNER -eq 1 ]] && return 0 + + [[ ! -e "$SPINNERFILE" ]] && return 1 + + while read -r pid ; do + if [[ $pid = *[[:digit:]]* ]]; then + { kill $pid && wait $pid; } 2>/dev/null + echo -en "\b " >/dev/stderr + fi + done < "$SPINNERFILE" + rm -f "$SPINNERFILE" + tput cnorm +} + +function getsysstats() { + CPUS=$(/usr/bin/grep ^processor /proc/cpuinfo | /usr/bin/wc -l | /usr/bin/bc) + MEM_GB=$(dmidecode -t memory | awk 'BEGIN { tot=0 } /Size: [0-9]/ { tot += $2 } END { print tot }') + MEM_MB=$(echo "$MEM_GB * 1024" | bc ) + DISKFREE_MB=$(df -m /data | grep -v Filesystem | awk '{ print $4 }') + 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 + readopts="-s" + shift + fi + prompt="$1" + default="$2" + retvar="$3" + cecho -n -s "$YELLOW" "$1 " + read $readopts answer + [[ -z $answer ]] && answer="$default" + eval "$retvar=\"$answer\"" +} + +function checkreqs() { # appname cpus ram_in_gb disk_in_gb + local rv=0 appname errs x + appname="$1" + shift + getsysstats + try "Checking system requirements" + if [[ $CPUS -lt $1 ]]; then + errs+=("- CPUs required: ^b${1}^p (system has $CPUS)") + fi + if [[ $MEM_GB -lt $2 ]]; then + errs+=("- RAM required: ^b${2} GB^p (system has ${MEM_GB} GB)") + fi + if [[ $DISKFREE_GB -lt $3 ]]; then + errs+=("- Disk space required: ^b${3} GB^p (system has ${DISKFREE_GB} GB)") + fi + if [[ -z $errs ]]; then + ok + rv=0 + else + fail + error "This system does not meet the requirements for ^b${appname}^p." + for x in ${!errs[@]}; do + cecho -s "$RED" " ${errs[$x]}" + done + + rv=1 + + fi + return $rv +} + +function generate_ssl_cert() { # usage: $0 fqdn (populates global $sslcert and $sslkey ) + local ssp name + local country state loc org orgunit email cn + name="$1" + + sslcert="" + sslkey="" + + [[ -z $name ]] && return 1; + + sslp=$(cat /proc/sys/kernel/random/uuid) + + cd /etc/ssl/certs/ + openssl genrsa -aes128 -passout pass:${sslp} 2048 > $name.key + openssl rsa -in $name.key -passin pass:${sslp} -out $name.key + + country=AU + state=Empty + loc=Empty + org=NTT + orgunit=IT + email=root@localhost + cn=${fqdn} + + + openssl req -utf8 -new -key $name.key -out $name.csr -subj "/C=$country/ST=$state/L=$loc/O=$org/OU=orgunit/CN=$cn/emailAddress=$email" + + # note: change this later to: + # 1. use our CA + # 2. not last for 1000 years + openssl x509 -in $name.csr -out $name.crt -req -signkey $name.key -days 36500 + chmod 600 $name.key + + # Set globals + sslcert=/etc/ssl/certs/$name.crt + sslkey=/etc/ssl/certs/$name.key + + cd - >/dev/null 2>&1 + + return 0 +} + +function dnslookup() { # $1 = a_record_to_look_up + local answer rv ip digargs="" + if [[ $ip =~ $REGEXP_IP ]]; then + digargs="-x" + fi + answer=$(dig +short $digargs ${1} 2>/dev/null) + rv=$? + answer=${answer%.} + echo "$answer" + return $rv +} +