# # Bash Functions for sourcing in scripts. # # This is a copy of some of the functions for HPR project use. # # ${HOME}/HPR/InternetArchive/function_lib.sh # # Revised: 2024-06-15 18:13:28 # #=== FUNCTION ================================================================ # NAME: abs_diff # DESCRIPTION: Compute the difference between two numbers, returning an # absolute value # PARAMETERS: $1 first number # $2 second number # RETURNS: The absolute value as a string #=============================================================================== function abs_diff { local p1="${1:?Usage: abs_diff n1 n2}" local p2="${2:?Usage: abs_diff n1 n2}" echo $((p1 >= p2 ? p1 - p2 : p2 - p1)) } #=== FUNCTION ================================================================ # NAME: cleanup_temp # DESCRIPTION: Cleanup temporary files in case of a keyboard interrupt # (SIGINT) or a termination signal (SIGTERM) and at script # exit # PARAMETERS: * - names of temporary files to delete # RETURNS: Nothing #=============================================================================== function cleanup_temp { for tmp in "$@"; do [ -e "$tmp" ] && rm --force "$tmp" done exit 0 } #=== FUNCTION ================================================================ # NAME: _DEBUG # DESCRIPTION: Writes one or more messages if in DEBUG mode. Each argument is # seen as a message and is written on a separate line. # References the global variable 'DEBUG' which is expected to be # True if debug output is wanted. # PARAMETERS: List of messages # RETURNS: Nothing #=============================================================================== _DEBUG () { [ "$DEBUG" == 0 ] && return for msg in "$@"; do printf 'D> %s\n' "$msg" done } #=== FUNCTION ================================================================ # NAME: yes_no # DESCRIPTION: Read a Yes or No response from STDIN (only these values are # accepted) and return a suitable numeric value. # PARAMETERS: 1 - Prompt string for the read # 2 - Default value (optional) # RETURNS: 0 for a response of Y or YES, 1 otherwise #=============================================================================== yes_no () { local prompt="${1:?Usage: yes_no prompt [default]}" local default="${2^^}" local ans res if [[ $prompt =~ %s ]]; then if [[ -n $default ]]; then default=${default:0:1} # shellcheck disable=SC2059 # { case "$default" in Y) printf -v prompt "$prompt" "[Y/n]" ;; N) printf -v prompt "$prompt" "[y/N]" ;; *) echo "Error: ${FUNCNAME[0]} @ line ${BASH_LINENO[0]}: Default must be 'Y' or 'N'" exit 1 ;; esac # } else echo "Error: ${FUNCNAME[0]} @ line ${BASH_LINENO[0]}: Default required" exit 1 fi fi # # Loop until a valid input is received # while true; do # # Read and handle CTRL-D (EOF) # read -r -e -p "$prompt" ans res="$?" if [[ $res -ne 0 ]]; then echo "Read aborted" return 1 fi [ -z "$ans" ] && ans="$default" # # Look for valid replies and return appropriate values. Print an error # message otherwise and loop around for another go # if [[ ${ans^^} =~ ^Y(E|ES)?$ ]]; then return 0 elif [[ ${ans^^} =~ ^NO?$ ]]; then return 1 else echo "Invalid reply; please use 'Y' or 'N'" fi done } #=== FUNCTION ================================================================ # NAME: make_file_list # DESCRIPTION: Given an array of filenames select from them and make # an array of choices in a different order if required # PARAMETERS: 1 - input array name # 2 - output array name # RETURNS: True (0) if a new list was generated, false (1) otherwise #=============================================================================== make_file_list () { local -n inarr=${1:?Usage make_file_list input_array out_array} local -n outarr=${2:?Usage make_file_list input_array out_array} local i msg prompt range lines choices # # Print the array contents # lines="${#inarr[@]}" for ((i = 0; i < lines; i++)); do printf '%-2d %s\n' "$((i+1))" "${inarr[$i]}" done msg="Enter a range like '1,2' or '4-7'" if [[ $lines -eq 1 ]]; then prompt="1" else prompt="1-$lines" msg+=" (between 1 and $lines)" fi # # Choose array elements by number where order is significant # echo "$msg" if read_value "Files to add to list: " range "$prompt"; then # # If there's a range then parse it and write the relevant elements to the # target array, otherwise exit with an error # if [[ -n "$range" ]]; then range_parse "$lines" "$range" choices 0 for i in $choices; do outarr+=("${inarr[$((i-1))]}") done else echo "No files chosen; no list created" return 1 fi return 0 else return 1 fi } #=== FUNCTION ================================================================ # NAME: range_parse # DESCRIPTION: Parse a collection of ranges # PARAMETERS: - maximum limit of the range # - entered range expression (e.g. 1-3,7,14) # - name of variable to receive the result # RETURNS: Writes a list of values to the nominated variable #=============================================================================== function range_parse { local max=${1?range_parse: arg 1 missing} local range=${2?range:parse: arg2 missing} local result=${3?range:parse: arg3 missing} local tosort=${4:-1} local selection= # # Turn this off. It affects how some code below behaves # set +o nounset # # Remove spaces from the range # range=${range// /} # # Check for invalid characters # if [[ $range =~ [^0-9,-] ]]; then echo "Invalid range: $range" exit fi # # Slice up the sub-ranges separated by commas and turn all n-m expressions # into the intermediate values. Trim the trailing space from the # concatenation. # until [[ -z $range ]]; do if [[ $range =~ [,] ]]; then item=${range%%,*} range=${range#*,} else item=$range range= fi if [[ $item =~ [-] ]]; then item=${item//-/ } if [[ $item =~ ^([0-9]{1,}).([0-9]{1,})$ ]]; then # shellcheck disable=SC2086 item=$(seq -s' ' $item) else echo "Invalid sequence: ${item/ /-}" item= fi fi selection+="$item " done selection=${selection%% } # # Check for out of bounds problems, sort values and and make unique # if [[ ${#selection} -gt 0 ]]; then # # Validate the resulting range # declare -a sel err msg for i in $selection; do if [[ $i -lt 1 || $i -gt $max ]]; then err+=( $i ) else sel+=( $i ) fi done # # Report any errors # if [[ ${#err[*]} -gt 0 ]]; then msg=( $(for i in ${err[*]}; do echo $i; done | sort -un) ) if [[ ${#msg[*]} -gt 1 ]]; then printf "Values out of range: %s .. %s\n" ${msg[0]} ${msg[${#msg[*]}-1]} else printf "Value out of range: %s\n" ${msg[0]} fi fi # # Rebuild the selection # selection= if [[ ${#sel[*]} -gt 0 ]]; then if [[ $tosort -eq 1 ]]; then selection="$(for i in ${sel[*]}; do echo $i; done | sort -un)" else selection="$(for i in ${sel[*]}; do echo $i; done)" fi fi fi # # Return the result # eval $result=\"$selection\" return } #=== FUNCTION ================================================================ # NAME: pad # DESCRIPTION: Pad $text to the left with $char characters to length $length # PARAMETERS: 1 - the text string to pad (no default) # 2 - how long the padded string is to be (default 80) # 3 - the character to pad with (default '-') # RETURNS: Nothing #=============================================================================== pad () { local text=${1?Usage: pad text [length] [character]} local length=${2:-80} local char=${3:--} local line printf -v line "%0*d%s\n" $((length - ${#text})) 0 "$text" char=${char:0:1} echo "${line//0/$char}" } #=== FUNCTION ================================================================ # NAME: read_value # DESCRIPTION: Read a value from STDIN and handle errors. # PARAMETERS: 1 - Prompt string for the read # 2 - Name of variable to receive the result # 3 - Default value (optional) # RETURNS: 1 on error, otherwise 0 #=============================================================================== read_value () { local prompt="${1:?Usage: read_value prompt outputname [default]}" local outputname="${2:?Usage: read_value prompt outputname [default]}" local default="${3:-}" local var # # Make an option for the 'read' if there's a default # if [[ -n $default ]]; then default="-i '$default'" fi # # Read and handle CTRL-D (EOF). Use 'eval' to deal with the argument being # a variable # eval "read -r -e $default -p '$prompt' var" res="$?" if [[ $res -ne 0 ]]; then echo "Read aborted" return 1 fi # # Return the value in the nominated variable # eval "$outputname='$var'" return 0 } #=== FUNCTION ================================================================ # NAME: define_colours # DESCRIPTION: Defines terminal colours # PARAMETERS: None # RETURNS: Nothing; justs sets the variables in the namespace #=============================================================================== define_colours () { # # Colour codes # red=$(tput setaf 1) export red green=$(tput setaf 2) export green yellow=$(tput setaf 3) export yellow blue=$(tput setaf 4) export blue purple=$(tput setaf 5) export purple reset=$(tput sgr0) export reset return } #=== FUNCTION ================================================================ # NAME: undefine_colours # DESCRIPTION: Remove all the colour codes from the colour variables # PARAMETERS: None # RETURNS: Nothing; justs sets the variables in the namespace #=============================================================================== undefine_colours () { # # Colour codes being removed # for code in red green yellow blue purple reset; do eval "$code=''" eval "export $code" done return } #=== FUNCTION ================================================================ # NAME: coloured # DESCRIPTION: Write a message with colour codes # PARAMETERS: 1 - colour name # 2 - message # RETURNS: Nothing #=============================================================================== coloured () { local colour="${1:-green}" local message="${2:-no message}" printf '%s%s%s\n' "${!colour}" "$message" "${reset}" } #=== FUNCTION ================================================================ # NAME: plural # DESCRIPTION: Adds an 's' to a word depending on the size of a number (based # on an example from O'Reilly). Also look at ngettext for a much # more sophisticated alternative. # PARAMETERS: 1 - the word to pluralise # 2 - the number used to decide # RETURNS: Nothing #=============================================================================== plural () { if [[ $2 -eq 1 || $2 -eq -1 ]]; then echo "${1}" else echo "${1}s" fi } #=== FUNCTION ================================================================ # NAME: trimpath # DESCRIPTION: Trim a file path to its last few components for display # PARAMETERS: $1 - path to trim # $2 - number of elements to retain # RETURNS: Trimmed path #=============================================================================== trimpath () { local path=${1:?Usage: mpath path elements} local elements=${2:?Usage: mpath path elements} local -a arr1 arr2 local result # Split the path # oldifs="$IFS" # IFS='/' mapfile -d'/' -t arr1 <<<"$path" IFS='/' read -r -a arr1 <<<"$path" # IFS="$oldifs" if [[ ${#arr1[@]} -gt $elements ]]; then # Put the last elements in another array mapfile -t arr2 < <(printf '%s\n' "${arr1[@]: -$elements}") # Return the second array interleaved with '/' result="${arr2[*]/%//}" result="${result//\/ /\/}" echo "${result:0:-1}" else echo "$path" fi } #=============================================================================== # NAME: stampfile # DESCRIPTION: Rename a file by adding a timestamp to its name. The timestamp # is the modification time. It is added at the end by default or # wherever the caller places a '%s'. Note there are not enough # error checks to deal with really weird requests :-) # PARAMETERS: $1 - name of file to rename # $2 - optional template string # RETURNS: Nothing #=============================================================================== function stampfile () { if [[ $# = 0 || $# -gt 2 ]]; then echo "Usage: ${FUNCNAME[0]} filename [template]" return fi local fname=${1} local tp=${2:-"${fname}_%s"} [ -e "$fname" ] || { echo "$fname does not exist"; return; } # shellcheck disable=SC2059 new=$(printf "$tp" "$(date -r "$fname" +%Y%m%d_%H%M%S)") mv "$fname" "$new" || { echo "Unable to mv $fname to $new"; return; } echo "Renamed $fname to $new" } # # vim: syntax=sh:ts=8:sw=4:et:tw=78:fo=tcrqn21