Last active
December 20, 2024 21:58
-
-
Save AfroThundr3007730/b761bd1a6b2f32a2e97727c7e049e354 to your computer and use it in GitHub Desktop.
Collection of utility functions for bash scripts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
See also: HelperFunctions.psm1
- 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
TODO: Add image prep functions from my baseline notes.
Also check out these scripts from the bitnami apache container.