diff --git a/InternetArchive/function_lib.sh b/InternetArchive/function_lib.sh new file mode 100644 index 0000000..0da0a59 --- /dev/null +++ b/InternetArchive/function_lib.sh @@ -0,0 +1,477 @@ +# +# 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