#! /usr/bin/env bash # ___________________________________________________________________________ # # # # BashLIB -- A library for Bash scripting convenience. # # # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # ___________________________________________________________________________ # # # # # # Copyright 2007-2013, lhunath # # * http://www.lhunath.com # # * Maarten Billemont # # # # ______________________________________________________________________ # | | # | .: TABLE OF CONTENTS :. | # |______________________________________________________________________| # # chr decimal # Outputs the character that has the given decimal ASCII value. # # ord character # Outputs the decimal ASCII value of the given character. # # hex character # Outputs the hexadecimal ASCII value of the given character. # # unhex character # Outputs the character that has the given decimal ASCII value. # # max numbers... # Outputs the highest of the given numbers. # # min numbers... # Outputs the lowest of the given numbers. # # totime "YYYY-MM-DD HH:MM:SS.mmm"... # Outputs the number of milliseconds in the given date string(s). # # exists application # Succeeds if the application is in PATH and is executable. # # eol message # Return termination punctuation for a message, if necessary. # # hr pattern [length] # Outputs a horizontal ruler of the given length in characters or the terminal column length otherwise. # # cloc # Outputs the current cursor location as two space-separated numbers: row column. # # readwhile command [args] # Outputs the characters typed by the user into the terminal's input buffer while running the given command. # # log [format] [arguments...] # Log an event at a certain importance level. # The event is expressed as a printf(1) format argument. # # ask [-c optionchars|-d default] [-s|-S maskchar] message... # Ask a question and read the user's reply to it. Then output the result on stdout. # # trim lines ... # Trim the whitespace off of the beginning and end of the given lines. # # reverse [-0|-d delimitor] [elements ...] [<<< elements] # Reverse the order of the given elements. # # order [-0|-d char] [-[cC] comparator|-n] [-t number] [elements ...] [<<< elements] # Orders the elements in ascending order. # # mutex file # Open a mutual exclusion lock on the file, unless another process already owns one. # # pushjob [poolsize] command # Start an asynchronous command within a pool, waiting for space in the pool if it is full. # # fsleep time # Wait for the given (fractional) amount of seconds. # # showHelp name description author [option description]... # Generate a prettily formatted usage description of the application. # # shquote [-e] [argument...] # Shell-quote the arguments to make them safe for injection into bash code. # # requote [string] # Escape the argument string to make it safe for injection into a regex. # # shorten [-p pwd] path [suffix]... # Shorten an absolute path for pretty printing. # # up .../path|num # Walk the current working directory up towards root num times or until path is found. # # buildarray name terms... -- elements... # Create an array by adding all the terms to it for each element, replacing {} terms by the element. # # inArray element array # Checks whether a certain element is in the given array. # # xpathNodes query [files...] # Outputs every xpath node that matches the query on a separate line. # # hideDebug [on|off] # Toggle Bash's debugging mode off temporarily. # # stackTrace # Output the current script's function execution stack. # _tocHash=71e13f42e1ea82c1c7019b27a3bc71f3 # ______________________________________________________________________ # | | # | .: GLOBAL CONFIGURATION :. | # |______________________________________________________________________| # Unset all exported functions. Exported functions are evil. while read _ _ func; do command unset -f "$func" done < <(command declare -Fx) { shopt -s extglob shopt -s globstar } 2>/dev/null ||: # Generate Table Of Contents genToc() { local line= comments=() usage= whatis= lineno=0 out= outhash= outline= while read -r line; do (( ++lineno )) [[ $line = '#'* ]] && comments+=("$line") && continue [[ $line = +([[:alnum:]])'() {' ]] && IFS='()' read func _ <<< "$line" && [[ $func != $FUNCNAME ]] && { usage=${comments[3]##'#'+( )} whatis=${comments[5]##'#'+( )} [[ $usage = $func* && $whatis = *. ]] || err "Malformed docs for %s (line %d)." "$func" "$lineno" printf -v outline '# %s\n# %s\n#\n' "$usage" "$whatis" out+=$outline } comments=() done < ~/.bin/bashlib outhash=$(openssl md5 <<< "$out") if [[ $_tocHash = "$outhash" ]]; then inf 'Table of contents up-to-date.' else printf '%s' "$out" printf '_tocHash=%q' "$outhash" wrn 'Table of contents outdated.' fi } # ______________________________________________________________________ # | | # | .: GLOBAL DECLARATIONS :. | # |______________________________________________________________________| # Environment TMPDIR=${TMPDIR:-/tmp} TMPDIR=${TMPDIR%/} # Variables for convenience sequences. bobber=( '.' 'o' 'O' 'o' ) spinner=( '-' \\ '|' '/' ) crosser=( '+' 'x' '+' 'x' ) runner=( '> >' \ '>> ' \ ' >>' ) # Variables for terminal requests. [[ -t 2 && $TERM != dumb ]] && { COLUMNS=$({ tput cols || tput co;} 2>&3) # Columns in a line LINES=$({ tput lines || tput li;} 2>&3) # Lines on screen alt=$( tput smcup || tput ti ) # Start alt display ealt=$( tput rmcup || tput te ) # End alt display hide=$( tput civis || tput vi ) # Hide cursor show=$( tput cnorm || tput ve ) # Show cursor save=$( tput sc ) # Save cursor load=$( tput rc ) # Load cursor dim=$( tput dim || tput mh ) # Start dim bold=$( tput bold || tput md ) # Start bold stout=$( tput smso || tput so ) # Start stand-out estout=$( tput rmso || tput se ) # End stand-out under=$( tput smul || tput us ) # Start underline eunder=$( tput rmul || tput ue ) # End underline reset=$( tput sgr0 || tput me ) # Reset cursor blink=$( tput blink || tput mb ) # Start blinking italic=$( tput sitm || tput ZH ) # Start italic eitalic=$( tput ritm || tput ZR ) # End italic [[ $TERM != *-m ]] && { red=$( tput setaf 1|| tput AF 1 ) green=$( tput setaf 2|| tput AF 2 ) yellow=$( tput setaf 3|| tput AF 3 ) blue=$( tput setaf 4|| tput AF 4 ) magenta=$( tput setaf 5|| tput AF 5 ) cyan=$( tput setaf 6|| tput AF 6 ) } black=$( tput setaf 0|| tput AF 0 ) white=$( tput setaf 7|| tput AF 7 ) default=$( tput op ) eed=$( tput ed || tput cd ) # Erase to end of display eel=$( tput el || tput ce ) # Erase to end of line ebl=$( tput el1 || tput cb ) # Erase to beginning of line ewl=$eel$ebl # Erase whole line draw=$( tput -S <<< ' enacs smacs acsc rmacs' || { \ tput eA; tput as; tput ac; tput ae; } ) # Drawing characters back=$'\b' } 3>&2 2>/dev/null ||: # ______________________________________________________________________ # | | # | .: FUNCTION DECLARATIONS :. | # |______________________________________________________________________| # ______________________________________________________________________ # |__ Chr _______________________________________________________________| # # chr decimal # # Outputs the character that has the given decimal ASCII value. # chr() { printf "\\$(printf '%03o' "$1")" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Ord _______________________________________________________________| # # ord character # # Outputs the decimal ASCII value of the given character. # ord() { printf '%d' "'$1" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Hex _______________________________________________________________| # # hex character # # Outputs the hexadecimal ASCII value of the given character. # hex() { printf '%x' "'$1" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Unhex _______________________________________________________________| # # unhex character # # Outputs the character that has the given hexadecimal ASCII value. # unhex() { printf "\\x$1" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ max _______________________________________________________________| # # max numbers... # # Outputs the highest of the given numbers. # max() { local max=$1 n for n; do (( n > max )) && max=$n done printf %d "$max" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ min _______________________________________________________________| # # min numbers... # # Outputs the lowest of the given numbers. # min() { local min=$1 n for n; do (( n < min )) && min=$n done printf '%d' "$min" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ si ________________________________________________________________| # # si number # # Output a human-readable version of the number using SI units. # si() { local number=$1 if (( number >= 1000000000000000 )); then printf '%dM' "$((number / 1000000000000000))" elif (( number >= 1000000000000 )); then printf '%dM' "$((number / 1000000000000))" elif (( number >= 1000000000 )); then printf '%dM' "$((number / 1000000000))" elif (( number >= 1000000 )); then printf '%dM' "$((number / 1000000))" elif (( number >= 1000 )); then printf '%dk' "$((number / 1000))" else printf '%d' "$number"; fi } # _____________________________________________________________________ # ______________________________________________________________________ # |__ totime ____________________________________________________________| # # totime "YYYY-MM-DD HH:MM:SS.mmm"... # # Outputs the number of milliseconds in the given date string(s). # # When multiple date string arguments are given, multiple time strings are output, one per line. # # The fields should be in the above defined order. The delimitor between the fields may be any one of [ -:.]. # If a date string does not follow the defined format, the result is undefined. # # Note that this function uses a very simplistic conversion formula which does not take any special calendar # convenions into account. It assumes there are 12 months in evert year, 31 days in every month, 24 hours # in every day, 60 minutes in every hour, 60 seconds in every minute and 1000 milliseconds in every second. # totime() { local arg time year month day hour minute second milli for arg; do IFS=' -:.' read year month day hour minute second milli <<< "$arg" && (( time = (((((((((((10#$year * 12) + 10#$month) * 31) + 10#$day) * 24) + 10#$hour) * 60) + 10#$minute) * 60) + 10#$second) * 1000) + 10#$milli )) && printf '%d\n' "$time" done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Exists ____________________________________________________________| # # exists application # # Succeeds if the application is in PATH and is executable. # exists() { [[ -x $(type -P "$1" 2>/dev/null) ]] } # _____________________________________________________________________ # ______________________________________________________________________ # |__ FirstExists ____________________________________________________________| # # firstExists file... # # Outputs the first of the arguments that is a file which exists. # firstExists() { local file; for file; do [[ -e "$file" ]] && printf %s "$file" && exit done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Eol _______________________________________________________________| # # eol message # # Return termination punctuation for a message, if necessary. # eol() { : #[[ $1 && $1 != *[\!\?.,:\;\|] ]] && printf .. ||: } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Hr _______________________________________________________________| # # hr pattern [length] # # Outputs a horizontal ruler of the given length in characters or the terminal column length otherwise. # The ruler is a repetition of the given pattern string. # hr() { local pattern=${1:--} length=${2:-$COLUMNS} ruler= while (( ${#ruler} < length )); do ruler+=${pattern:0:length-${#ruler}} done printf %s "$ruler" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ CLoc ______________________________________________________________| # # cloc # # Outputs the current cursor location as two space-separated numbers: row column. # cloc() { local old=$(stty -g) trap 'stty "$old"' RETURN stty raw # If the tty has input waiting then we can't read back its response. We'd only break and pollute the tty input buffer. read -t 0 < /dev/tty 2>/dev/null && return 1 printf '\e[6n' > /dev/tty IFS='[;' read -dR _ row col < /dev/tty printf '%d %d' "$row" "$col" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ readwhile ______________________________________________________________| # # readwhile command [args] # # Outputs the characters typed by the user into the terminal's input buffer while running the given command. # readwhile() { local old=$(stty -g) in result REPLY trap 'stty "$old"' RETURN stty raw "$@" result=$? while read -t 0; do IFS= read -rd '' -n1 && in+=$REPLY done printf %s "$in" return $result } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Latest ____________________________________________________________| # # latest [file...] # # Output the argument that represents the file with the latest modification time. # latest() ( shopt -s nullglob local file latest=$1 for file; do [[ $file -nt $latest ]] && latest=$file done printf '%s\n' "$latest" ) # _____________________________________________________________________ # _______________________________________________________________________ # |__ Iterate ____________________________________________________________| # # iterate [command] # # All arguments to iterate make up a single command that will be executed. # # Any of the arguments may be of the format {x..y[..z]} which causes the command # to be executed in a loop, each iteration substituting the argument for the # current step the loop has reached from x to y. We step from x to y by # walking from x's position in the ASCII character table to y's with a step of z # or 1 if z is not specified. # iterate() ( local command=( "$@" ) iterationCommand=() loop= a= arg= current=() step=() target=() for a in "${!command[@]}"; do arg=${command[a]} if [[ $arg = '{'*'}' ]]; then loop=${arg#'{'} loop=${loop%'}'} step[a]=${loop#*..*..} current[a]=${loop%%..*} target[a]=${loop#*..} target[a]=${target[a]%.."${step[a]}"} [[ ! ${step[a]} || ${step[a]} = $loop ]] && step[a]=1 fi done if (( ${#current[@]} )); then for loop in "${!current[@]}"; do while true; do iterationCommand=() for a in "${!command[@]}"; do (( a == loop )) \ && iterationCommand+=( "${current[a]}" ) \ || iterationCommand+=( "${command[a]}" ) done iterate "${iterationCommand[@]}" [[ ${current[loop]} = ${target[loop]} ]] && break current[loop]="$(chr "$(( $(ord "${current[loop]}") + ${step[loop]} ))")" done done else "${command[@]}" fi ) # _____________________________________________________________________ # _______________________________________________________________________ # |__ csvline ____________________________________________________________| # # csvline [-d delimiter] [-D line-delimiter] # # Parse a CSV record from standard input, storing the fields in the CSVLINE array. # # By default, a single line of input is read and parsed into comma-delimited fields. # Fields can optionally contain double-quoted data, including field delimiters. # # A different field delimiter can be specified using -d. You can use -D # to change the definition of a "record" (eg. to support NULL-delimited records). # csvline() { CSVLINE=() local line field quoted=0 delimiter=, lineDelimiter=$'\n' c local OPTIND=1 arg while getopts :d: arg; do case $arg in d) delimiter=$OPTARG ;; esac done IFS= read -d "$lineDelimiter" -r line || return while IFS= read -rn1 c; do case $c in \") (( quoted = !quoted )) continue ;; $delimiter) if (( ! quoted )); then CSVLINE+=( "$field" ) field= continue fi ;; esac field+=$c done <<< "$line" [[ $field ]] && CSVLINE+=( "$field" ) ||: } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Logging ___________________________________________________________| # # log format [arguments...] # # Log an event at a certain importance level. The event is expressed as a printf(1) format argument. # The current exit code remains unaffected by the execution of this function. # # Instead of 'log', you can use a level as command name, to log at that level. Using log, messages are # logged at level inf. The supported levels are: trc, dbg, inf, wrn, err, ftl. # # If you prefix the command name with a p, the log message is shown as a spinner and waits for the next # closing statement. Eg. # # pinf 'Converting image' # convert image.png image.jpg # fnip # # The closing statement (here fnip) is the reverse of the opening statement and exits with the exit code # of the last command. If the last command failed, it shows the exit code in the spinner before it is stopped. # The closing statement also takes a format and arguments, which are displayed in the spinner. # log() { local exitcode=$? result=0 level=${level:-inf} supported=0 end=$'\n' type=msg conMsg= logMsg= format= colorFormat= date= info= arg= args=() colorArgs=() ruler= # Handle options. local OPTIND=1 while getopts :tpuPrR:d:nx arg; do case $arg in p) end='.. ' type=startProgress ;; u) end='.. ' type=updateProgress ;; P) type=stopProgress ;; r) ruler='____' ;; R) ruler=$OPTARG ;; d) end=$OPTARG ;; n) end= ;; x) result=$exitcode ;; esac done shift "$((OPTIND-1))" format=$1 args=( "${@:2}" ) (( ! ${#args[@]} )) && [[ $format ]] && { args=("$format") format=%s; local bold=; } date=${_logDate+$(date +"${_logDate:-%H:%M}")} # Level-specific settings. local logLevelColor case $level in TRC) (( supported = _logVerbosity >= 4 )) logLevelColor=$_logTrcColor ;; DBG) (( supported = _logVerbosity >= 3 )) logLevelColor=$_logDbgColor ;; INF) (( supported = _logVerbosity >= 2 )) logLevelColor=$_logInfColor ;; WRN) (( supported = _logVerbosity >= 1 )) logLevelColor=$_logWrnColor ;; ERR) (( supported = _logVerbosity >= 0 )) logLevelColor=$_logErrColor ;; FTL) (( supported = 1 )) logLevelColor=$_logFtlColor ;; *) log FTL 'Log level %s does not exist' "$level" exit 1 ;; esac (( ! supported )) && return "$result" local logColor=${_logColor:+$logLevelColor} # Generate the log message. case $type in msg|startProgress) printf -v logMsg "${date:+%s }${_logLevel:+%-3s }$format$end" ${date:+"$date"} ${_logLevel:+"$level"} "${args[@]}" if (( _logColor )); then colorFormat=$(sed ${reset:+-e "s/$(requote "$reset")/$reset$_logAttributes$logColor/g"} -e "s/%[^a-z]*[a-z]/$reset$_logAttributes$bold$logColor&$reset$_logAttributes$logColor/g" <<< "$format") colorArgs=("${args[@]//$reset/$reset$_logAttributes$bold$logColor}") printf -v conMsg "$reset$_logAttributes${date:+%s }${_logLevel:+$logColor$bold%-3s$reset $_logAttributes}$logColor$colorFormat$reset$_logAttributes$black\$$reset$end$save" ${date:+"$date"} ${_logLevel:+"$level"} "${colorArgs[@]}" else conMsg=$logMsg fi ;; updateProgress) printf -v logMsg printf " [$format]" "${args[@]}" if (( _logColor )); then colorFormat=$(sed ${reset:+-e "s/$(requote "$reset")/$reset$_logAttributes$logColor/g"} -e "s/%[^a-z]*[a-z]/$reset$_logAttributes$bold$logColor&$reset$_logAttributes$logColor/g" <<< "$format") colorArgs=("${args[@]//$reset/$reset$_logAttributes$bold$logColor}") printf -v conMsg "$load$eel$blue$bold[$reset$_logAttributes$logColor$colorFormat$reset$_logAttributes$blue$bold]$reset$end" "${colorArgs[@]}" else conMsg=$logMsg fi ;; stopProgress) kill -0 "$_logSpinner" 2>/dev/null || return case $exitcode in 0) printf -v logMsg "done${format:+ ($format)}.\n" "${args[@]}" if (( _logColor )); then colorFormat=$(sed ${reset:+-e "s/$(requote "$reset")/$reset$logColor/g"} -e "s/%[^a-z]*[a-z]/$reset$bold$logColor&$reset$logColor/g" <<< "$format") colorArgs=("${args[@]//$reset/$reset$bold$logColor}") printf -v conMsg "$load$eel$green${bold}done${colorFormat:+ ($reset$logColor$colorFormat$reset$green$bold)}$reset.\n" "${colorArgs[@]}" else conMsg=$logMsg fi ;; *) info=${format:+$(printf ": $format" "${args[@]}")} printf -v logMsg "error(%d%s).\n" "$exitcode" "$info" if (( _logColor )); then printf -v conMsg "${eel}${red}error${reset}(${bold}${red}%d${reset}%s).\n" "$exitcode" "$info" else conMsg=$logMsg fi ;; esac ;; esac # Create the log file. if [[ $_logFile && ! -e $_logFile ]]; then [[ $_logFile = */* ]] || _logFile=./$_logFile mkdir -p "${_logFile%/*}" && touch "$_logFile" fi # Stop the spinner. if [[ $type = stopProgress && $_logSpinner ]]; then { kill "$_logSpinner" ||: wait "$_logSpinner" ||: unset _logSpinner } 2>/dev/null fi # Output the ruler. if [[ $ruler ]]; then printf >&2 '%s\n' "$(hr "$ruler")" [[ -w $_logFile ]] \ && printf >> "$_logFile" '%s' "$ruler" fi # Output the log message. printf >&2 '%s' "$conMsg" [[ -w $_logFile ]] \ && printf >> "$_logFile" '%s' "$logMsg" # Start the spinner. if [[ $type = startProgress && ! $_logSpinner && $TERM != dumb ]]; then { set +m trap 'printf >&2 %s "$show"' EXIT printf >&2 %s "$hide" while printf >&2 "$eel$blue$bold[$reset%s$reset$blue$bold]$reset\b\b\b" "${spinner[s++ % ${#spinner[@]}]}" && sleep .1 do :; done } & _logSpinner=$! addtrap EXIT 'level=%q _logSpinner=%q golp' "$level" "$_logSpinner" fi return $result } trc() { level=TRC log "$@"; } dbg() { level=DBG log "$@"; } inf() { level=INF log "$@"; } wrn() { level=WRN log "$@"; } err() { level=ERR log "$@"; } ftl() { level=FTL log "$@"; } plog() { log -p "$@"; } ulog() { log -u "$@"; } golp() { log -P "$@"; } ptrc() { level=TRC plog "$@"; } pdbg() { level=DBG plog "$@"; } pinf() { level=INF plog "$@"; } pwrn() { level=WRN plog "$@"; } perr() { level=ERR plog "$@"; } pftl() { level=FTL plog "$@"; } utrc() { level=TRC ulog "$@"; } udbg() { level=DBG ulog "$@"; } uinf() { level=INF ulog "$@"; } uwrn() { level=WRN ulog "$@"; } uerr() { level=ERR ulog "$@"; } uftl() { level=FTL ulog "$@"; } gtrc() { level=trc golp "$@"; } gbdp() { level=DBG golp "$@"; } fnip() { level=INF golp "$@"; } nrwp() { level=WRN golp "$@"; } rrep() { level=ERR golp "$@"; } ltfp() { level=FTL golp "$@"; } _logColor=${_logColor:-$([[ -t 2 ]] && echo 1)} _logVerbosity=2 _logTrcColor=$grey _logDbgColor=$blue _logInfColor=$white _logWrnColor=$yellow _logErrColor=$red _logFtlColor=$bold$red #_logDate=%H:%M # Set this to enable date output in log messages. #_logLevel=1 # Set this to enable level output in log messages. # _______________________________________________________________________ # ______________________________________________________________________ # |__ Ask _______________________________________________________________| # # ask [-c optionchars|-d default] [-s|-S maskchar] format [arguments...] # # Ask a question and read the user's reply to it. Then output the result on stdout. # # When in normal mode, a single line is read. If the line is empty and # -d was specified, the default argument is output instead of an empty line. # The exit code is always 0. # # When in option mode (-c), the user is shown the option characters with # which he can reply and a single character is read. # If the reply is empty (user hits enter) and any of the optionchars are # upper-case, the upper-case option (= the default option) character will # be output instead of an empty line. # If the reply character is not amoungst the provided options the default # option is again output instead if present. If no default was given, an # exit code of 2 is returned. # You may mark an optionchar as 'valid' by appending a '!' to it. As a # result, an exit code of 0 will only be returned if this valid option # is replied. If not, an exit code of 1 will be returned. # ask() { # Initialize the vars. local opt arg local option= local options= local default= local silent= local valid= local muteChar= local format= # Parse the options. local OPTIND=1 while getopts :sS:c:d: opt; do case $opt in s) silent=1 ;; S) silent=1 muteChar=$OPTARG ;; c) while read -n1 arg; do case $arg in [[:upper:]]) default=$arg ;; !) valid=${options: -1}; continue ;; esac options+=$arg done <<< "$OPTARG" ;; d) default=$OPTARG option=$default ;; esac done # Trim off the options. shift $((OPTIND-1)) # Figure out what FD to use for our messages. [[ -t 1 ]] && local fd=1 || local fd=2 # Ask the question. format=$1; shift level=${level:-WRN} log -n "$format${option:+ [%s]}${options:+ [%s]}" "$@" ${option:+"$option"} ${options:+"$options"} # Read the reply. exec 8<&0; [[ -t 8 ]] || exec 8&$fd done REPLY=$reply [[ $options && $REPLY ]] || (( silent )) && printf '\n' >&$fd else read -u8 -e ${options:+-n1} ${silent:+-s} fi # Evaluate the reply. while true; do if [[ $REPLY && ( ! $options || $options = *$REPLY* ) ]]; then if [[ $valid ]] then [[ $REPLY = $valid ]] else printf "%s" "$REPLY" fi return fi [[ -z $default || $REPLY = $default ]] \ && return 2 REPLY=$default done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Trim ______________________________________________________________| # # trim lines ... # # Trim the whitespace off of the beginning and end of the given lines. # Each argument is considdered one line; is treated and printed out. # # When no arguments are given, lines will be read from standard input. # trim() { { (( $# )) && printf '%s\n' "$@" || cat; } | \ sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Reverse ___________________________________________________________| # # reverse [-0|-d delimitor] [elements ...] [<<< elements] # # Reverse the order of the given elements. # Elements are read from command arguments or standard input if no element # arguments are given. # They are reversed and output on standard output. # # If the -0 option is given, input and output are delimited by NUL bytes. # If the -d option is given, input and output are delimited by the # character argument. # Otherwise, they are delimited by newlines. # reverse() { # Initialize the vars. local elements=() delimitor=$'\n' i # Parse the options. local OPTIND=1 while getopts :0d: opt; do case $opt in 0) delimitor=$'\0' ;; d) delimitor=$OPTARG ;; esac done shift "$((OPTIND-1))" # Get the elements. if (( $# )); then elements=( "$@" ) else while IFS= read -r -d "$delimitor"; do elements+=("$REPLY") done fi # Iterate in reverse order. for (( i=${#elements[@]} - 1; i >=0; --i )); do printf "%s${delimitor:-'\0'}" "${elements[i]}" done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Order _____________________________________________________________| # # order [-0|-d char] [-[fF] isDesired] [-[cC] comparator|-n|-R|-t] [-r] [-T number] [-a array|elements ...] [<<< elements] # # Orders the elements in ascending order. # Elements are read from command arguments or standard input if no element # arguments are given. # The result is output on standard output. # # By default, the elements will be ordered using lexicographic comparison. # If the -n option is given, the elements will be ordered numerically. # If the -R option is given, the elements will be ordered randomly. # If the -t option is given, the elements are ordered by file mtime. # If the -f option is given, the command name following it will be used # as a filter. # If the -c option is given, the command name following it will be used # as a comparator. # If the -C option is given, the bash code following it will be used # as a comparator. # If the -r option is given, the ordering will be reversed. # If the -T option is given, only the first number results are returned. # If the -a option is given, the elements in array are ordered instead and # array is mutated to contain the result. # If number is 0, all results are returned. # # isDesired is a command name which will get one parameter. The parameter # is an element which will only be included if the command exits successfully. # comparator is a command name which will be executed for each element # comparison and will be passed two element arguments. The command should # succeed if the first argument is less than the second argument for the # purpose of this sort. # # If the -0 option is given, input and output are delimited by NUL bytes. # If the -d option is given, input and output are delimited by the # character argument. # Otherwise, they are delimited by newlines. # # The ordering is implemented by an insertion sort algorithm. # order() { # Initialize the vars. local _delimitor=$'\n' _i _j _isDesired=true _comparator=string_ascends _comparator_ascends=1 _top=0 _arrayName= _array= # Parse the options. local OPTIND=1 while getopts :0nrRd:f:F:c:C:tT:a: opt; do case $opt in 0) _delimitor=$'\0' ;; d) _delimitor=$OPTARG ;; n) _comparator=number_ascends ;; R) _comparator=random_ascends ;; t) _comparator=mtime_ascends ;; f) _isDesired=$OPTARG ;; F) _isDesired=bash_desired _bash_desired_code=$OPTARG ;; c) _comparator=$OPTARG ;; C) _comparator=bash_ascends _bash_ascends_code=$OPTARG ;; r) _comparator_ascends=0 ;; T) _top=$OPTARG ;; a) _arrayName=$OPTARG _array=$_arrayName[@] ;; esac done shift "$((OPTIND-1))" # Get the elements. local _elements=() _element if [[ $_arrayName ]]; then for _element in "${!_array}"; do "$_isDesired" "$_element" && _elements+=("$_element") done elif (( $# )); then for _element; do "$_isDesired" "$_element" && _elements+=("$_element") done else while IFS= read -r -d "$_delimitor" _element; do "$_isDesired" "$_element" && _elements+=("$_element") done fi # Iterate in reverse order. for (( _i = 1; _i < ${#_elements[@]}; ++_i )); do for (( _j = _i; _j > 0; --_j )); do _element=${_elements[_j]} if ( (( _comparator_ascends )) && "$_comparator" "$_element" "${_elements[_j-1]}" ) || ( (( ! _comparator_ascends )) && ! "$_comparator" "$_element" "${_elements[_j-1]}" ); then _elements[_j]=${_elements[_j-1]} _elements[_j-1]=$_element fi done done (( _top )) || _top=${#_elements[@]} if [[ $_array ]]; then declare -ga "$_array=($(printf '%q ' "${_elements[@]:0:_top}"))" else printf "%s${_delimitor:-\0}" "${_elements[@]:0:_top}" fi } # _____________________________________________________________________ string_ascends() { [[ $1 < $2 ]]; } number_ascends() { (( $1 < $2 )); } random_ascends() { (( RANDOM % 2 )); } mtime_ascends() { [[ $1 -ot $2 ]]; } exists_desired() { [[ -e $1 ]]; } line_desired() { [[ $1 ]]; } code_desired() { line_desired "$1" && ! comment_desired "$1"; } comment_desired() { line_desired "$1" && [[ $1 = @(#|//|/\*)* ]]; } bash_desired() { bash -c "$_bash_desired_code" -- "$@"; } bash_ascends() { bash -c "$_bash_ascends_code" -- "$@"; } # ______________________________________________________________________ # |__ AddTrap _____________________________________________________________| # # addtrap signal command-format [args...] # # Add a command to the current commands executed when a signal is received by the bash process. # # The command-format is a printf-style format for the command to execute. The optional # args are interpolated into the command-format by bash's built-in printf. # addtrap() { local signal=$1 cmd=$2; shift 2 printf -v cmd "$cmd" "$@" read _ _ oldtrap <<< "$(trap -p "$signal")" eval "declare oldtrap=${oldtrap% *}" trap "$oldtrap${oldtrap:+; }$cmd" "$signal" } # ______________________________________________________________________ # |__ Mutex _____________________________________________________________| # # mutex file # # Open a mutual exclusion lock on the file, unless another process already owns one. # # If the file is already locked by another process, the operation fails. # This function defines a lock on a file as having a file descriptor open to the file. # This function uses FD 9 to open a lock on the file. To release the lock, close FD 9: # exec 9>&- # mutex() { local lockfile=${1:-${BASH_SOURCE[-1]}} pid pids [[ -e $lockfile ]] || err "No such file: $lockfile" || return exec 9>> "$lockfile" && [[ $({ fuser -f "$lockfile"; } 2>&- 9>&-) == $$ ]] } # ______________________________________________________________________ # |__ PushJob ___________________________________________________________| # # pushjob [poolsize] command # # Start an asynchronous command within a pool, waiting for space in the pool if it is full. # # The pool is pruned automatically as running jobs complete. This function # allows you to easily run asynchronous commands within a pool of N, # automatically starting the next command as soon as there's space. # pushjob() { local size=$1; shift 1 # Wait for space in the pool. until (( ${#jobpool[@]} < size )); do sleep 1 & pushjobsleep=$! wait "$pushjobsleep" done 2>/dev/null # Register prunejobs and start the pushed job. trap _prunejobs SIGCHLD set -m "$@" & jobpool[$!]= } _prunejobs() { # Prune all pool jobs that are no longer running. for pid in "${!jobpool[@]}"; do kill -0 "$pid" 2>/dev/null || unset "jobpool[$pid]" done # Unregister SIGCHLD if our pool is empty. (( ${#jobpool[@]} )) || trap - SIGCHLD # Wake up pushjob. kill "$pushjobsleep" 2>/dev/null } # ______________________________________________________________________ # |__ FSleep _____________________________________________________________| # # fsleep time # # Wait for the given (fractional) amount of seconds. # # This implementation solves the problem portably, assuming that either # bash 4.x or a fractional sleep(1) is available. # fsleep() { local fifo=${TMPDIR:-/tmp}/.fsleep.$$ trap 'rm -f "$fifo"' RETURN mkfifo "$fifo" && { read -t "$1" <> "$fifo" 2>/dev/null || sleep "$1"; } } # _____________________________________________________________________ # ______________________________________________________________________ # |__ options ___________________________________________________________| # # options [option description]... # # Specify and handle options in arguments. # # The 'setopt' function will be called for each option expected option # passed to the script, with $1 set to the option character and $2 # its description. Check OPTARG if the option takes an argument. # 'setopt' will be called with '?' if an invalid option is passed. # # Unless specified, the -h option will show a usage description, # explaining the options. # # Proposed usage: # setopt() { # case "$1" in # a) echo "got option a" ;; # b) echo "got option b with argument $OPTARG" ;; # esac # } # options \ # a 'option a' \ # b: 'option b with argument' # options() { # Parse the expected options and their description. declare -A options=() while (( $# )); do local optchar=$1 optdesc=$2 shift 2 || ftl 'Missing arguments, expected option (%s), description (%s).' "$optchar" "$optdesc" || exit options[$optchar]=$optdesc done # Find the script's options. local argc=${BASH_ARGC[@]: -1} argv=("${BASH_ARGV[@]: -argc}") arg local optstring=$(printf %s "${!options[@]}")h set -- # Sigh. BASH_ARGV is all backwards. for arg in "${argv[@]}"; do set -- "$arg" "$@" done # Handle the script's options. while getopts "$optstring" arg; do if [[ $arg = h && ! ${options[h]} ]]; then # Show usage message. [[ -t 1 ]]; local fd=$(( $? + 1 )) optarg # Print out the app usage. printf " Usage: $reset$bold%s$reset" "${BASH_SOURCE[1]##*/}" >&$fd for optchar in "${!options[@]}"; do [[ $optchar = *: ]] && optarg=" arg" || optarg= printf " [$bold$green-%s$reset%s]" "${optchar%:}" "$optarg" >&$fd done printf "\n\n" >&$fd # Print out the option descriptions. for optchar in "${!options[@]}"; do local optdesc=${options[$optchar]} [[ $optchar = *: ]] && optarg=" arg" || optarg= printf " $bold$green-%s$reset%s\t" "${optchar%:}" "$optarg" fmt -w "$COLUMNS" <<< "${optdesc//+( )/ }" | sed $'1!s/^/ \t/' printf "\n" done | column -t -s $'\t' >&$fd else optchar=$arg; [[ ! ${options[$arg]} && ${options[$arg:]} ]] && optchar=$arg: setopt "$arg" "${options[$arg]}" fi done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ ShowHelp __________________________________________________________| # # showHelp name description author [option description]... # # Generate a prettily formatted usage description of the application. # # name Provide the name of the application. # # description Provide a detailed description of the application's # purpose and usage. # # option An option the application can take as argument. # # description A description of the effect of the preceding option. # showHelp() { # Parse the options. local appName=$1; shift local appDesc=${1//+([[:space:]])/ }; shift local appAuthor=$1; shift local cols=$(tput cols) (( cols = ${cols:-80} - 10 )) # Figure out what FD to use for our messages. [[ -t 1 ]]; local fd=$(( $? + 1 )) # Print out the help header. printf "$reset$bold\n" >&$fd printf "\t\t%s\n" "$appName" >&$fd printf "$reset\n" >&$fd printf "%s\n" "$appDesc" | fmt -w "$cols" | sed $'s/^/\t/' >&$fd printf "\t $reset$bold~ $reset$bold%s\n" "$appAuthor" >&$fd printf "$reset\n" >&$fd # Print out the application options and columnize them. while (( $# )); do local optName=$1; shift local optDesc=$1; shift printf " %s\t" "$optName" printf "%s\n" "${optDesc//+( )/ }" | fmt -w "$cols" | sed $'1!s/^/ \t/' printf "\n" done | column -t -s $'\t' \ | sed "s/^\( [^ ]*\)/$bold$green\1$reset/" >&$fd printf "\n" >&$fd } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Quote _____________________________________________________________| # # shquote [-e] [argument...] # # Shell-quote the arguments to make them safe for injection into bash code. # # The result is bash code that represents a series of words, where each # word is a literal string argument. By default, quoting happens using # single-quotes. # # -e Use backslashes rather than single quotes. # -d Use double-quotes rather than single quotes (does NOT disable expansions!). # -a Normally, shquote doesn't quote arguments that don't need it. This forces all arguments to be quoted. # shquote() { # Initialize the defaults. local OPTIND=1 arg escape=0 sq="'\\''" dq='\"' quotedArgs=() type=single always=0 # Parse the options. while getopts :eda arg; do case $arg in e) type=escape ;; d) type=double ;; a) always=1 ;; esac done shift "$((OPTIND-1))" # Print out each argument, quoting it properly. for arg; do (( ! always )) && [[ $arg = "$(printf %q "$arg")" ]] && quotedArgs+=("$arg") && continue case "$type" in escape) quotedArgs+=("$(printf "%q" "$arg")") ;; single) arg=${arg//"'"/$sq} quotedArgs+=("$(printf "'%s'" "$arg")") ;; double) arg=${arg//'"'/$dq} quotedArgs+=("$(printf '"%s"' "$arg")") ;; esac done printf '%s\n' "$(IFS=' '; echo "${quotedArgs[*]}")" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ ReQuote __________________________________________________________| # # requote [string] # # Escape the argument string to make it safe for injection into a regex. # # The result is a regular expression that matches the literal argument # string. # requote() { # Initialize the defaults. local char printf '%s' "$1" | while IFS= read -r -d '' -n1 char; do printf '[%s]' "$char" done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Shorten ___________________________________________________________| # # shorten [-p pwd] path [suffix]... # # Shorten an absolute path for pretty printing. # Paths are shortened by replacing the homedir by ~, making it relative and # cutting off given suffixes from the end. # # -p Use the given pathname as the base for relative filenames instead of PWD. # path The path string to shorten. # suffix Suffix strings that must be cut off from the end. # Only the first suffix string matched will be cut off. # shorten() { # Parse the options. local suffix path pwd=$PWD [[ $1 = -p ]] && { pwd=$2; shift 2; } path=$1; shift # Make path absolute. [[ $path = /* ]] || path=$PWD/$path # If the path denotes something that exists; it's easy. if [[ -d $path ]] then path=$(cd "$path"; printf "%s" "$PWD") elif [[ -d ${path%/*} ]] then path=$(cd "${path%/*}"; printf "%s" "$PWD/${path##*/}") # If not, we'll try readlink -m. elif readlink -m / >/dev/null 2>&1; then path=$(readlink -m "$path") # If we don't have that - unleash the sed(1) madness. else local oldpath=/ while [[ $oldpath != $path ]]; do oldpath=$path path=$(sed -e 's,///*,/,g' -e 's,\(^\|/\)\./,\1,g' -e 's,\(^\|/\)[^/]*/\.\.\($\|/\),\1,g' <<< "$path") done fi # Replace special paths. path=${path/#$HOME/'~'} path=${path#$pwd/} # Cut off suffix. for suffix; do [[ $path = *$suffix ]] && { path=${path%$suffix} break } done printf "%s" "$path" } # _____________________________________________________________________ # ______________________________________________________________________ # |__ Up ________________________________________________________________| # # up .../path|num # # Walk the current working directory up towards root num times or until path is found. # # Returns 0 if the destination was reached or 1 if we hit root. # # Prints PWD on stdout on success. # up() { local up=0 until [[ $PWD = / ]]; do cd ../ if [[ $1 = .../* ]]; then [[ -e ${1#.../} ]] && pwd && return elif (( ++up == $1 )); then pwd && return fi done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ BuildArray ________________________________________________________| # # buildarray name terms... -- elements... # # Create an array by adding all the terms to it for each element, replacing {} terms by the element. # # name The name of the array to put the result into. # terms The values to add to the array for each of the elements. A {} term is replaced by the current element. # elements The elements to iterate the terms for. # buildarray() { local target=$1 term terms=() element value shift while [[ $1 != -- ]]; do terms+=("$1") shift done shift for element; do for term in "${terms[@]}"; do [[ $term = {} ]] && value="$element" || value="$term" declare -ag "$target+=($(printf '%q' "$value"))" done done } # _____________________________________________________________________ # ______________________________________________________________________ # |__ InArray ___________________________________________________________| # # inArray element array # # Checks whether a certain element is in the given array. # # element The element to search the array for. # array This is a list of elements to search through. # inArray() { # Parse the options. local element local search=$1; shift # Perform the search. for element do [[ $element = $search ]] && return 0; done return 1 } # _____________________________________________________________________ # ______________________________________________________________________ # |__ HideDebug _________________________________________________________| # # hideDebug [on|off] # # Toggle Bash's debugging mode off temporarily. # To hide Bash's debugging output for a function, you should have # hideDebug on # as its first line, and # hideDebug off # as its last. # hideDebug() { if [[ $1 = on ]]; then : -- HIDING DEBUG OUTPUT .. [[ $- != *x* ]]; bashlib_debugWasOn=$? set +x elif [[ $1 = off ]]; then : -- SHOWING DEBUG OUTPUT .. (( bashlib_debugWasOn )) && \ set -x fi } # ______________________________________________________________________ # |__ anfunc ____________________________________________________________| # # anfunc [on|off] # # Turn on or off support for annonymous functions. # # WARNING: This is a hack. It turns on extdebug and causes any argument # that matches (){code} to be replaced by a function name that if invoked # runs code. # # eg. # confirm '(){ rm "$1" }' *.txt # # In this example, confirm() could be a function that asks confirmation # # for each argument past the first and runs the anfunc in the first # # argument on each confirmed argument. # # Don't use this. It is an academic experiment and has bugs. # # Bugs: # - commands lose their exit code. # To inhibit the real command from running, we use extdebug and # a DEBUG trap that returns non-0. As a result, the actual return # code is lost. # anfunc() { case "$1" in on) shopt -s extdebug trap _anfunc_trap DEBUG ;; off) trap - DEBUG shopt -u extdebug ;; esac } _anfunc_trap() { local f w # Perform the command parsing and handling up to its word splitting. # This includes command substitution, quote handling, pathname expansion, etc. declare -a words="($BASH_COMMAND)" # Iterate the words to run in the final stage, and handle anfunc matches. for ((w=0; w<${#words[@]}; ++w)); do [[ ${words[w]} = '(){'*'}' ]] && # Declare a new function for this anfunc. eval "_f$((++f))${words[w]}" && # Replace the word by the new function's name. words[w]="_f$f" done # Run the command. eval "$(printf '%q ' "${words[@]}")" # Clean up the anfuncs. for ((; f>0; --f)); do unset -f "_f$f" done # Inhibit the real command's execution. return 1 } # ______________________________________________________________________ # |__ StackTrace ________________________________________________________| # # stackTrace # # Output the current script's function execution stack. # stackTrace() { # Some general debug information. wrn " [PID : %15s] [PPID : %8s] [Main PID : %8s]" "$BASHPID" "$PPID" "$$" wrn " [Level : %15s] [Subshells : %8s] [Runtime : %7ss]" "$SHLVL" "$BASH_SUBSHELL" "$SECONDS" wrn " [Locale : %15s] [IFS : %8s]" "${LC_ALL:-${LC_COLLATE:-${LANG:-C}}}" "$(printf %q "$IFS")" wrn " Dir Stack : %s" "${DIRSTACK[*]}" wrn " Shell : %s v%s" "$BASH" "$BASH_VERSION" wrn " Shell Opts : %s" "${SHELLOPTS//:/, }" wrn " Bash Opts : %s" "${BASHOPTS//:/, }" wrn " Functions :" # Search through the map. local arg=0 for stack in "${!FUNCNAME[@]}"; do (( stack+1 >= ${#BASH_SOURCE[@]} )) && break func=${FUNCNAME[stack]} line=${BASH_LINENO[stack]} file=${BASH_SOURCE[stack+1]} args=() for (( arg=0, s=0; s <= stack; ++s )); do for (( sarg=0; sarg < ${BASH_ARGC[s]:-0}; ++sarg, ++arg )); do (( s == stack )) && args[${BASH_ARGC[s]} - sarg]=${BASH_ARGV[arg]} done done wrn '%40s:%-3d | %s %s' "$file" "$line" "$func" "$(printf '%s ' "$(shquote "${args[@]}")")" done } # _____________________________________________________________________ # ______________________________________________________________________ # | | # | .: ENTRY POINT :. | # |______________________________________________________________________| # Make sure this file is sourced and not executed. ( return 2>/dev/null ) || { help=$(sed -n '1,/_tocHash=/{ /^#/p; }' "$BASH_SOURCE") if [[ $1 ]]; then while [[ $1 ]]; do awk "p && !/^# *[^ ]/ {exit} p || /^# $1/ {print; p=1}" <<< "$help" shift done else echo "$help" echo echo "To use bashlib, copy it into your PATH and put ''source bashlib'' at the top of your script." fi } : : .: END SOURCING :. : ______________________________________________________________________ :