#!/bin/bash -e set -u set +o histexpand shopt -qs failglob shopt -s expand_aliases ## ====================== GLOBALS ====================== ## export g__EXISTS=true # The global logging level. Lower the number, higher the priority. g__LOGGING_LEVEL=2 # True if we should log messages to stdout g__LOGGING_TO_STDOUT=true # True if we should log messages to stderr g__LOGGING_TO_STDERR=false # True if we should log messages to a file g__LOGGING_TO_FILE=false # If file logging is enabled, the absolute path to the file g__LOGGING_FILE='g.log' ## ====================== UTILITIES ====================== ## # # Check if an input value is numeric. (Supports decimals.) # # @param value - the value to check # @return 0 if numeric, 1 otherwise # function g::util::isNumeric() { local value="$1" local rex='^[+-]?[0-9]+([.][0-9]+)?$' if [[ "$value" =~ $rex ]]; then return 0 # true else return 1 # false fi } # # Returns true if the given array contains a value. # # e.g. `g::arr::includes 'value' "${array[@]}"` # # @param value - the value to search for # @param array - the array, by value # @return 0 if included, 1 otherwise # function g::arr::includes() { local value="$1" shift local array=("$@") for element in "${array[@]}"; do if [[ $element == "$value" ]]; then return 0 fi done return 1 } # # Canonical true return value. # @return true # function g::util::true() { return 0; } # # Canonical false return value. # @return false # function g::util::false() { return 1; } # # Cast the return code of the command to a boolean echo output. # # @param ...cmd - the command to execute # @echo 1 if return is true, 0 otherwise # function g::util::returnToEcho() { local cmd="$@" if eval "$cmd"; then echo 1 else echo 0 fi } # # Append the input string to a given file. # # @param filename # @param string # function g::file::appendString() { local filename="$1" local string="$2" echo "$string" >> "$filename" } function g::file::exists() { local filename="$1" if [[ -f "$filename" ]]; then return $(g::util::true) else return $(g::util::false) fi } function g::file::directoryExists() { local filename="$1" if [[ -d "$filename" ]]; then return $(g::util::true) else return $(g::util::false) fi } function g::file::touch() { local filename="$1" echo '' >> "$filename" } function g::file::truncate() { local filename="$1" if g::file::exists "$filename"; then echo '' > "$filename" fi } # # Throw a program error. Exits. # @todo move to g::err # function g::error::throw() { local message="$1" echo "$message" 1>&2 exit 1 } # # Get the current date in UTC format. # # @echo the date string # function g::now() { date -u +"%Y-%m-%dT%H:%M:%SZ" } # # Get a stack trace. # # @param message - optional message to include # @echo the stack trace # function g::util::trace() { local stack="" local i message="${1:-""}" local stack_size=${#FUNCNAME[@]} # to avoid noise we start with 1 to skip the trace function for (( i=1; i<$stack_size; i++ )); do local func="${FUNCNAME[$i]}" [ x$func = x ] && func=MAIN local linen="${BASH_LINENO[$(( i - 1 ))]}" local src="${BASH_SOURCE[$i]}" [ x"$src" = x ] && src=non_file_source stack+=$'\n'" at: "$func" "$src" "$linen done stack="${message}${stack}" echo "${stack}" } ## ====================== STRINGS ====================== ## function g::str::quote() { local string="$1" echo "'$string'" } function g::str::quote::eval() { local cmd="$@" local str="$(eval "$cmd")" echo "$(g::str::quote "$str")" } function g::str::length() { local string="$1" echo "$(expr length "$string")" } function g::str::padLeft() { local string="$1" local padToLength="$2" local padCharacter="${3:- }" local length="$(expr length "$string")" while [[ "$length" -lt "$padToLength" ]]; do string="${padCharacter}${string}" length="$(expr length "$string")" done echo "$string" } function g::str::padRight() { local string="$1" local padToLength="$2" local padCharacter="${3:- }" local length="$(expr length "$string")" while [[ "$length" -lt "$padToLength" ]]; do string="${string}${padCharacter}" length="$(expr length "$string")" done echo "$string" } function g::str::padCenter() { local string="$1" local padToLength="$2" local padCharacter="${3:- }" local length="$(expr length "$string")" local leftSide=1 while [[ "$length" -lt "$padToLength" ]]; do if [[ "$leftSide" -eq "1" ]]; then string="${padCharacter}${string}" leftSide=0 else string="${string}${padCharacter}" leftSide=1 fi length="$(expr length "$string")" done echo "$string" } function g::str::substring() { local string="$1" local startAt="$2" local length="$3" echo "${string:$startAt:$length}" } function g::str::reverse() { local input="$1" local output="" local length="$(g::str::length "$input")" for (( i=$length; i>=0; i-- )); do output="${output}${input:$i:1}" done echo "${output}" } function g::str::startsWith() { local string="$1" local startsWith="$2" local startsLength="$(g::str::length "$startsWith")" local substring="$(g::str::substring "$string" 0 $startsLength)" if [[ "$substring" == "$startsWith" ]]; then return $(g::util::true) else return $(g::util::false) fi } function g::str::endsWith() { local string="$1" local endsWith="$2" local revString="$(g::str::reverse "$string")" local revEndsWith="$(g::str::reverse "$endsWith")" if g::str::startsWith "$revString" "$revEndsWith"; then return $(g::util::true) else return $(g::util::false) fi } # string helpers # string split ## ====================== LOGGING ====================== ## # output functions and logging # capture output # silence output # colors # powerline # tables # width-fill # # Convert a string-form logging level to its numeric form. # # @param name # @echo the numeric form # function g::log::stringNameToNumber() { local name="$1" if [ "$name" = "error" ]; then echo 1 elif [ "$name" = "warn" ]; then echo 2 elif [ "$name" = "info" ]; then echo 3 elif [ "$name" = "debug" ]; then echo 4 elif [ "$name" = "verbose" ]; then echo 5 elif [ "$name" = "internal" ]; then echo 6 else echo 0 fi } # # Convert the numeric logging level to its string name. # # @param level # @echo the string name # function g::log::numberToStringName() { local level="$1" if [ "$level" = "1" ]; then echo error elif [ "$level" = "2" ]; then echo warn elif [ "$level" = "3" ]; then echo info elif [ "$level" = "4" ]; then echo debug elif [ "$level" = "5" ]; then echo verbose elif [ "$level" = "6" ]; then echo internal else echo none fi } # # Normalize a value that may be either a string- or numeric-form # logging level to a numeric form. # # @param level # @echo the numeric form # function g::log::normalizeLevelToNumber() { local level="$1" if g::util::isNumeric "$level"; then echo $level else echo $(g::log::stringNameToNumber $level) fi } # # Get the current logging level, as a string. # # @echo the string name # function g::log::getLevel() { echo $(g::log::numberToStringName $g__LOGGING_LEVEL) } # # Set the logging level by string name. # # @param name # function g::log::setLevel() { local name="$1" local level=$(g::log::stringNameToNumber "$name") g__LOGGING_LEVEL=$level } # # Enable a particular logging format. # # @param target - stdout | stderr | file # @param param - if target is "file", the path of the file to log to # @throws if the target is invalid # function g::log::enable() { local target="$1" local param="$2" if [[ $target == 'stdout' ]]; then g__LOGGING_TO_STDOUT=true elif [[ $target == 'stderr' ]]; then g__LOGGING_TO_STDERR=true elif [[ $target == 'file' ]]; then g__LOGGING_TO_FILE=true g__LOGGING_FILE="$(realpath "${param:-$g__LOGGING_FILE}")" g::file::appendString "$g__LOGGING_FILE" "$(g::log::format 3 "====== Logging started. ======")" else g::error::throw "Invalid logging target: ${target}" fi } # # Disable a particular logging format. # # @param target - stdout | stderr | file # @throws if the target is invalid # function g::log::disable() { local target="$1" if [[ $target == 'stdout' ]]; then g__LOGGING_TO_STDOUT=false elif [[ $target == 'stderr' ]]; then g__LOGGING_TO_STDERR=false elif [[ $target == 'file' ]]; then g__LOGGING_TO_FILE=false else g::error::throw "Invalid logging target: ${target}" fi } # # Write to standard output. # # @param output # function g::log::stdout() { local output="$1" echo "$output" } # # Write to standard error. # # @param output # function g::log::stderr() { local output="$1" echo "$output" 1>&2 } # # Write to the log, on all configured formats. # # @param output # function g::log::write() { local output="$1" if $g__LOGGING_TO_STDOUT; then g::log::stdout "$output" fi if $g__LOGGING_TO_STDERR; then g::log::stderr "$output" fi if $g__LOGGING_TO_FILE; then g::file::appendString "$g__LOGGING_FILE" "$output" fi } # # Format an output string with logging information like log level and date. # # @param level - the numeric logging level # @param output # function g::log::format() { local level="$1" local output="$2" local levelDisplay="[$(g::log::numberToStringName $level)]" local paddedLevelDisplay="$(g::str::padLeft "$levelDisplay" 10)" echo "$paddedLevelDisplay [$(g::now)] $output" } # # Log a message. # # @param inputLevel # @param output # function g::log() { local inputLevel="$1" local output="$2" local level="$(g::log::normalizeLevelToNumber $inputLevel)" if [ $level -le $g__LOGGING_LEVEL ]; then g::log::write "$(g::log::format $level "$output")" fi } # # Log an error message. # # @param output # function g::error() { local output="$1" g::log error "$output" } # # Log a warning message. # # @param output # function g::warn() { local output="$1" g::log warn "$output" } # # Log an informational message. # # @param output # function g::info() { local output="$1" g::log info "$output" } # # Log a debugging message. # # @param output # function g::debug() { local output="$1" g::log debug "$output" } # # Log a verbose message. # # @param output # function g::internal() { local output="$1" g::log verbose "$output" } # # Log a verbose message. # # @param output # function g::internal() { local output="$1" g::log internal "$output" } function g::log::rotate() { local logfile="$g__LOGGING_FILE" if g::file::exists "$logfile"; then cp "$logfile" "${logfile}-$(g::now)" g::file::truncate "$logfile" fi } ## ====================== ERROR HANDLING ====================== ## declare -ig g__INSIDE_TRY_CATCH=0 declare -g g__PRESET_SHELL_OPTS="$-" alias try='[[ $g__INSIDE_TRY_CATCH -eq 0 ]] || g__PRESET_SHELL_OPTS="$(echo $- | sed 's/[is]//g')"; g__INSIDE_TRY_CATCH+=1; set +e; ( set -e; true; ' alias catch='); declare g__TRY_RESULT=$?; g__INSIDE_TRY_CATCH+=-1; [[ $g__INSIDE_TRY_CATCH -lt 1 ]] || set -${g__PRESET_SHELL_OPTS:-e} && g::err::parse $g__TRY_RESULT || ' function g::err::parse() { local tryCode="$1" unset g__TRY_RESULT; if [[ $tryCode -gt 0 ]]; then local IFS=$'\n' return $tryCode else return 0 fi } # math helpers # dependency declarations # package/script imports & singleton requires # output helpers # argument parsing # try-catch & exception handling # named parameters & arrays/maps # JSON parsing, INI parsing, rich data types