Skip to content

Instantly share code, notes, and snippets.

@AfroThundr3007730
Last active December 20, 2024 21:58
Show Gist options
  • Save AfroThundr3007730/b761bd1a6b2f32a2e97727c7e049e354 to your computer and use it in GitHub Desktop.
Save AfroThundr3007730/b761bd1a6b2f32a2e97727c7e049e354 to your computer and use it in GitHub Desktop.
Collection of utility functions for bash scripts
#!/bin/bash
# Collection of utility functions for bash scripts
# Version 0.8.0 modified 2024-12-19 by AfroThundr
# SPDX-License-Identifier: GPL-3.0-or-later
# For issues or updated versions of this script, browse to the following URL:
# https://gist.github.com/AfroThundr3007730/b761bd1a6b2f32a2e97727c7e049e354
# Take caution sourcing this file in your shell, as it uses strict mode.
#----------------------------------------------------------------------#
# MARK: How to use this file
#----------------------------------------------------------------------#
# This is a library that should be sourced rather than executed directly.
#
# The functions in this library are categorized as follows:
# - Basic functions - Checks and other elementary functions
# - Helper functions - Do trivial work, simple interface or structure
# - Utility functions - Do non-trivial work, complex interface or structure
#
# Every function in this library will have a help header documenting its use.
#
# Minimal validation is done with function arguments, as they are intended for
# internal use by scripts, not direct use by end users. Review the help header
# to ensure correct usage. Validation for code correctness will remain the
# responsibility of the script author.
#
# Each function documents its dependencies, in case authors wish to use one
# directly instead of sourcing this file. Some functions are convenience
# wrappers around others in this library.
#
# Ordered list of valid help sections:
# - Synopsis - (Required) Short description of the function
# (This section may be unlabeled)
# - Usage - (Required) List of usage and command syntax statements
# (Should enumerate mutually exclusive option sets)
# - Arguments - (Required) List and type of function arguments, or (none)
# (May be optional if function only has options)
# - Options - (Optional) List of any options used by the function
# (Required if options are present)
# - Variables - (Optional) List and type of variables used by the function
# (Required for any external use variables)
# (Local variables are not included here)
# - Returns - (Optional) List of return value(s) and descriptions
# (Required if behavior is non default)
# - Notes - (Optional) Additional details about the function
# - See Also - (Optional) List of related functions from this library
# - Examples - (Optional) List of examples of how to use the function
# (Recommended for complex functions)
# - Compat Info - (Optional) List of bash versions required to use function
# (If omitted, assumed to work in bash >= v3.0)
# - Dependencies - (Required) List of function dependencies, or (none)
#
# A single list item goes on the same line, otherwise one per line, indented.
#
# Valid types are: int, bool, string, array, path, function, args, opts,
# regex, date, time
#
# Bash variables are technically strings, ints, or arrays thereof. We're
# implementing a sort of weak typing here, and providing context.
#
# Variables can be subdivided into external use (ALL_CAPS) and internal use
# (_lower_case) variables. External use variables are part of the public
# interface, while internal use variables should be limited to necessary state
# management functions. All others should be declared `local`.
#
# A function using several external state variables may be an indication it no
# longer counts as trivial, and should be moved to the utility section.
#
# Examples may also be an indication that a function has graduated from the
# trivial classification. Corollary: Any non-trivial function needs examples.
set -euo pipefail
shopt -s extdebug
#----------------------------------------------------------------------#
# MARK: Basic functions
#----------------------------------------------------------------------#
## BEGIN: utils.check_files_match
# Check if two files have matching contents
#
# Usage: utils.check_files_match file_a file_b
#
# Arguments:
# - [path] file_a - The first file to compare
# - [path] file_b - The second file to compare
#
# Returns: [bool] - True (0) if the files match, False (1) if not
#
# Dependencies: (none)
utils.check_files_match() {
(($# == 2)) || return
[[ -f $1 && -f $2 ]] && comm "$1" "$2" &>/dev/null
}
## END: utils.check_files_match
## BEGIN: utils.check_bool_value
# Check if a boolean variable is true or false
#
# Usage: utils.check_bool_value my_bool
#
# Arguments: [bool] my_bool - The variable to check for truthiness or falseness
#
# Returns: [int] - True (0) if the value is one of: true|t|on|yes|y|1
# False (1) if the value is one of: false|f|off|no|n|0
# Error (2) if the value is invalid or an error occurred
#
# See Also:
# - `utils.check_bool_true` - The truth only variant of this function
# - `utils.check_bool_false` - The false only variant of this function
#
# Dependencies: utils.check_bool_true, utils.check_bool_false
utils.check_bool_value() {
(($# == 1)) || return 2
utils.check_bool_true "$1" && return 0
utils.check_bool_false "$1" && return 1
return 2
}
## END: utils.check_bool_value
## BEGIN: utils.check_bool_valid
# Check if a boolean variable is a valid value
#
# Usage: utils.check_bool_valid my_bool
#
# Arguments: [bool] my_bool - The variable to check for a valid value
#
# Returns: [bool] - True (0) if the value is a valid boolean value
# False (1) if the value is invalid or an error occurred
#
# See Also:
# - `utils.check_bool_value` - Checks if a boolean variable is true or false
#
# Dependencies: utils.check_bool_true, utils.check_bool_false
utils.check_bool_valid() {
(($# == 1)) || return
utils.check_bool_true "$1" || utils.check_bool_false "$1"
}
## END: utils.check_bool_valid
## BEGIN: utils.check_bool_true
# Check if a boolean variable is true
#
# Usage: utils.check_bool_true my_bool
#
# Arguments: [bool] my_bool - The variable to check for truthiness
#
# Returns: [bool] - True (0) if the value is one of: true|t|on|yes|y|1
# False (1) if the value is not, or an error occurred
#
# Notes:
# Negating this function doesn't always mean the inverse is true.
#
# See Also:
# - `utils.check_bool_false` - The inverse of this function
# - `utils.check_bool_value` - The combined version to test both cases
#
# Dependencies: (none)
utils.check_bool_true() {
(($# == 1)) || return
[[ ${1,,} =~ ^(true|t|on|yes|y|1)$ ]]
}
## END: utils.check_bool_true
## BEGIN: utils.check_bool_false
# Check if a boolean variable is false
#
# Usage: utils.check_bool_false my_bool
#
# Arguments: [bool] my_bool - The variable to check for falseness
#
# Returns: [bool] - True (0) if the value is one of: false|f|off|no|n|0
# False (1) if the value is not, or an error occurred
#
# Notes:
# Negating this function doesn't always mean the inverse is true.
#
# See Also:
# - `utils.check_bool_true` - The inverse of this function
# - `utils.check_bool_value` - The combined version to test both cases
#
# Dependencies: (none)
utils.check_bool_false() {
(($# == 1)) || return
[[ ${1,,} =~ ^(false|f|off|no|n|0)$ ]]
}
## END: utils.check_bool_false
## BEGIN: utils.check_bools_and
# Check if two booleans are both truthy
#
# Usage: utils.check_bools_and bool_a bool_b
#
# Arguments:
# - [bool] bool_a - The first boolean to check
# - [bool] bool_b - The second boolean to check
#
# Returns: [bool] - True (0) if the values are both truthy
# False (1) if they are otherwise, or invalid
#
# Notes:
# Negating this function doesn't always mean the inverse is true.
#
# See Also:
# - `utils.check_bools_or` - Checks that at least one boolean is truthy
# - `utils.check_bools_xor` - Checks that only one boolean is truthy
#
# Dependencies: utils.check_bool_value
utils.check_bools_and() {
(($# == 2)) || return 2
local -i a=0 b=0
utils.check_bool_value "$1" || a=$?
utils.check_bool_value "$2" || b=$?
((a == 0 && b == 0))
}
## END: utils.check_bools_and
## BEGIN: utils.check_bools_or
# Check if one or both booleans are truthy
#
# Usage: utils.check_bools_or bool_a bool_b
#
# Arguments:
# - [bool] bool_a - The first boolean to check
# - [bool] bool_b - The second boolean to check
#
# Returns: [bool] - True (0) if one or both values are truthy
# False (1) if they are otherwise, or invalid
#
# Notes:
# Negating this function doesn't always mean the inverse is true.
#
# See Also:
# - `utils.check_bools_and` - Checks that both booleans are truthy
# - `utils.check_bools_xor` - Checks that only one boolean is truthy
#
# Dependencies: utils.check_bool_value
utils.check_bools_or() {
(($# == 2)) || return 2
local -i a=0 b=0
utils.check_bool_value "$1" || a=$?
utils.check_bool_value "$2" || b=$?
((a == 0 || b == 0))
}
## END: utils.check_bools_or
## BEGIN: utils.check_bools_xor
# Check if only one boolean is truthy
#
# Usage: utils.check_bools_xor bool_a bool_b
#
# Arguments:
# - [bool] bool_a - The first boolean to check
# - [bool] bool_b - The second boolean to check
#
# Returns: [bool] - True (0) if only one value is truthy
# False (1) if they are otherwise, or invalid
#
# Notes:
# Negating this function doesn't always mean the inverse is true.
#
# See Also:
# - `utils.check_bools_or` - Checks that at least one boolean is truthy
# - `utils.check_bools_and` - Checks that both booleans are truthy
#
# Dependencies: utils.check_bool_value
utils.check_bools_xor() {
(($# == 2)) || return 2
local -i a=0 b=0
utils.check_bool_value "$1" || a=$?
utils.check_bool_value "$2" || b=$?
(((a == 0 || b == 0) && a != b))
}
## END: utils.check_bools_xor
## BEGIN: utils.check_string_xor
# Check if only one of two string variables are set
#
# Usage: utils.check_string_xor var1 var2
#
# Arguments: [string] (x2) - The variables to check for parity
#
# Returns: [int] - True (0) if one variable is set and the other is not
# False (1) if both variables are set or both empty
# Error (2) if an error occurred
#
# Notes:
# Negating this function doesn't always mean the inverse is true.
#
# See Also:
# - `utils.check_string_xnor` - The inverse of this function
#
# Dependencies: (none)
utils.check_string_xor() {
(($# == 2)) || return 2
[[ $1 && ! $2 || ! $1 && $2 ]]
}
## END: utils.check_string_xor
## BEGIN: utils.check_string_xnor
# Check if two string variables are both set or both empty
#
# Usage: utils.check_string_xnor var1 var2
#
# Arguments: [string] (x2) - The variables to check for parity
#
# Returns: [int] - True (0) if both variables are set or both empty
# False (1) if one is set and the other is not
# Error (2) if an error occurred
#
# Notes:
# Negating this function doesn't always mean the inverse is true.
#
# See Also:
# - `utils.check_string_xor` - The inverse of this function
#
# Dependencies: (none)
utils.check_string_xnor() {
(($# == 2)) || return 2
[[ $1 && $2 || ! $1 && ! $2 ]]
}
## END: utils.check_string_xnor
## BEGIN: utils.array_contains
# Check if an item exists in an array
#
# Usage: utils.array_contains item "${array[@]}"
#
# Arguments:
# - [string] item - The item to search for in the array
# - [array] array - The array to be searched for a matching item
#
# Returns: [bool] - True (0) if found, False (1) if not found
#
# See Also: `utils.array_indexof` - Gets the index of an item in an array
#
# Dependencies: (none)
utils.array_contains() {
(($# >= 2)) || return
local i a=("${@:2}")
for i in "${!a[@]}"; do
[[ $1 == "${a[i]}" ]] && return
done
return 1
}
## END: utils.array_contains
## BEGIN: utils.array_indexof
# Get the index of an item in an array
#
# Usage: utils.array_indexof item "${array[@]}"
#
# Arguments:
# - [string] item - The item to search for in the array
# - [array] array - The array to be searched for a matching item
#
# Notes:
# Outputs index of the matching array element, or '-1' if not found.
#
# See Also: `utils.array_contains` - Checks if an item exists in an array
#
# Dependencies: (none)
utils.array_indexof() {
(($# >= 2)) || return
local i m a=("${@:2}")
for i in "${!a[@]}"; do
[[ $1 == "${a[i]}" ]] && m=$i && break
done
printf '%s' "${m:=-1}"
}
## END: utils.array_indexof
## BEGIN: utils.sleep
# Sleep for the specified number of seconds
#
# Usage: utils.sleep [seconds]
#
# Arguments: [num] seconds - Seconds to sleep, may be decimal
#
# Variables: [int] _sleep_fd - Internal persisted file descriptor
#
# Dependencies: (none)
utils.sleep() {
(($# == 1)) || return
[[ ${_sleep_fd:-} ]] || {
exec {_sleep_fd}<> <(:) && utils.sleep 0
}
read -r -t "${1#*-}" -u "$_sleep_fd" || :
}
## END: utils.sleep
#----------------------------------------------------------------------#
# MARK: Helper functions
#----------------------------------------------------------------------#
## BEGIN: utils.call_trace
# Trace function call stack, propagating return code
#
# Usage: utils.call_trace some_function [function_args ...]
#
# Arguments:
# - [function] some_function - The name of the function to be called
# - [args] function_args - The arguments of the function to be called
#
# Variables:
# - [bool] DEBUG - Set this to true to produce output
# - [int] _call_count - The persisted call stack depth
#
# Returns: [int] - Return code of the called function
#
# Notes:
# If the invoking script uses `set -e` this will never propagate a non zero
# return value (the script terminates immediately). The easiest way to use
# this is by prepending it to each of the function calls in your script.
#
# See Also: bash builtin: `set -x``
#
# Dependencies: (none)
utils.call_trace() {
(($# > 0)) || return
declare -gi _call_count=${_call_count:-2}
utils.check_bool_value "${DEBUG:-}" &&
printf '%-*s enter %s\n' $((_call_count++)) '->' "$1"
"$@"
local return=$?
utils.check_bool_value "${DEBUG:-}" &&
printf '%-*s leave %s\n' $((--_call_count)) '<-' "$1"
return $return
}
## END: utils.call_trace
## BEGIN: utils.say
# Write a message with timestamp, switches behavior by environment
#
# Usage:
# utils.say 'message'
# SAY_HEAVY=1 utils.say [args ...] 'format_string' 'message'
#
# Arguments: (see help for say_lite and say_full)
#
# Variables: [bool] SAY_HEAVY - Use the full featured version, if set to true
#
# Notes:
# This is a wrapper around the other two say functions in this library.
# Further usage details can be found in their respective help text.
#
# See Also:
# - `utils.say_lite` - The lightweight version invoked by this wrapper
# - `utils.say_med` - The mid sized version (not called here)
# - `utils.say_full` - The full feature version invoked by this wrapper
#
# Dependencies: utils.check_bool_value, utils.say_lite, utils.say_full
utils.say() {
(($# > 0)) || return
utils.check_bool_value "${SAY_HEAVY:-}" ||
{ utils.say_lite "$@" && return; } &&
{ utils.say_full "$@" && return; }
}
## END: utils.say
## BEGIN: utils.die
# Write message to stderr then exit 1
#
# Usage: utils.die 'message'
#
# Arguments: [string] message - The message to be printed before exiting
#
# Notes:
# This function does not return. It immediately exits the script.
#
# See Also:
# - `utils.say_lite` - Lightweight logging function with timestamps
# - `utils.say_full` - Full featured logging function with log levels
#
# Dependencies: (none)
utils.die() {
(($# > 0)) || exit
printf '\e[91m%s\n\e[m' "$1" >&2 && exit 1
}
## END: utils.die
## BEGIN: utils.say_lite
# Write a message with timestamp (lite version)
#
# Usage: utils.say_lite 'message'
#
# Arguments: [string] message - The message to be printed
#
# Variables:
# - [bool] QUIET - Suppress output to console, if set to true
# - [int] EPOCHSECONDS - Seconds since the UNIX epoch (Bash builtin)
# - [int] TZOFFSET - The local timezone offset from UTC, in seconds
#
# See Also:
# - `utils.say_med` - The mid-sized version of this function
# - `utils.say_full` - The full-featured version of this function
#
# Dependencies: utils.check_bool_value, utils.get_tz_offset_secs
utils.say_lite() {
(($# == 1)) || return
[[ ${TZOFFSET:-} ]] || utils.get_tz_offset_secs
utils.check_bool_value "${QUIET:-}" ||
printf '%(%FT%TZ)T: %s\n' $((EPOCHSECONDS - TZOFFSET)) "$1"
}
## END: utils.say_lite
## BEGIN: utils.say_med
# Write a message with timestamp (mid-size version)
#
# Usage: utils.say_med [-h|-e] 'message'
#
# Arguments: [string] message - The message to be printed
#
# Options:
# -h Print a help message (no timestamp, green text)
# -e Print an error message (to stderr, red text)
#
# Variables:
# - [path] LOGFILE - Path to log file, should it be desired
# If set, message is also copied here
# - [bool] QUIET - Suppress output to console, if set to true
# - [int] EPOCHSECONDS - Seconds since the UNIX epoch (Bash builtin)
# - [int] TZOFFSET - The local timezone offset from UTC, in seconds
#
# See Also:
# - `utils.say_lite` - The lightweight version of this function
# - `utils.say_full` - The full-featured version of this function
#
# Dependencies: utils.check_bool_value, utils.get_tz_offset_secs
utils.say_med() {
(($# == 1 || $# == 2)) || return
[[ $1 == -h ]] && printf '\e[34m%s\n' "$2" && return
[[ $1 == -e ]] && printf '\e[31m' && local fd=2 && shift
[[ ${TZOFFSET:-} ]] || utils.get_tz_offset_secs
utils.check_bool_value "${QUIET:-}" ||
printf '%(%FT%TZ)T: %s\n' $((EPOCHSECONDS - TZOFFSET)) "$1" >&"${fd:-1}"
[[ ${LOGFILE:-} ]] &&
printf '%(%FT%TZ)T: %s\n' $((EPOCHSECONDS - TZOFFSET)) "$1" >>"$LOGFILE"
printf '\e[m'
}
## END: utils.say_med
## BEGIN: utils.ensure_dir_exists
# Creates a directory if it doesn't exist
#
# Usage: utils.ensure_dir_exists [-f] target
#
# Arguments: [path] target - The path to the directory
#
# Options: -f Overwrite if target exists and is not a directory
#
# See Also: `utils.ensure_file_exists` - Does the same for files
#
# Dependencies: (none)
utils.ensure_dir_exists() {
(($# > 0)) || return
if [[ $1 == -f ]]; then
(($# == 2)) || return && shift
[[ -d $1 ]] || rm -fr "${1:-}"
fi
mkdir -p "$1"
}
## END: utils.ensure_dir_exists
## BEGIN: utils.ensure_file_exists
# Creates a file if it doesn't exist
#
# Usage: utils.ensure_file_exists [-f] target
#
# Arguments: [path] target - The path to the file
#
# Options: -f Overwrite if target exists and is not a file
#
# See Also: `utils.ensure_dir_exists` - Does the same for directories
#
# Dependencies: (none)
utils.ensure_file_exists() {
(($# > 0)) || return
if [[ $1 == -f ]]; then
(($# == 2)) || return && shift
[[ -f $1 ]] || rm -f "${1:-}"
fi
[[ $1 == "${1%*/}" ]] || mkdir -p "${1%*/}"
touch "$1"
}
## END: utils.ensure_file_exists
## BEGIN: utils.add_if_missing
# Add a line to a file if it's not already present
#
# Usage: utils.add_if_missing config_file 'some_line'
#
# Arguments:
# - [path] config_file - The path to the config file to be modified
# - [string] some_line - The line to add to the config file
#
# Dependencies: (none)
utils.add_if_missing() {
(($# == 2)) || return
utils.ensure_file_exists "$1" || return
grep -qsF -- "$2" "$1" || printf '%s\n' "$2" >>"$1"
}
## END: utils.add_if_missing
## BEGIN: utils.copy_if_different
# Copy a file if the source and destination don't match
#
# Usage: utils.copy_if_different src_file dst_file
#
# Arguments:
# - [path] src_file - The source file to be copied
# - [path] dst_file - The destination file to be copied to
#
# Dependencies: utils.check_files_match
utils.copy_if_different() {
(($# == 2)) && [[ -f $1 ]] || return
utils.check_files_match "$1" "$2" || cp -f "$1" "$2"
}
## END: utils.copy_if_different
## BEGIN: utils.force_symlink
# Make a symlink, overwriting the destination if necessary
#
# Usage: utils.force_symlink target_path dst_path
#
# Arguments:
# - [path] target_path - The location the link points to
# - [path] dst_path - The location to place the link
#
# Dependencies: (none)
utils.force_symlink() {
(($# == 2)) || return
[[ ! -e $2 ]] || rm -fr "${2:-}" && ln -fs "$1" "$2"
}
## END: utils.force_symlink
## BEGIN: utils.create_dir_symlink
# Make a directory symlink, creating the target if necessary
#
# Usage: utils.create_dir_symlink target_path dst_path
#
# Arguments:
# - [path] target_path - The directory the link points to
# - [path] dst_path - The location to place the link
#
# Dependencies: utils.ensure_file_exists
utils.create_dir_symlink() {
(($# == 2)) || return
ln -fs "$1" "$2" && utils.ensure_dir_exists "$1"
}
## END: utils.create_dir_symlink
## BEGIN: utils.create_file_symlink
# Make a file symlink, creating the target if necessary
#
# Usage: utils.create_file_symlink target_path dst_path
#
# Arguments:
# - [path] target_path - The file the link points to
# - [path] dst_path - The location to place the link
#
# Dependencies: utils.ensure_dir_exists
utils.create_file_symlink() {
(($# == 2)) || return
ln -fs "$1" "$2" && utils.ensure_file_exists "$1"
}
## END: utils.create_file_symlink
## BEGIN: utils.get_tz_offset_secs
# Get local timezone offset from UTC in seconds
#
# Usage: utils.get_tz_offset_secs
#
# Variables: [int] TZOFFSET - Stores the offset in seconds
#
# Dependencies: (none)
utils.get_tz_offset_secs() {
local off && printf -v off '\n%(%z)T' -1 && declare -gi \
TZOFFSET=${off: -5:1}$((${off: -4:2} * 3600 + ${off: -2:2} * 60))
}
## END: utils.get_tz_offset_secs
## BEGIN: utils.get_timestamp
# Prints a timestamp in ISO-8601 format
#
# Usage: utils.get_timestamp [-l|-u] [seconds]
#
# Arguments: [int] seconds - The timestamp to print in Unix epoch seconds
# Defaults to the current time if not specified
#
# Variables:
# - [int] EPOCHSECONDS - Seconds since the UNIX epoch (Bash builtin)
# - [int] TZOFFSET - The local timezone offset from UTC, in seconds
#
# Options:
# -l Print timestamp in local timezone
# -u Print timestamp in UTC (default)
#
# Dependencies: utils.get_tz_offset_secs
utils.get_timestamp() {
(($# <= 2)) || return
[[ ${1:-} == -l ]] && shift &&
printf '%(%FT%T%z)T\n' "${1:--1}" && return
[[ ${1:-} == -u ]] && shift
[[ ${TZOFFSET:-} ]] || utils.get_tz_offset_secs
printf '%(%FT%TZ)T\n' $((${1:-$EPOCHSECONDS} - TZOFFSET))
}
## END: utils.get_timestamp
## BEGIN: utils.seconds_to_hms
# Convert time duration in seconds to [D.]HH:MM:SS format
#
# Usage:
# utils.seconds_to_hms duration
# echo duration | utils.seconds_to_hms
#
# Arguments: [int] duration - The time span in seconds to be converted
# (This value may also be read from stdin)
#
# See Also: `utils.hms_to_seconds` - The inverse of this function
#
# Dependencies: (none)
utils.seconds_to_hms() {
local -i in=${1:-$(</dev/stdin)}
((in / 86400 > 0)) && printf '%d.' $((in / 86400))
printf '%.02d:%.02d:%.02d\n' \
$((in % 86400 / 3600)) $((in % 3600 / 60)) $((in % 60))
}
## END: utils.seconds_to_hms
## BEGIN: utils.hms_to_seconds
# Convert time durations in [D.]HH:MM:SS format to seconds
#
# Usage:
# utils.hms_to_seconds duration
# echo duration | utils.hms_to_seconds
#
# Arguments: [string] duration - The time span in HH:MM:SS to be converted
# (This value may also be read from stdin)
#
# See Also: `utils.seconds_to_hms` - The inverse of this function
#
# Dependencies: (none)
utils.hms_to_seconds() {
local in=${1:-$(</dev/stdin)}
local -i day=0 hour=0 min=0 sec=0
[[ $in =~ (([0-9]+\.)?[0-9]+:)?[0-9]+:[0-9]+ ]] || return
[[ $in =~ : ]] && sec=${in##*:} in=${in%:*} || sec=$in
[[ $in =~ : ]] && min=${in##*:} in=${in%:*} || min=$in
[[ $in =~ \. ]] && hour=${in##*.} day=${in%.*} || hour=$in
printf '%d\n' $((day * 86400 + hour * 3600 + min * 60 + sec))
}
## END: utils.hms_to_seconds
## BEGIN: utils.copy_function
# Copy a function to another name
#
# Usage: utils.copy_function function_a function_b
#
# Arguments:
# - [string] function_a - The function name to be copied from
# - [string] function_b - The function name to be copied to
#
# See Also: `utils.rename_function` - A utility to rename a function
#
# Dependencies: (none)
utils.copy_function() {
(($# == 2)) || return
[[ $(declare -f "$1") ]] || return
local -r func=$(declare -f "$1")
eval "${func/$1/$2}"
}
# END: utils.copy_function
# BEGIN: utils.rename_function
# Rename a function to another name
#
# Usage: utils.rename_function function_a function_b
#
# Arguments:
# - [string] function_a - The function name to be renamed
# - [string] function_b - The function name to rename to
#
# See Also: `utils.copy_function` - A utility to copy a function
#
# Dependencies: utils.copy_function
utils.rename_function() {
(($# == 2)) || return
utils.copy_function "$@" || return
unset -f "$1"
}
# END: utils.rename_function
# BEGIN: utils.test_args
# Test that an option is followed by a valid argument
#
# Usage: utils.test_args "$1" "$2"
#
# Arguments: [args] (x2) - The option and following argument to test, usually
# both supplied as positional parameters to the script
#
# Returns: [bool] - True (0) if the second parameter is not an option
# False (1) if the second parameter is an option
#
# Notes:
# This function tests that the next argument doesn't begin with a hyphen,
# unless it happens to be a negative number. Hence, it can be a false
# positive if you use numbers as option names.
#
# See Also: `utils.test_args_die` - The exit variant of this function
#
# Examples:
# - Consider a script called as such: `my_script.sh -foo bar`
# $ utils.test_args "$1" "$2" # returns true
# - Consider a script called as such: `my_script.sh -foo -bar`
# $ utils.test_args "$1" "$2" # returns false
#
# Dependencies: (none)
utils.test_args() {
(($# == 2)) || return
[[ ${#2} -gt 0 && ($2 == "${2#-}" || $2 =~ ^-?[\.0-9]+) ]]
}
# END: utils.test_args
# BEGIN: utils.test_args_die
# Test that an option is followed by a valid argument
#
# Usage: utils.test_args_die "$1" "$2"
#
# Arguments: [args] (x2) - The option and following argument to test, usually
# both supplied as positional parameters to the script
#
# Notes:
# This function tests that the next argument doesn't begin with a hyphen,
# unless it happens to be a negative number. Hence, it can be a false
# positive if you use numbers as option names.
#
# This variant will directly exit the script on failure
#
# See Also: `utils.test_args` - The return only variant of this function
#
# Examples:
# - Consider a script called as such: `my_script.sh -foo bar`
# $ utils.test_args_die "$1" "$2" # returns true
# - Consider a script called as such: `my_script.sh -foo -bar`
# $ utils.test_args_die "$1" "$2" # exits with error message
#
# Dependencies: utils.die
utils.test_args_die() {
(($# == 2)) || return
[[ ${#2} -gt 0 && ($2 == "${2#-}" || $2 =~ ^-?[\.0-9]+) ]] ||
utils.die "Option '$1' expects an argument, but we got: '$2'"
}
# END: utils.test_args_die
# BEGIN: utils.get_script_metadata_tag
# Extract the value of a tag in a script or text file
#
# Usage: utils.get_script_metadata_tag -t tag_name -f file_name -p pattern
#
# Options:
# -t [string] tag_name - The name of the tag to search for
# -f [path] file_name - The path to the file to be searched
# -p [regex] pattern - Regex pattern to extract the tag value
#
# Notes:
# This function does pattern matching on the first line matching the tag name
# to extract the desired value. For example: a version or date tag embedded in
# a comment or variable.
#
# See Also:
# - `utils_set_script_metadata_tag` - Sibling function to set a tag value
# - `utils.get_script_version` - A derived function to get a version tag
# - `utils.get_script_date` - A derived function to get a date tag
#
# Dependencies: (none)
utils.get_script_metadata_tag() {
(($# == 6)) || return
while (($# > 0)); do
case $1 in
-t) local tag=$2 ;;
-f) local file=$2 ;;
-p) local regex=$2 ;;
*) return 1 ;;
esac
shift 2
done
awk -v tag="$tag" -v regex="$regex" '
$0 ~ tag && $0 ~ regex {
print gensub(".*("regex").*", "\\1", 1, $0)
exit
}' "$file"
}
# END: utils.get_script_metadata_tag
# BEGIN: utils.set_script_metadata_tag
# Adjust the value of a tag in a script or text file
#
# Usage: utils.set_script_metadata_tag -t tag_name -f file_name
# -o old_value -n new_value
#
# Options:
# -t [string] tag_name - The name of the tag to be altered
# -f [path] file_name - The path to the file to be edited
# -o [string] old_value - The original tag value to be removed
# -n [string] new_value - The new tag value to be inserted
#
# Notes:
# This function does pattern matching on the first line matching the tag name
# to find and replace the desired value. For example: a version or date tag
# embedded in a comment or variable.
#
# See also:
# - `utils.get_script_metadata_tag` - Sibling function to get a tag value
# - `utils.set_script_version` - A derived function to set a version tag
# - `utils.set_script_date` - A derived function to set a date tag
#
# Dependencies: (none)
utils.set_script_metadata_tag() {
(($# == 8)) || return
while (($# > 0)); do
case $1 in
-t) local tag=$2 ;;
-f) local file=$2 ;;
-o) local old=$2 ;;
-n) local new=$2 ;;
*) return 1 ;;
esac
shift 2
done
awk -i inplace -v tag="$tag" -v old="$old" -v new="$new" '
! replace && $0 ~ tag && $0 ~ old {
gsub(old, new, $0)
replace = 1
} 1' "$file"
}
# END: utils.get_script_metadata_tag
# BEGIN: utils.get_script_date
# Get the value of a date tag in a script or text file
#
# Usage: utils.get_script_date [tag_name] file_name
#
# Arguments:
# - [string] tag_name - The name of the date tag to search for
# (This defaults to 'MODIFIED' if not specified)
# - [path] file_name - The path to the file to be searched
#
# Notes:
# This function matches dates in YYYY-MM-DD (ISO 8601) or YYYYMMDD format,
# which are the two most sensible methods of unambiguously recording dates.
#
# See Also: `utils.set_script_date` - Sibling function to set the date tag
#
# Dependencies: utils.get_script_metadata_tag
utils.get_script_date() {
(($# == 1 || $# == 2)) || return
(($# == 1)) && local tag=MODIFIED file=$1
(($# == 2)) && local tag=$1 file=$2
local regex='([0-9]{8}|[0-9]{4}-[0-9]{2}-[0-9]{2})'
utils.get_script_metadata_tag -t "$tag" -f "$file" -p "$regex"
}
# END: utils.get_script_date
# BEGIN: utils.set_script_date
# Set the value of a date tag in a script or text file
#
# Usage: utils.set_script_date [tag_name] new_date file_name
#
# Arguments:
# - [string] tag_name - The name of the date tag to be altered
# (This defaults to 'MODIFIED' if not specified)
# - [date] new_date - The date to be set in the tag, or 'now'
# - [path] file_name - The path to the file to be edited
#
# Notes:
# This function sets dates in YYYY-MM-DD (ISO 8601) or YYYYMMDD format,
# which are the two most sensible methods of unambiguously recording dates.
#
# See Also: `utils.get_script_date` - Sibling function to get the date tag
#
# Dependencies: utils.get_script_date, utils.set_script_metadata_tag
utils.set_script_date() {
(($# == 2 || $# == 3)) || return
(($# == 2)) && local tag=MODIFIED when=$1 file=$2
(($# == 3)) && local tag=$1 when=$2 file=$3
local date_new date_old
date_old=$(utils.get_script_date "$tag" "$file")
[[ $date_old =~ - || $when =~ - ]] &&
date_new=$(date +%F -d "$when") ||
date_new=$(date +%4Y%m%d -d "$when")
utils.set_script_metadata_tag \
-t "$tag" -f "$file" -o "$date_old" -n "$date_new"
}
# END: utils.set_script_date
# BEGIN: utils.get_script_version
# Get the value of a version tag in a script or text file
#
# Usage: utils.get_script_version [tag_name] file_name
#
# Arguments:
# - [string] tag_name - The name of the version tag to search for
# (This defaults to 'VERSION' if not specified)
# - [path] file_name - The path to the file to be searched
#
# Notes:
# This function matches versions in Major.Minor.Patch(-RC) (semantic version)
# format, which is the most sensible and clear method of file versioning.
#
# See Also: `utils.set_script_version` - Sibling function to set the version tag
#
# Dependencies: utils.get_script_metadata_tag
utils.get_script_version() {
(($# == 1 || $# == 2)) || return
(($# == 1)) && local tag=VERSION file=$1
(($# == 2)) && local tag=$1 file=$2
local regex='([0-9]+\\.[0-9]+\\.[0-9]+)(-rc[0-9]+)?'
utils.get_script_metadata_tag -t "$tag" -f "$file" -p "$regex"
}
# END: utils.get_script_version
# BEGIN: utils.set_script_version
# Set the value of a version tag in a script or text file
#
# Usage: utils.set_script_version [tag_name] bump_type file_name
#
# Arguments:
# - [string] tag_name - The name of the version tag to be altered
# (This defaults to 'VERSION' if not specified)
# - [string] bump_type - The version increment type to be set in the tag
# (This can be one of: major|minor|patch|rc)
# - [path] file_name - The path to the file to be edited
#
# Notes:
# This function sets versions in Major.Minor.Patch(-RC) (semantic version)
# format, which is the most sensible and clear method of file versioning.
#
# See Also: `utils.get_script_version` - Sibling function to get the version tag
#
# Dependencies: utils.get_script_version, utils.set_script_metadata_tag
utils.set_script_version() {
(($# == 2 || $# == 3)) || return
(($# == 2)) && local tag=VERSION type=$1 file=$2
(($# == 3)) && local tag=$1 type=$2 file=$3
local tmp vers_new vers_old
local -i major=0 minor=0 patch=0 rc=0
vers_old=$(utils.get_script_version "$tag" "$file")
[[ $vers_old =~ - ]] &&
tmp=$vers_old rc=${vers_old#*-rc} vers_old=${vers_old%-*}
read -r major minor patch <<<"${vers_old//./ }"
vers_old=${tmp:-$vers_old}
case $type in
major) major+=1 minor=0 patch=0 rc=0 ;;
minor) minor+=1 patch=0 rc=0 ;;
patch) patch+=1 rc=0 ;;
rc) rc+=1 ;;
*) return 1 ;;
esac
((rc == 0)) && vers_new="$major.$minor.$patch" ||
vers_new="$major.$minor.$patch-rc$rc"
utils.set_script_metadata_tag \
-t "$tag" -f "$file" -o "$vers_old" -n "$vers_new"
}
# END: utils.set_script_version
# BEGIN: utils.get_ini_value
# TODO: Maybe switch these to regex parsing to accomodate values with '='
# Get value from INI file
#
# Usage: utils.get_ini_value file section cfg_key
#
# Arguments:
# - [path] file - The path of the INI file to be searched
# - [string] section - The section to be searched within
# - [string] cfg_key - THe config key to be searched for
#
# Notes:
# If the 'global' section is specified, the head of the file before any secion
# is searched first, then a section named '[global]', if it exists.
#
# See Also: `utils.set_ini_value` - Sibling function to set INI value
#
# Dependencies: (none)
utils.get_ini_value() {
(($# == 3)) || return
awk -F= -v sect="$2" -v key="$3" '
(! head && sect == "global") || $1 == "[" sect "]" {
found = 1
} found {
if ($1 == key) {
print $2
exit
}
if ($1 ~ /^\[/ && $1 != "[" sect "]") {
found = 0
head = 1
}
}' "$1"
}
# END: utils.get_ini_value
# BEGIN: utils.set_ini_value
# Set value in INI file
#
# Usage: utils.set_ini_value file section cfg_key cfg_value
#
# Arguments:
# - [path] file - The path of the INI file to be modified
# - [string] section - The section to be searched within
# - [string] cfg_key - The config key to be modified
# - [string] cfg_value - The value to be inserted
#
# Notes:
# If the 'global' section is specified, the head of the file before any secion
# is searched first, then a section named '[global]', if it exists.
#
# If the key or the section doesn't exist, it will be added.
#
# See Also: `utils.get_ini_value` - Sibling function to get INI value
#
# Dependencies: (none)
utils.set_ini_value() {
(($# == 4)) || return
awk -F= -v sect="$2" -v key="$3" -v value="$4" -i inplace '
! replace {
if ((! head && sect == "global") || $1 == "[" sect "]") {
found = 1
}
if (found) {
if ($1 == key) {
gsub($2, value, $0)
replace = 1
}
if ($1 ~ /^\[/ && $1 != "[" sect "]") {
print key "=" value
found = 0
head = 1
replace = 1
}
}
} 1; ENDFILE {
if (! replace) {
if (! found) {
print "\n[" sect "]"
}
print key "=" value
}
}' "$1"
}
# END: utils.set_ini_value
# BEGIN: utils.get_config_option
# TODO: Adjust these to account for valueless options
# Get config option value from config file
#
# Usage: utils.get_config_option [options ...] cfg_file cfg_key
#
# Arguments:
# - [opts] options - One or more options, see next section for details
# - [path] cfg_file - The path to the config file to be searched
# - [string] cfg_key - The config key or option to search for
#
# Options:
# -d delim Specify the delimeter separating the key and value
# (This defaults to a space if not specified)
# -i Include commented out config options
# -q Value to be searched is double quoted
# -Q Value to be searched is single quoted
#
# See Also: `utils.set_config_option` - Sibling function to set a config option
#
# Dependencies: (none)
utils.get_config_option() {
(($# >= 2)) || return
local cdelim='#' ctemp delim include quote r1 r2
while (($# > 2)); do
case $1 in
-d) delim=$2 && shift ;;
-i) include=1 ;;
-q) quote='"' ;;
-Q) quote="'" ;;
*) return 1 ;;
esac
shift
done
(($# == 2)) || return
[[ ${include:-} ]] && ctemp=$cdelim cdelim=''
r1="^[ \t]*(|${cdelim:-}+.*)$"
r2="^[ \t${ctemp:-}]*${2}[ \t]*${delim:-[ \t]+}[ \t]*${quote:-}"
r2+="([^${cdelim:-$ctemp}]*)${quote:-}.*$"
awk -v r1="$r1" -v r2="$r2" '
$0 !~ r1 && $0 ~ r2 {
match($0, r2, matches)
gsub(/^[ \t]*|[ \t]*$/, "", matches[1])
print matches[1]
exit
}' "$1"
}
# END: utils.get_config_option
# BEGIN: utils.set_config_option
# TODO: Still need an update_config_option to add/remove from a config value
# Set config option value in config file
#
# Usage: utils.set_config_option [options ...] cfg_file cfg_key 'cfg_value'
#
# Arguments:
# - [opts] options - One or more options, see next section for details
# - [path] cfg_file - The path to the config file to be modified
# - [string] cfg_key - The config key or option to set
# - [string] cfg_value - The config value to be set
#
# Options:
# -a Append to the end of the file instead of updating in-place
# (If the option is not found, it will be appended anyway)
# -d delim Specify the delimeter separating the key and value
# (This defaults to a space if not specified)
# -i Include commented out config options
# -q Value to be modified is double quoted
# -Q Value to be modified is single quoted
#
# See Also: `utils.get_config_option` - Sibling function to get a config option
#
# Dependencies: utils.add_if_missing, utils.get_config_option
utils.set_config_option() {
(($# >= 3)) || return
local append cdelim='#' ctemp delim iflag old_opt qflag quote r1 r2
while (($# > 3)); do
case $1 in
-a) append=1 ;;
-d) delim=$2 && shift ;;
-i) iflag='-i' ;;
-q) quote='"' qflag='-q' ;;
-Q) quote="'" qflag='-Q' ;;
*) return 1 ;;
esac
shift
done
(($# == 3)) || return
[[ -f $1 ]] && old_opt=$(utils.get_config_option \
-c "$cdelim" -d "$delim" ${iflag:-} ${qflag:-} "$1" "$2")
[[ ${append:-} || ! ${old_opt:-} ]] &&
utils.add_if_missing "$1" "${2}${delim:- }${quote:-}${3}${quote:-}" &&
return
[[ ${iflag:-} ]] && ctemp=$cdelim cdelim=''
r1="^[ \t]*(|${cdelim:-}+.*)$"
r2="^([ \t${ctemp:-}]*)(${2}[ \t]*${delim:-[ \t]+}[ \t]*${quote:-})"
r2+="([^${cdelim:-$ctemp}]*)(${quote:-}.*)$"
awk -i inplace -v r1="$r1" -v r2="$r2" -v new="$3" '
! replace && $0 !~ r1 && $0 ~ r2 {
match($0, r2, matches)
gsub($0, matches[2] new matches[4], $0)
replace = 1
} 1' "$1"
}
# END: utils.set_config_option
#----------------------------------------------------------------------#
# MARK: Utility functions
#----------------------------------------------------------------------#
## BEGIN: utils.set_global_variable
# Synopsis:
# Set global variable based on precedence hierarchy
#
# Usage:
# utils.set_global_variable PREFIX CFG_TAG NAME "default"
#
# Arguments:
# - [string] PREFIX - The prefix to use for the global variable
# - [string] CFG_TAG - The infix used to differentiate config variables
# - [string] NAME - The name of the variable to be set
# - [string] default - The default value to use if unset
#
# Notes:
# Generated variable names will be CAPS and prefixed to enforce namespacing.
#
# This respects a precedence hierarchy, where variables already set take
# precedence over variables sourced from a config file, which in turn take
# precedence over the specified default value. The variable might already be
# set by command line option or by environment variable.
#
# In all cases, the final value is exported readonly.
#
# See Also:
# - `utils.set_global_array_variable` - Equivalent function for arrays
# - bash builtin: `declare`
#
# Examples:
# - This sets GV_MY_DISTRO to (in order of precedence):
# 1. The value of `GV_MY_DISTRO` if set
# 2. The value of `GV_CFG_MY_DISTRO` if set
# 3. The literal value 'Fedora Rawhide'
# $ utils.set_global_variable GV CFG MY_DISTRO 'Fedora Rawhide'
#
# Dependencies: (none)
utils.set_global_variable() {
(($# == 4)) || return
local var_name=$1_$3 var_cfg=$1_$2_$3 var_def=$4
if [[ ${!var_name:-} ]]; then
declare -grx "$var_name"
elif [[ ${!var_cfg:-} ]]; then
declare -grx "$var_name=${!var_cfg}"
else
declare -grx "$var_name=$var_def"
fi
}
## END: utils.set_global_variable
## BEGIN: utils.set_global_array_variable
# Synopsis:
# Set global array variable based on precedence hierarchy
#
# Usage:
# utils.set_global_array_variable PREFIX CFG_TAG NAME "default"...
#
# Arguments:
# - [string] PREFIX - The prefix to use for the global variable
# - [string] CFG_TAG - The infix used to differentiate config variables
# - [string] NAME - The name of the variable to be set
# - [string] default - The list of default value(s) to use if unset
#
# Notes:
# Generated variable names will be CAPS and prefixed to enforce namespacing.
#
# This enforces a precedence hierarchy, where variables already set take
# precedence over variables sourced from a config file, which in turn take
# precedence over the specified default value. The variable might already be
# set by command line option or by environment variable.
#
# In all cases, the final value is exported readonly.
#
# See Also:
# - `utils.set_global_variable` - Equivalent function for non-arrays
# - bash builtin `declare`
#
# Examples:
# - This sets GV_MY_DISTRO to (in order of precedence):
# 1. The value of `GV_MY_DISTRO` if set
# 2. The value of `GV_CFG_MY_DISTRO` if set
# 3. The literal value ('Fedora' 'Rawhide')
# $ utils.set_global_array_variable GV CFG MY_DISTRO 'Fedora' 'Rawhide'
#
# Dependencies: (none)
utils.set_global_array_variable() {
(($# >= 4)) || return
local -gn var_name=$1_$3 var_cfg=$1_$2_$3
local -a var_def=("${@:4}")
if [[ ${var_name[*]:-} ]]; then
declare -agrx "${!var_name}"
elif [[ ${var_cfg[*]:-} ]]; then
declare -agrx var_name=("${var_cfg[@]}")
else
declare -agrx var_name=("${var_def[@]}")
fi
unset -n var_name var_cfg
}
## END: utils.set_global_array_variable
## BEGIN: utils.bytes_prefix_to_raw
# Converts human readable file size to bytes
#
# Usage:
# utils.bytes_prefix_to_raw size
# echo size | utils.bytes_prefix_to_raw
#
# Arguments:
# - [string] size - The file size to convert to byte format
# (this value may also be read from stdin)
#
# Notes:
# This function recognizes IEC, SI, and traditional units.
# The output suffix matches the system used in the input.
#
# See Also:
# - `utils.bytes_raw_to_prefix` - The inverse of this function
# - coreutils: `numfmt` - Robust number format conversion tool
#
# Dependencies: utils.array_indexof
utils.bytes_prefix_to_raw() {
(($# <= 2)) || return
local -i base decimal fraction length power=0
local suffix types value
[[ ${value:=${1:-}} ]] || read -r value
[[ $value =~ ^([0-9]+)?\.?([0-9]+)?([A-Za-z]+)?$ ]] || return
read -r decimal fraction suffix <<< "${BASH_REMATCH[@]}"
: "${decimal:=0}" "${fraction:=0}"
[[ $value =~ \. && ${suffix:-} =~ ^B?$ ]] && return 1
case ${suffix:-} in
?(K|M|G|T|P|E|Z|Y)iB)
base=1024 types=(B KiB MiB GiB TiB PiB EiB ZiB YiB)
;;
?(k|M|G|T|P|E|Z|Y)B)
base=1000 types=(B kB MB GB TB PB EB ZB YB)
;;
?(K|M|G|T|P|E|Z|Y)'')
base=1024 types=('' K M G T P E Z Y)
;;
*) return 1 ;;
esac
while ((power < $(utils.array_indexof "$suffix" "${types[@]}"))); do
decimal=$((decimal * base))
length=${length:-${#fraction}}
fraction=$((fraction * base))
power=$((++power))
done
printf '%.0f%s\n' \
$((decimal + 10#0${fraction:0:-length})) \
"${types[0]}"
}
## END: utils.bytes_prefix_to_raw
## BEGIN: utils.bytes_raw_to_prefix
# Converts bytes to human readable file size
#
# Usage:
# utils.bytes_raw_to_prefix [opts] size
# echo size | utils.bytes_raw_to_prefix [opts]
#
# Arguments:
# - [string] size - The file size to convert to human readable format
# (this value may also be read from stdin)
#
# Options:
# --iec Print size in IEC units (B KiB MiB GiB ...)
# --si Print size in SI units (B kB MB GB ...)
# --tr Print size in traditional units (K M G ...)
#
# Notes:
# This function recognizes IEC, SI, and traditional units.
# If no unit type is specified, traditional is used.
#
# See Also:
# - `utils.bytes_prefix_to_raw` - The inverse of this function
# - coreutils: `numfmt` - Robust number format conversion tool
#
# Dependencies: (none)
utils.bytes_raw_to_prefix() {
(($# <= 2)) || return
local -i base decimal fraction power=0
local types value
case ${1:-} in
--iec)
base=1024 types=(B KiB MiB GiB TiB PiB EiB ZiB YiB)
;;
--si)
base=1000 types=(B kB MB GB TB PB EB ZB YB)
;;
--tr | '' | +([0-9B]))
base=1024 types=('' K M G T P E Z Y)
;;
*) return 1 ;;
esac
[[ ${1:-} =~ -- ]] && shift
[[ ${value:=${1:-}} ]] || read -r value
[[ $value =~ ^[0-9]+B?$ ]] && value=${value%B} || return
while ((${decimal:=$value} >= base)); do
fraction=$((decimal % base))
decimal=$((decimal / base))
power=$((++power))
done
printf '%.4g%s\n' \
"$decimal.$(printf '%03d' $((fraction * 1000 / base)))" \
"${types[power]}"
}
## END: utils.bytes_raw_to_prefix
## BEGIN: utils.say_full
# TODO: Fix this so VERBOSE and DEBUG don't print to main log
# Synopsis:
# Write a message with timestamp (full version)
#
# Usage:
# utils.say_full -s [log_file [log_verbose [tee_options]]]
# utils.say_full [options ...] ['format_string'] 'the_message'
#
# Arguments:
# - [opts] options - One or more options, see next section for details
# - [path] log_file - Log file for normal logging (/dev/null if unset)
# - [path] log_verbose - Log file for verbose logging (/dev/null if unset)
# - [args] tee_options - Override the tee command used for logging
# (if unset, its `tee -a log_file log_verbose`)
# - [string] format_string - The `printf` format string for the message
# (if uspecified, the message is used as-is)
# - [string] the_message - The message to be printed and formatted
#
# Options:
# Options to control log level (specify only one):
# -h, --help Prints help message to stdout (no logging or timestamp)
# -d, --debug Prints DEBUG level message to stdout
# -i, --info Prints INFO level message to stdout
# -w, --warn Prints WARN level message to stderr
# -e, --error Prints ERROR level message to stderr
# -f, --fatal Prints FATAL level message to stderr
# Options to control other function behavior:
# -s Initialize the function state. User may optionally specify a log file,
# verbose log file, and `tee` command arguments as positional parameters
# (in that that order), or omit them to disable them.
# -n Don't log this message (if logging is enabled)
# -T Truncate the main log file, then add this message
# -t Truncate the verbose log file, then add this message
# -x Print message then immediately exit 1 (only used with FATAL)
#
# Variables:
# Global variables that control output:
# - [bool] DEBUG - Set to true to show messages logged at DEBUG level
# - [bool] VERBOSE - Set to true to show messages logged at INFO level
# - [bool] QUIET - Set to true to suppress all non-error output
# (The message will still be logged if enabled)
# - [bool] SILENT - Set to true to suppress all output, including errors
# (The message will still be logged if enabled)
# - [string] TERM - If unset or empty, it will be set to 'xterm'
# Global variables read during operation:
# - [int] EPOCHSECONDS - Seconds since the UNIX epoch (Bash builtin)
# - [int] TZOFFSET - The local timezone offset from UTC, in seconds
# Internal variables for storing state:
# - [path] _say_log_main - Persisted path of log_file
# - [path] _say_log_verb - Persisted path of log_verbose
# - [array] _say_tee - Persisted value of tee_options
#
# Returns: [int] - (0) if message printed and logged successfully
# (1) if an error occurred, or invalid option usage
#
# Notes:
# This function supports multiple logging levels, with colorization.
#
# This function can send output to log files, and supports output level
# control and suppression (controlled via environment variables).
#
# If no log level is specified, the message will be printed at LOG level,
# which is unsuppressed by default.
#
# See Also:
# - `utils.say_lite` - The lightweight version of this function
# - `utils.say_med` - The mid-sized version of this function
#
# Examples:
# - This sets log_file to `my_log_file`, leaving log_verbose unset
# $ utils.say_full -s my_log_file
# - This logs prints a warning message to the console
# $ utils.say_full --warn 'Can't find the file: %s' "$my_file"
# - This prints a debug message to the console (if DEBUG is set)
# $ utils.say_full -d 'Files to be written: %s' "${list[*]}"
#
# Dependencies:
# utils.check_bool_value, utils.check_bools_or, utils.get_tz_offset_secs
# shellcheck disable=SC2059
utils.say_full() {
(($# > 0)) || return
[[ $1 == -s || ${_say_log_main:-} ]] || ${FUNCNAME[0]} -s
printf '\e[m'
case $1 in
-s)
export TERM=${TERM:-xterm}
declare -g _say_log_main=${2:-/dev/null}
declare -g _say_log_verb=${3:-/dev/null}
declare -ag _say_tee=(tee -a "$_say_log_main" "$_say_log_verb")
(($# >= 4)) && declare -ag _say_tee=("${@:4}")
[[ ${TZOFFSET:-} ]] || utils.get_tz_offset_secs
;;
-h | --help) printf "\e[34m${2:-}\n" "${@:3}" ;;
*)
local die=false fd=1 format='' log=true tag='LOG'
local regex='^-((d|i|w|e|f)|-(debug|info|warn|error|fatal))$'
utils.check_bool_value "${QUIET:-}" && fd=/dev/null
while (($# > 0)); do
case $1 in
-n) log=false ;;
-T) : >"$_say_log_main" ;;
-t) : >"$_say_log_verb" ;;
-x) die=true ;;
*) break ;;
esac
shift
done
(($# > 0)) || return 0
if [[ $1 =~ $regex ]]; then
case $1 in
-d | --debug)
utils.check_bool_value "${DEBUG:-}" || return 0
printf '\e[94m' && tag='DEBUG'
;;
-i | --info)
utils.check_bools_or "${VERBOSE:-}" "${DEBUG:-}" || return 0
printf '\e[32m' && tag='INFO'
;;
-w | --warn)
printf '\e[33m' && tag='WARN' fd=2
;;
-e | --error)
printf '\e[31m' && tag='ERROR' fd=2
;;
-f | --fatal)
printf '\e[91m' && tag='FATAL' fd=2
;;
esac
shift
fi
format="%(%FT%TZ)T: ${tag}: ${1}\n"
utils.check_bool_value "${SILENT:-}" && fd=/dev/null
if [[ $log == true && $_say_log_main != /dev/null ]]; then
printf "$format" $((EPOCHSECONDS - TZOFFSET)) "${@:2}" |
"${_say_tee[@]}" >&"$fd"
else
printf "$format" $((EPOCHSECONDS - TZOFFSET)) "${@:2}" >&"$fd"
fi
;;
esac
printf '\e[m'
[[ ${tag:-} != FATAL || ${die:-} != true ]] || exit
}
## END: utils.say_full
## BEGIN: utils.stopwatch_lite
# Simple stopwatch, supports resuming and reset
#
# Usage: utils.stopwatch_lite command
#
# Arguments: [string] command - May be one of: reset|start|stop|show
#
# Variables:
# - [int] EPOCHSECONDS - Seconds since the UNIX epoch (Bash builtin)
# - [int] _sw_start - Stopwatch start time, in seconds
# - [int] _sw_stop - Stopwatch stop time, in seconds
# - [int] _sw_total - Stopwatch duration, in seconds
#
# Notes:
# The current duration of the stopwatch can be checked without stopping it.
#
# The stopwatch may be resumed after stopping by starting it again.
#
# This function records and returns values in seconds. Formatting the
# durations can be done with another utility in this library.
#
# See Also:
# - `utils.stopwatch_full` - Supports concurrent stopwatches
# - `utils.seconds_to_hms` - Formats a raw duration into HH:mm:ss
#
# Dependencies: (none)
utils.stopwatch_lite() {
(($# > 0)) || return
declare -ig _sw_start _sw_stop _sw_total
case $1 in
reset) unset _sw_start _sw_stop _sw_total ;;
start)
if [[ ${_sw_start:-} ]]; then
((${_sw_stop:-0} && _sw_start < _sw_stop)) || return
fi
_sw_start=$EPOCHSECONDS
_sw_total+=0
;;
stop)
[[ ${_sw_start:-} ]] || return
if [[ ${_sw_stop:-} ]]; then
((_sw_start > _sw_stop)) || return
fi
_sw_stop=$EPOCHSECONDS
_sw_total+=$((_sw_stop - _sw_start))
;;
show)
[[ ${_sw_start:-} ]] || return
if ((${_sw_stop:-0} && _sw_start < _sw_stop)); then
printf '%d\n' "$_sw_total"
else
printf '%d\n' $((_sw_total + EPOCHSECONDS - _sw_start))
fi
;;
*) return 1 ;;
esac
}
## END: utils.stopwatch_lite
## BEGIN: utils.stopwatch_full
# Synopsis:
# Concurrent stopwatch manager with support for resuming and resetting
#
# Usage:
# utils.stopwatch_full -g
# utils.stopwatch_full [option|number] command
#
# Arguments:
# - [opt] option - An option, see the next section for details
# - [int] number - The index of the stopwatch to interact with, defaults to 0
# - [arg] command - The action to perform, one of: reset|start|stop|show
#
# Options:
# -c Set stopwatch index to the bookmark value (current stopwatch)
# -g Get the current stopwatch bookmark value (get stopwatch)
# -n Increment the stopwatch bookmark by one (next stopwatch)
# -p Decrement the stopwatch bookmark by one (previous stopwatch)
#
# Variables:
# - [int] EPOCHSECONDS - Seconds since the UNIX epoch (Bash builtin)
# - [int] _sw_bookmark - Tracks the index of the last used stopwatch
# - [array] _sw_start - Array of stopwatch start times, in seconds
# - [array] _sw_stop - Array of stopwatch stop times, in seconds
# - [array] _sw_total - Array of stopwatch durations, in seconds
#
# Notes:
# The current duration of a stopwatch can be checked without stopping it.
#
# A stopwatch may be resumed after stopping by starting it again.
#
# This function records and returns values in seconds. Formatting the
# durations can be done with another utility in this library.
#
# See Also:
# - `utils.stopwatch_lite` - Lightweight version, supports a single stopwatch
# - `utils.seconds_to_hms` - Formats a raw duration into HH:mm:ss
#
# Examples:
# - This starts the default stopwatch (0), waits 30s, stops, then shows it
# $ utils.stopwatch_full start; sleep 30
# $ utils.stopwatch_full stop; utils.stopwatch_full show
# - This starts stopwatch 5, waits 10s, then shows it (without stopping)
# $ utils.stopwatch_full 5 start; sleep 10; utils.stopwatch_full 5 show
# - This starts stopwatch 6, waits 10s, then shows stopwatch 6 then 5
# $ utils.stopwatch_full -n start; sleep 10
# $ utils.stopwatch_full show; utils.stopwatch_full -p show
#
# Dependencies: (none)
utils.stopwatch_full() {
(($# > 0)) || return
local -i i=0
declare -gi _sw_bookmark=${_sw_bookmark:-0}
declare -agi _sw_start _sw_stop _sw_total
case $1 in
+([0-9])) _sw_bookmark=$1 i=$1 ;;
-g) printf '%d\n' "$_sw_bookmark" && return ;;
-n) i=$((++_sw_bookmark)) ;;
-p) i=$((--_sw_bookmark)) ;;
-c) i=$_sw_bookmark ;;
esac
shift && (($# > 0)) || return
case $1 in
reset)
unset '_sw_start[i]'
unset '_sw_stop[i]'
unset '_sw_total[i]'
;;
start)
if [[ ${_sw_start[i]:-} ]]; then
((${_sw_stop[i]:-0} && _sw_start[i] < _sw_stop[i])) || return
fi
_sw_start[i]=$EPOCHSECONDS
_sw_total[i]+=0
;;
stop)
[[ ${_sw_start[i]:-} ]] || return
if [[ ${_sw_stop[i]:-} ]]; then
((_sw_start[i] > _sw_stop[i])) || return
fi
_sw_stop[i]=$EPOCHSECONDS
_sw_total[i]+=$((_sw_stop[i] - _sw_start[i]))
;;
show)
[[ ${_sw_start[i]:-} ]] || return
if ((${_sw_stop[i]:-0} && _sw_start[i] < _sw_stop[i])); then
printf '%d\n' "${_sw_total[i]}"
else
printf '%d\n' $((_sw_total[i] + EPOCHSECONDS - _sw_start[i]))
fi
;;
*) return 1 ;;
esac
return 0
}
## END: utils.stopwatch_full
#----------------------------------------------------------------------#
# MARK: End of file
#----------------------------------------------------------------------#
# Return error if not being sourced
[[ ${BASH_SOURCE[0]} != "$0" ]] || return
@AfroThundr3007730
Copy link
Author

AfroThundr3007730 commented Apr 5, 2024

TODO: Add image prep functions from my baseline notes.

Also check out these scripts from the bitnami apache container.

@AfroThundr3007730
Copy link
Author

@AfroThundr3007730
Copy link
Author

AfroThundr3007730 commented Dec 20, 2024

  • Rename utils.common.sh > utils.common.bash
  • Add compatibility section to docs, test all bash versions >=3.0
  • Review pure-bash-bible for additional function ideas
  • Review these docs too
  • Investigate bash loadables

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment