Skip to content

Instantly share code, notes, and snippets.

@AfroThundr3007730
Last active September 17, 2024 01:45
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.6.5 modified 2024-09-09 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
#----------------------------------------------------------------------#
# The functions in this file 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) 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)
# - Dependencies - (Required) List of command 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
# TODO: Add left/right/center padding functions (and justify?)
# TODO: https://gist.github.com/colatkinson/1c73ffea5a77c2b555cc3d2c581fad42
# TODO: Add with utilities for getting actual file modification date
# TODO: Maybe also ELF/PE version getters too...
#----------------------------------------------------------------------#
# 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() {
[[ $# -eq 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() {
[[ $# -eq 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() {
[[ $# -eq 1 ]] || return
utils.check_bool_true "$1" || utils.check_bool_false "$1" || return
}
## 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() {
[[ $# -eq 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() {
[[ $# -eq 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() {
[[ $# -eq 2 ]] || return 2
local -i a=0 b=0
utils.check_bool_value "$1" || a=$?
utils.check_bool_value "$2" || b=$?
[[ $a -eq 0 && $b -eq 0 ]] || return
}
## 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() {
[[ $# -eq 2 ]] || return 2
local -i a=0 b=0
utils.check_bool_value "$1" || a=$?
utils.check_bool_value "$2" || b=$?
[[ $a -eq 0 || $b -eq 0 ]] || return
}
## 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() {
[[ $# -eq 2 ]] || return 2
local -i a=0 b=0
utils.check_bool_value "$1" || a=$?
utils.check_bool_value "$2" || b=$?
[[ ($a -eq 0 || $b -eq 0) && $a -ne $b ]] || return
}
## END: utils.check_bools_xor
## 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() {
[[ $# -ge 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() {
[[ $# -ge 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
#----------------------------------------------------------------------#
# 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() {
[[ $# -gt 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_full` - The full feature version invoked by this wrapper
#
# Dependencies: utils.check_bool_value, utils.say_lite, utils.say_full
utils.say() {
[[ $# -gt 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() {
[[ $# -gt 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
#
# See Also: `utils.say_full` - The full-featured version of this function
#
# Dependencies: utils.check_bool_value
utils.say_lite() {
[[ $# -gt 0 ]] || return
utils.check_bool_value "${QUIET:-}" ||
printf '%s: %s\n' "$(date -u +%FT%TZ)" "$@"
}
## END: utils.say_lite
## 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() {
[[ $# -gt 0 ]] || return
if [[ $1 == -f ]]; then
[[ $# -eq 2 ]] && shift || return
[[ ! -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() {
[[ $# -gt 0 ]] || return
if [[ $1 == -f ]]; then
[[ $# -eq 2 ]] && shift || return
[[ ! -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() {
[[ $# -eq 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() {
[[ $# -eq 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() {
[[ $# -eq 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() {
[[ $# -eq 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() {
[[ $# -eq 2 ]] || return
ln -fs "$1" "$2" && utils.ensure_file_exists "$1"
}
## END: utils.create_file_symlink
## 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)) -eq 0 ]] &&
printf '%.02d:%.02d:%.02d\n' \
$((in % 86400 / 3600)) $((in % 3600 / 60)) $((in % 60)) ||
printf '%d.%.02d:%.02d:%.02d\n' $((in / 86400)) \
$((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() {
[[ $# -eq 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() {
[[ $# -eq 2 ]] || return
utils.copy_function "$@" || return
unset -f "$1"
}
# END: utils.rename_function
# 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
#
# Arguments:
# -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() {
[[ $# -eq 6 ]] || return
while [[ $# -gt 0 ]]; do
[[ $1 =~ ^-(t|f|p)$ ]] || return
[[ $1 == -t ]] && local tag=$2 && shift 2
[[ $1 == -f ]] && local file=$2 && shift 2
[[ $1 == -p ]] && local regex=$2 && 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() {
[[ $# -eq 8 ]] || return
while [[ $# -gt 0 ]]; do
[[ $1 =~ ^-(t|f|o|n)$ ]] || return
[[ $1 == -t ]] && local tag=$2 && shift 2
[[ $1 == -f ]] && local file=$2 && shift 2
[[ $1 == -o ]] && local old=$2 && shift 2
[[ $1 == -n ]] && local new=$2 && 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() {
[[ $# -eq 1 || $# -eq 2 ]] || return
[[ $# -eq 1 ]] && local tag=MODIFIED file=$1
[[ $# -eq 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() {
[[ $# -eq 2 || $# -eq 3 ]] || return
[[ $# -eq 2 ]] && local tag=MODIFIED when=$1 file=$2
[[ $# -eq 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() {
[[ $# -eq 1 || $# -eq 2 ]] || return
[[ $# -eq 1 ]] && local tag=VERSION file=$1
[[ $# -eq 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() {
[[ $# -eq 2 || $# -eq 3 ]] || return
[[ $# -eq 2 ]] && local tag=VERSION type=$1 file=$2
[[ $# -eq 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}
[[ $type == major ]] && major+=1 minor=0 patch=0 rc=0
[[ $type == minor ]] && minor+=1 patch=0 rc=0
[[ $type == patch ]] && patch+=1 rc=0
[[ $type == rc ]] && rc+=1
[[ $rc -eq 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() {
[[ $# -eq 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() {
[[ $# -eq 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() {
[[ $# -ge 2 ]] || return
local cdelim='#' ctemp delim include=0 quote r1 r2
while [[ $# -gt 2 ]]; do
[[ $1 =~ ^-(d|i|q|Q)$ ]] || return
[[ $1 == -d ]] && delim=$2 && shift 2
[[ $1 == -i ]] && include=1 && shift
[[ $1 == -q ]] && quote='"' && shift
[[ $1 == -Q ]] && quote="'" && shift
done
[[ $# -eq 2 ]] || return
[[ $include -eq 1 ]] && 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() {
[[ $# -ge 3 ]] || return
local append cdelim='#' ctemp delim iflag old_opt qflag quote r1 r2
while [[ $# -gt 3 ]]; do
[[ $1 =~ ^-(a|d|i|q|Q)$ ]] || return
[[ $1 == -a ]] && append=1 && shift
[[ $1 == -d ]] && delim=$2 && shift 2
[[ $1 == -i ]] && iflag='-i' && shift
[[ $1 == -q ]] && quote='"' qflag='-q' && shift
[[ $1 == -Q ]] && quote="'" qflag='-Q' && shift
done
[[ $# -eq 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:
# - $ utils.set_global_variable GV CFG MY_DISTRO 'Fedora Rawhide'
# This sets GV_MY_DISTRO to (in order of precedence):
# - The value of `GV_MY_DISTRO` if set
# - The value of `GV_CFG_MY_DISTRO` if set
# - The literal value 'Fedora Rawhide'
#
# Dependencies: (none)
utils.set_global_variable() {
[[ $# -eq 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:
# - $ utils.set_global_array_variable GV CFG MY_DISTRO 'Fedora' 'Rawhide'
# This sets GV_MY_DISTRO to (in order of precedence):
# - The value of `GV_MY_DISTRO` if set
# - The value of `GV_CFG_MY_DISTRO` if set
# - The literal value ('Fedora' 'Rawhide')
#
# Dependencies: (none)
utils.set_global_array_variable() {
[[ $# -ge 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() {
[[ $# -le 2 ]] || return
local base decimal fraction length power suffix types value
[[ ${value:=${1:-}} ]] || read -r value
[[ $value =~ ^([0-9]+)?\.?([0-9]+)?([A-Za-z]+)?$ ]] || return
decimal=${BASH_REMATCH[1]:-0}
fraction=${BASH_REMATCH[2]:-0}
suffix=${BASH_REMATCH[3]:-}
[[ $value =~ \. && ${suffix:-} =~ ^B?$ ]] && return 1
[[ ${suffix:-} =~ ^((K|M|G|T|P|E|Z|Y)i)?B$ ]] &&
base=1024 types=(B KiB MiB GiB TiB PiB EiB ZiB YiB)
[[ ${suffix:-} =~ ^(k|M|G|T|P|E|Z|Y)?B$ ]] &&
base=1000 types=(B kB MB GB TB PB EB ZB YB)
[[ ${suffix:-} =~ ^(K|M|G|T|P|E|Z|Y)?$ ]] &&
base=1024 types=('' K M G T P E Z Y)
[[ ${base:-} ]] || return
while [[ ${power:=0} -lt $(
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() {
[[ $# -le 2 ]] || return
local base decimal fraction power types value
[[ ${1:-} == --iec ]] && shift &&
base=1024 types=(B KiB MiB GiB TiB PiB EiB ZiB YiB)
[[ ${1:-} == --si ]] && shift &&
base=1000 types=(B kB MB GB TB PB EB ZB YB)
[[ ${1:-} == --tr ]] && shift
[[ ${types:-} ]] ||
base=1024 types=('' K M G T P E Z Y)
[[ ${value:=${1:-}} ]] || read -r value
[[ $value =~ ^[0-9]+B?$ ]] && value=${value%B} || return
while [[ ${decimal:=$value} -ge $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
# 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'
# 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
#
# Examples:
# - $ utils.say_full -s my_log_file
# This sets log_file to `my_log_file`, leaving log_verbose unset
# - $ utils.say_full --warn 'Can't find the file: %s' "$my_file"
# This logs prints a warning message to the console
# - $ utils.say_full -d 'Files to be written: %s' "${list[*]}"
# This prints a debug message to the console (if DEBUG is set)
#
# Dependencies: utils.check_bool_value
# shellcheck disable=SC2059
utils.say_full() {
[[ $# -gt 0 ]] || return
[[ $1 == -s || ${say_log_main:-} ]] || ${FUNCNAME[0]} -s
printf '\e[m'
if [[ $1 == -s ]]; then
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")
[[ $# -ge 4 ]] && declare -ag say_tee=("${@:4}")
elif [[ $1 =~ -h|--help ]]; then
printf "\e[34m${2:-}\n" "${@:3}"
else
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
[[ $1 == -n ]] && log=false && shift
[[ $1 == -T ]] && : >"$say_log_main" && shift
[[ $1 == -t ]] && : >"$say_log_verb" && shift
[[ $1 == -x ]] && die=true && shift
[[ $# -gt 0 ]] || return 0
if [[ $1 =~ $regex ]]; then
if [[ $1 =~ -d|--debug ]]; then
utils.check_bool_value "${DEBUG:-}" || return 0
printf '\e[94m' && tag='DEBUG'
elif [[ $1 =~ -i|--info ]]; then
utils.check_bool_value "${VERBOSE:=${DEBUG:-}}" || return 0
printf '\e[32m' && tag='INFO'
elif [[ $1 =~ -w|--warn ]]; then
printf '\e[33m' && tag='WARN' fd=2
elif [[ $1 =~ -e|--error ]]; then
printf '\e[31m' && tag='ERROR' fd=2
elif [[ $1 =~ -f|--fatal ]]; then
printf '\e[91m' && tag='FATAL' fd=2
fi
shift
fi
format="$(date -u +%FT%TZ): ${tag}: ${1}\n"
utils.check_bool_value "${SILENT:-}" && fd=/dev/null
if [[ $log == true && $say_log_main != /dev/null ]]; then
printf "$format" "${@:2}" | "${say_tee[@]}" >&"$fd"
else
printf "$format" "${@:2}" >&"$fd"
fi
fi
printf '\e[m'
[[ ${tag:-} == FATAL && ${die:-} == true ]] && exit 1
}
## 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() {
[[ $# -gt 0 ]] || return
declare -ig sw_start sw_stop sw_total
if [[ $1 == reset ]]; then
unset sw_start sw_stop sw_total
elif [[ $1 == start ]]; then
if [[ ${sw_start:-} ]]; then
[[ ${sw_stop:-} && $sw_start -lt $sw_stop ]] || return
fi
sw_start=$EPOCHSECONDS
sw_total+=0
elif [[ $1 == stop ]]; then
[[ ${sw_start:-} ]] || return
if [[ ${sw_stop:-} ]]; then
[[ $sw_start -gt $sw_stop ]] || return
fi
sw_stop=$EPOCHSECONDS
sw_total+=$((sw_stop - sw_start))
elif [[ $1 == show ]]; then
[[ ${sw_start:-} ]] || return
if [[ ${sw_stop:-} && $sw_stop -gt $sw_start ]]; then
printf '%d\n' "$sw_total"
else
printf '%d\n' $((sw_total + EPOCHSECONDS - sw_start))
fi
else
return 1
fi
}
## 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:
# - $ utils.stopwatch_full start; sleep 30; utils.stopwatch_full stop;
# utils.stopwatch_full show
# This starts the default stopwatch (0), waits 30s, stops, then shows it
# - $ utils.stopwatch_full 5 start; sleep 10; utils.stopwatch_full 5 show
# This starts stopwatch 5, waits 10s, then shows it (without stopping)
#
# Dependencies: (none)
utils.stopwatch_full() {
[[ $# -gt 0 ]] || return
local -i i=0
declare -gi sw_bookmark=${sw_bookmark:-0}
declare -agi sw_start sw_stop sw_total
[[ $1 =~ ^[0-9]+$ ]] &&
sw_bookmark=$1 i=$1 && shift
if [[ $1 == -g ]]; then
printf '%d\n' $sw_bookmark
elif [[ $1 == -n ]]; then
i=$((++sw_bookmark)) && shift
elif [[ $1 == -p ]]; then
i=$((--sw_bookmark)) && shift
elif [[ $1 == -c ]]; then
i=$sw_bookmark && shift
fi
[[ $# -gt 0 ]] || return
if [[ $1 == reset ]]; then
unset 'sw_start[i]'
unset 'sw_stop[i]'
unset 'sw_total[i]'
elif [[ $1 == start ]]; then
if [[ ${sw_start[i]:-} ]]; then
[[ ${sw_start[i]} -lt ${sw_stop[i]:-} ]] || return
fi
sw_start[i]=$EPOCHSECONDS
sw_total[i]+=0
elif [[ $1 == stop ]]; then
[[ ${sw_start[i]:-} ]] || return
if [[ ${sw_stop[i]:-} ]]; then
[[ ${sw_start[i]} -gt ${sw_stop[i]} ]] || return
fi
sw_stop[i]=$EPOCHSECONDS
sw_total[i]+=$((sw_stop[i] - sw_start[i]))
elif [[ $1 == show ]]; then
[[ ${sw_start[i]:-} ]] || return
if [[ ${sw_stop[i]:-} && ${sw_stop[i]} -gt ${sw_start[i]} ]]; then
printf '%d\n' "${sw_total[i]}"
else
printf '%d\n' $((sw_total[i] + EPOCHSECONDS - sw_start[i]))
fi
else
return 1
fi
return 0
}
## END: utils.stopwatch_full
#----------------------------------------------------------------------#
# MARK: End of file
#----------------------------------------------------------------------#
@AfroThundr3007730
Copy link
Author

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