#!/bin/bash -e set -u set +o histexpand shopt -qs failglob set -o pipefail shopt -s expand_aliases export PS4='+(${BASH_SOURCE##*/}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' ## ====================== GLOBALS ====================== ## export g__EXISTS=true g__PATH="${BASH_SOURCE[0]}" g__CALLER_PATH="${BASH_SOURCE[1]:-shell}" # 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=false # True if we should log messages to stderr g__LOGGING_TO_STDERR=true # 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' # List of tags being debugged g__LOGGING_TARGETS=() # Enable all logging targets by default g__LOGGING_ALL_TARGETS=no # Array of g::source slugs that have been imported g__IMPORTED_FILES=() # Paths to search for g::source-able scripts g__SOURCE_DIRS=("$HOME/.g.bash/src" "/etc/g.bash/src") # The filesystem path separator g__PATH_SEPARATOR='/' # The directory where config files are stored g__CONFIG_DIR="$HOME/.config" # The default config file name (see g::config::setDefault and g::config::getDefault) g__CONFIG_DEFAULT_NAME='g.bash' # 'yes' if the framework has been bootstrapped, 'no' otherwise g__BOOTSTRAPPED=no # Array of file paths that should be deleted on exit g__FILES_TO_CLEANUP=() g__APP_NAMES=() g__APP_CURRENT_NAME="app" g__APP_LAST_ARGPARSE='' g__APP_LAST_ALIAS='' g__LOCKING_FILE_DIR="/tmp" g__LOCKING_HOLDS=() g__LOCKING_INTERVAL=0.25 g__DEBUG_EVAL=yes ## # Initialize the framework # function g::internals::bootstrap() { if [ "$g__BOOTSTRAPPED" == "yes" ]; then return $(g::util::true) fi g__PATH="$(g::path::resolve "$g__PATH")" trap g::internals::cleanup EXIT g__BOOTSTRAPPED=yes } ## # Clean up before exit. # function g::internals::cleanup() { if [ "$g__BOOTSTRAPPED" == "no" ]; then echo "Called cleanup before bootstrap!"; exit 1; fi for file in "${g__FILES_TO_CLEANUP[@]}"; do rm -rf "$file" done g::lock::cleanupForExit } ## ====================== UTILITIES ====================== ## function g::eval() { if [[ "$g__DEBUG_EVAL" == yes ]]; then g::log::stderr "eval: $*" fi eval "$@" } function g::silence() { "$@" > /dev/null 2>&1 } # # 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 } function g::arr::assoc::hasKey() { local key="$1" shift local array="$@" for possibleKey in "${!array[@]}"; do if [[ "$key" == "$possibleKey" ]]; then return $(g::util::true) fi done return $(g::util::false) } ## # Join an array of values by some delimiter. # # @param delimiter # @param ...array # @echo the joined string # function g::arr::join() { local delimiter=${1-} local array=${2-} if shift 2; then printf %s "$array" "${@/#/$delimiter}" fi } # # 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 g::eval "$cmd"; then echo 1 else echo 0 fi } ## # Generate a UUID. # # @echo the UUID # function g::util::uuid() { ## https://gist.github.com/markusfisch/6110640 local N B C='89ab' for (( N=0; N < 16; ++N )) do B=$(( $RANDOM%256 )) case $N in 6) printf '4%x' $(( B%16 )) ;; 8) printf '%c%x' ${C:$RANDOM%${#C}:1} $(( B%16 )) ;; 3 | 5 | 7 | 9) printf '%02x-' $B ;; *) printf '%02x' $B ;; esac done } ## # Generate a UUID separated by underscores. # # @echo the UUID # function g::util::uuid::underscore() { ## https://gist.github.com/markusfisch/6110640 local N B C='89ab' for (( N=0; N < 16; ++N )) do B=$(( $RANDOM%256 )) case $N in 6) printf '4%x' $(( B%16 )) ;; 8) printf '%c%x' ${C:$RANDOM%${#C}:1} $(( B%16 )) ;; 3 | 5 | 7 | 9) printf '%02x_' $B ;; *) printf '%02x' $B ;; esac done } ## # Bash-escape a value function g::util::escape() { local value="$1" local escaped="$(echo "$value" | ( read -rsd '' x; echo ${x@Q} ))" echo "$escaped" } # # 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 } function g::path::concat() { local concat="$1" shift local paths=("$@") if g::str::endsWith "$concat" "$g__PATH_SEPARATOR"; then concat="$(g::str::substring "$concat" 0 -1)" fi for pathName in "${paths[@]}"; do pathName="$(g::str::trim "$pathName")" if g::str::endsWith "$pathName" "$g__PATH_SEPARATOR"; then pathName="$(g::str::substring "$pathName" 0 -1)" fi if ! g::str::startsWith "$pathName" "$g__PATH_SEPARATOR"; then pathName="${g__PATH_SEPARATOR}${pathName}" fi concat="${concat}${pathName}" done echo "$concat" } function g::util::realpath() { local target="$1" # Resolve the base path if g::str::startsWith "$target" '..'; then local strlen="$(g::str::length "$target")" target="$(g::str::substring "$target" 2 $strlen)" target="$(dirname $(pwd))${target}" elif g::str::startsWith "$target" '.'; then local strlen="$(g::str::length "$target")" target="$(g::str::substring "$target" 1 $strlen)" target="$(pwd)${target}" elif ! g::str::startsWith "$target" '/'; then target="$(pwd)/${target}" fi # Resolve relative paths local tempname="$(mktemp -d)" local temptarget="${tempname}${target}" mkdir -p "${temptarget}" ( cd "${temptarget}" local temppwd="$(pwd)" local targetLength="$(g::str::length "$target")" local tempLength="$(g::str::length "$tempname")" echo "$(g::str::substring "$temppwd" $tempLength $targetLength)" rm -rf "${temptarget}" ) } function g::path::resolve() { local paths=("$@") local concat="$(g::path::concat "${paths[@]}")" if ! g::str::startsWith "$concat" "$g__PATH_SEPARATOR"; then concat="$(g::util::realpath "$concat")" fi echo "$concat" } function g::path::resolveFrom() { local baseDir="$1" shift local paths=("$@") ( cd "$baseDir" echo "$(g::path::resolve "${paths[@]}")" ) } function g::path::tmp() { local name="$(mktemp)" g__FILES_TO_CLEANUP+=("$name") echo "$name" } function g::path::tmpdir() { local name="$(mktemp -d)" g__FILES_TO_CLEANUP+=("$name") echo "$name" } # # 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}" } ## # Pipe inputs to BC with the math module enabled. # function g::bc() { local inputs="$@" echo "$@" | bc -l } ## # Pipe inputs to an AWK `print` command. function g::awk() { local inputs="$@" echo '' | awk "{ print $inputs }" } ## ====================== STRINGS ====================== ## function g::str::quote() { local string="$1" echo "'$string'" } function g::str::quote::eval() { local cmd="$@" local str="$(g::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::offset() { local string="$1" local startAt="$2" local length="$(g::str::length "$string")" g::str::substring "$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 } function g::str::trim() { local string="$1" echo "$string" | xargs } function g::str::indexOf() { local string="$1" local substring="$2" local length="$(g::str::length "$string")" for (( i=0; i<$length; i++ )); do local sliced="$(g::str::offset "$string" $i)" if g::str::startsWith "$sliced" "$substring"; then printf $i return $(g::util::true) fi done return $(g::util::false) } function g::str::replace() { local string="$1" local find="$2" local replaceWith="${3:-}" printf "${string//${find}/${replaceWith}}" } function g::str::replace::once() { local string="$1" local find="$2" local replaceWith="${3:-}" printf "${string/${find}/${replaceWith}}" } ## ====================== LOGGING ====================== ## # output functions and logging # capture output # silence output # colors # powerline # tables # width-fill function g::log::enableTarget() { local name="$1" g__LOGGING_TARGETS+=("$name") } function g::log::enableAllTargets() { g__LOGGING_ALL_TARGETS=yes } function g::log::all() { g::log::setLevel internal g::log::enableAllTargets } # # 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 target="${3:-}" local targetLength="$(g::str::length "$target")" if [[ $targetLength -gt 0 ]] && [[ $g__LOGGING_ALL_TARGETS == no ]]; then if ! g::arr::includes "$target" "${g__LOGGING_TARGETS[@]}"; then return $(g::util::true) fi fi 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" local target="${2:-}" g::log error "$output" "$target" } # # Log a warning message. # # @param output # function g::warn() { local output="$1" local target="${2:-}" g::log warn "$output" "$target" } # # Log an informational message. # # @param output # function g::info() { local output="$1" local target="${2:-}" g::log info "$output" "$target" } # # Log a debugging message. # # @param output # function g::debug() { local output="$1" local target="${2:-}" g::log debug "$output" "$target" } # # Log a verbose message. # # @param output # function g::verbose() { local output="$1" local target="${2:-}" g::log verbose "$output" "$target" } # # Log a verbose message. # # @param output # function g::internal() { local output="$1" local target="${2:-}" g::log internal "$output" "$target" } 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 CONSTANTS ====================== ## function g::math::c::e() { echo 0.577215665 } function g::math::c::ln2() { echo 0.69314718056 } function g::math::c::ln10() { echo 2.30258509299 } function g::math::c::pi() { echo 3.14159265359 } function g::math::c::sqrt05() { echo 0.70710678119 } function g::math::c::sqrt2() { echo 1.41421356237 } ## ====================== MATH HELPERS ====================== ## function g::math::abs() { local input="$1" echo "${input#-}" } function g::math::cubeRoot() { local input="$1" echo "$input" | awk '{ print $1^(1/3) }' } function g::math::squareRoot() { local input="$1" echo "$input" | awk '{ print $1^(1/2) }' } function g::math::lessThan() { local left="$1" local right="$2" if (( $(echo "$left < $right" | bc -l) )); then return $(g::util::true) else return $(g::util::false) fi } function g::math::greaterThan() { local left="$1" local right="$2" if (( $(echo "$left > $right" | bc -l) )); then return $(g::util::true) else return $(g::util::false) fi } function g::math::equalTo() { local left="$1" local right="$2" if (( $(echo "$left == $right" | bc -l) )); then return $(g::util::true) else return $(g::util::false) fi } function g::math::isPositive() { local input="$1" if g::math::greaterThan "$input" 0; then return $(g::util::true) else return $(g::util::false) fi } function g::math::isNegative() { local input="$1" if g::math::lessThan "$input" 0; then return $(g::util::true) else return $(g::util::false) fi } function g::math::signOf() { local input="$1" if g::math::isNegative "$input"; then echo - else echo + fi } function g::math::mod() { local input="$1" local modulus="$2" echo "oldscale=scale; scale=0; $input%$modulus; scale=oldscale; l(2)" | bc -l | head -n 1 } function g::math::ceiling() { local input="$1" local decimal="$(g::math::mod "$input" 1)" if g::math::equalTo "$decimal" 0; then echo "$input" return fi local plusOne="$(g::bc $input + 1)" echo "$(g::math::truncate "$plusOne")" } function g::math::truncate() { local input="$1" echo "${input%.*}" } function g::math::floor() { local input="$1" echo "$(g::math::truncate "$input")" } function g::math::exponent() { local base="$1" local exponent="$2" echo "$(g::awk $base^$exponent)" } function g::math::cos() { local input="$1" echo "$(g::awk cos\($input\))" } function g::math::sin() { local input="$1" echo "$(g::awk sin\($input\))" } function g::math::tan() { local input="$1" echo "$(g::awk tan\($input\))" } function g::math::ln() { local input="$1" echo "$(g::bc "l($input)")" } function g::math::log10() { local input="$1" echo "$(g::bc "l($input)/l(10)")" } function g::math::log2() { local input="$1" echo "$(g::bc "l($input)/l(2)")" } function g::math::log() { local input="$1" local base="${2:-10}" echo "$(g::bc "l($input)/l($base)")" } function g::math::random() { echo "$(g::bc "scale=16 ; ${RANDOM}/32767")" } function g::math::max() { local array=("$@") local max="${array[0]}" for num in "${array[@]}"; do if g::math::greaterThan "$num" "$max"; then max="$num" fi done echo "$max" } function g::math::min() { local array=("$@") local min="${array[0]}" for num in "${array[@]}"; do if g::math::lessThan "$num" "$min"; then min="$num" fi done echo "$min" } ## # Round a number to the specified precision. # # @param input - the number to round # @param [toScale=0] - the number of decimals to round # @echo the result # function g::math::round() { local input="$1" local toScale="${2:-0}" bc < 0) h=0.5 a=(m * t + h) scale=$toScale a / t MATH } function g::source::resolve() { local path="$1" local fsPath="$(realpath "${path}.bash")" if g::file::exists "$fsPath"; then echo "$fsPath" return $(g::util::true) elif g::file::directoryExists "$fsPath"; then echo "$fsPath" return $(g::util::true) else for dir in "${g__SOURCE_DIRS[@]}"; do local resolved="$(g::path::resolve "$dir" "${path}.bash")" if g::file::exists "$resolved"; then echo "$resolved" return $(g::util::true) elif g::file::directoryExists "$resolved"; then echo "$resolved" return $(g::util::true) fi done fi return $(g::util::false) } function g::source::exists() { local path="$1" local resolved="$(g::source::resolve "$path")" local length="$(g::str::length "$resolved")" if [[ "$length" -gt 0 ]]; then return $(g::util::true) fi return $(g::util::false) } function g::source::force() { local slug="$1" local resolved="$(g::source::resolve "$slug")" if ! g::source::exists "$slug"; then g::error::throw "Cannot resolve source to include: $slug" fi if g::file::exists "$resolved"; then source "$resolved" return $(g::util::true) fi if g::file::directoryExists "$resolved"; then for file in "$(g::path::resolve "$resolved" './*.bash')"; do source "$file" done return $(g::util::true) fi return $(g::util::false) } function g::source::has() { local slug="$1" if g::arr::includes "$slug" "${g__IMPORTED_FILES[@]}"; then return $(g::util::true) fi return $(g::util::false) } function g::source() { local slug="$1" if ! g::source::has "$slug"; then g::source::force "$slug" g__IMPORTED_FILES+=("$slug") fi } ## # Set the default config file name. function g::config::setDefault() { local name="$1" g__CONFIG_DEFAULT_NAME="${name}" } ## # Get the default config file name. function g::config::getDefault() { echo "$g__CONFIG_DEFAULT_NAME" } ## # Initialize a config file with the given name. function g::config::init() { local filename="${1:-$g__CONFIG_DEFAULT_NAME}" if ! g::file::directoryExists "$g__CONFIG_DIR"; then mkdir -p "$g__CONFIG_DIR" fi if ! g::file::exists "$(g::path::resolve "$g__CONFIG_DIR" "$filename")"; then g::file::touch "$(g::path::resolve "$g__CONFIG_DIR" "$filename")" fi } ## # Get a value in the default config file. function g::config::get() { local name="$1" local defaultValue="${2:-}" g::config::init ( local varname="g__config__${name}" source "$(g::path::resolve "$g__CONFIG_DIR" "$g__CONFIG_DEFAULT_NAME")" echo "${!varname:-$defaultValue}" ) } ## # Get a value in a config, by file. function g::config::get::forFile() { local file="$1" shift 1 local args="$@" local oldDefault="$(g::config::getDefault)" g::config::setDefault "$file" local value="$(g::config::get "$@")" g::config::setDefault "$oldDefault" echo "$value" } ## # Set a value in the default config file. function g::config::set() { local name="$1" local value="$2" g::config::init local filepath="$(g::path::resolve "$g__CONFIG_DIR" "$g__CONFIG_DEFAULT_NAME")" local contents="$(cat "$filepath")" local newlines=() ( cat "$filepath" | readarray -t lines for line in "${lines[@]}"; do if ! g::str::startsWith "$line" "g__config__$name"; then newlines+=("$line") fi done ) local escaped="$(g::util::escape "$value")" newlines+=("g__config__$name=$escaped") local newcontents="$(g::arr::join '\n' "${newlines[@]}")" echo "$newcontents" > "$filepath" } ## # Set a value in a config, by file. function g::config::set::forFile() { local file="$1" shift 1 local args="$@" local oldDefault="$(g::config::getDefault)" g::config::setDefault "$file" g::config::set "$@" g::config::setDefault "$oldDefault" } function g::app() { local name="${1:-app}" local description="${2:-}" g__APP_CURRENT_NAME="$name" g__APP_NAMES+=("$name") g::app::set DESCRIPTION "$description" } function g::app::var() { local varname="$1" printf "g__appnamespace__${g__APP_CURRENT_NAME}_${varname}" } function g::app::get() { local varname="$1" local defaultValue="${2:-}" local resolved="$(g::app::var "$varname")" g::internal "[g::app::get] $resolved" g::app::vars echo "${!resolved:-$defaultValue}" } function g::app::set() { local varname="$1" local value="$2" local resolved="$(g::app::var "$varname")" local escaped="$(g::util::escape "$value")" g::internal "[g::app::set] $resolved" g::app::vars g::eval "export ${resolved}=${escaped}" } function g::app::has() { local varname="$1" local resolved="$(g::app::var "$varname")" if [[ -v "$resolved" ]]; then g::internal "[g::app::has] $resolved - YES" g::app::vars return $(g::util::true) fi g::internal "[g::app::has] $resolved - NO" g::app::vars return $(g::util::false) } function g::app::command::var() { local currentCommand="$(g::app::get DEFINE_CURRENT_COMMAND)" local name="$1" local command="${2:-$currentCommand}" printf "COMMAND__${command}__${name}" } function g::app::command::exists() { local name="$1" local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" if g::arr::includes "$name" $(g::eval "echo \"\${$cmdsVar[@]}\""); then return $(g::util::true) fi return $(g::util::false) } function g::app::command::description() { local name="$1" local varname="$(g::app::command::var DESCRIPTION "$name")" g::app::get "$varname" } function g::app::command() { local name="$1" local description="${2:-$name}" g::app::set DEFINE_CURRENT_COMMAND "$name" local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" if ! [[ -v $cmdsVar ]]; then g::eval "export ${cmdsVar}=()" fi g::eval "${cmdsVar}+=(\"$name\")" g::app::set "$(g::app::command::var DESCRIPTION)" "$description" } function g::app::command::arg() { local name="$1" local description="${2:-$name}" local hasDefaultValue=no local defaultValue if [[ -v 3 ]]; then hasDefaultValue=yes defaultValue="$3" fi local currentArgVar="$(g::app::command::var CURRENT_ARG_NUM)" if ! g::app::has "$currentArgVar"; then g::app::set "$currentArgVar" 0 fi local currentArg="$(g::app::get $currentArgVar)" local nextArg=$(( $currentArg + 1 )) g::app::set "$currentArgVar" $nextArg local nameVar="$(g::app::command::var ARG_${nextArg}_NAME)" local descriptionVar="$(g::app::command::var ARG_${nextArg}_DESCRIPTION)" local hasDefaultVar="$(g::app::command::var ARG_${nextArg}_HAS_DEFAULT)" local defaultVar="$(g::app::command::var ARG_${nextArg}_DEFAULT)" g::app::set "$nameVar" "$name" g::app::set "$descriptionVar" "$description" g::app::set "$hasDefaultVar" "$hasDefaultValue" if [[ $hasDefaultValue == 'yes' ]]; then g::app::set "$defaultVar" "$defaultValue" fi } function g::app::command::flag() { local flag="$1" local description="${2:-$name}" local hasValue=no if g::str::endsWith "$flag" =; then hasValue=yes flag="$(g::str::substring "$flag" 0 -1)" fi local currentFlagVar="$(g::app::command::var CURRENT_FLAG_NUM)" if ! g::app::has "$currentFlagVar"; then g::app::set "$currentFlagVar" 0 fi local currentFlag="$(g::app::get $currentFlagVar)" local nextFlag=$(( $currentFlag + 1 )) g::app::set "$currentFlagVar" $nextFlag local flagVar="$(g::app::command::var FLAG_${nextFlag}_NAME)" local descriptionVar="$(g::app::command::var FLAG_${nextFlag}_DESCRIPTION)" local hasValueVar="$(g::app::command::var FLAG_${nextFlag}_HAS_VALUE)" g::internal "Setting flag name: ${flagVar}" g::app::command::parse g::app::set "$flagVar" "$flag" g::app::set "$descriptionVar" "$description" g::app::set "$hasValueVar" "$hasValue" } function g::app::command::flag::getIndexByName() { local flagName="$1" local cmdName="$2" local currentFlagVar="$(g::app::command::var CURRENT_FLAG_NUM "$cmdName")" if ! g::app::has "$currentFlagVar"; then g::app::set "$currentFlagVar" 0 fi local currentFlag="$(g::app::get $currentFlagVar)" for (( i=0; i<=$currentFlag; i++ )); do local flagVar="$(g::app::command::var FLAG_${i}_NAME "${cmdName}")" if g::app::has "$flagVar"; then local possibleFlagName="$(g::app::get "$flagVar")" if [[ "$possibleFlagName" == "$flagName" ]] || [[ "$possibleFlagName" == "${flagName}=" ]] then printf $i return $(g::util::true) fi fi done return $(g::util::false) } function g::app::command::arg::getIndexByName() { local argName="$1" local cmdName="$2" local currentArgVar="$(g::app::command::var CURRENT_ARG_NUM "$cmdName")" if ! g::app::has "$currentArgVar"; then g::app::set "$currentArgVar" 0 fi local currentArg="$(g::app::get $currentArgVar)" for (( i=0; i<=$currentArg; i++ )); do local argVar="$(g::app::command::var ARG_${i}_NAME "${cmdName}")" if g::app::has "$argVar"; then local possibleArgName="$(g::app::get "$argVar")" if [[ "$possibleArgName" == "$argName" ]]; then printf $i return $(g::util::true) fi fi done return $(g::util::false) } function g::app::command::parse::var() { local name="$1" local uuid="$2" printf "COMMAND_PARSE__${uuid}__${name}" } function g::app::command::parse::register::flag() { local flag="$1" local value="$2" local uuid="$3" local varName="$(g::app::command::parse::var "FLAG_${flag}_VALUE" "$uuid")" g::app::set "$varName" "$value" } function g::app::command::parse::get::flag() { local position="$1" local uuid="$2" local varName="$(g::app::command::parse::var "FLAG_${position}_VALUE" "$uuid")" g::internal "Getting: $varName" g::app::command::parse g::app::get "$varName" } function g::app::command::parse::has::flag() { local flag="$1" local uuid="$2" local varName="$(g::app::command::parse::var "FLAG_${flag}_VALUE" "$uuid")" if g::app::has "$varName"; then return $(g::util::true) fi return $(g::util::false) } ## # Register the value of a positional argument in an argument set. # # @param position # @param value # @param uuid - the identifier of the argument set # function g::app::command::parse::register::arg() { local position="$1" local value="$2" local uuid="$3" local varName="$(g::app::command::parse::var "ARG_${position}_VALUE" "$uuid")" g::internal "Registering: $varName" g::app::command::parse g::app::set "$varName" "$value" } ## # Get the value of a positional argument in an argument set. # # @param position - the argument position # @param uuid - the argument set identifier # @echo the argument value # function g::app::command::parse::get::arg() { local position="$1" local uuid="$2" local varName="$(g::app::command::parse::var "ARG_${position}_VALUE" "$uuid")" g::internal "Getting: $varName" g::app::command::parse g::app::get "$varName" } ## # Check if an argument set has an argument for the given position. # # @param position - the argument position # @param uuid - the argument set identifier # @return true if the argument exists # function g::app::command::parse::has::arg() { local position="$1" local uuid="$2" local varName="$(g::app::command::parse::var "ARG_${position}_VALUE" "$uuid")" g::internal "Checking: $varName" g::app::command::parse if g::app::has "$varName"; then return $(g::util::true) fi return $(g::util::false) } ## # Parse a raw argument string for the given command and create a new argument set. # # Sets the argument set identifier in $g__APP_LAST_ARGPARSE. # # @param cmdName - the identifier of the command the arguments are for # @param ...args - the raw arguments # function g::app::command::parse() { local cmdName="$1" shift 1 local args=("$@") local groupUuid=$(g::util::uuid::underscore) local cmdNameVar="$(g::app::command::parse::var "COMMAND_NAME" "$groupUuid")" local allVar="$(g::app::command::parse::var "ALL_ARGS" "$groupUuid")" g::app::set "$allVar" "${args[*]}" g::app::set "$cmdNameVar" "$cmdName" local nextPositionalIndex=1 local trappedFlag=no local trappedFlagName local trappedFlagIndex for arg in "${args[@]}"; do g::internal "Parsing argument: ${arg}" g::app::command::parse local isFlag=no local flagOffset=0 # If the string starts with -- or -, then it is a flag parameter if g::str::startsWith "$arg" "--"; then isFlag=yes flagOffset=2 elif g::str::startsWith "$arg" "-"; then isFlag=yes flagOffset=1 fi if [[ $trappedFlag == yes ]]; then # If we had a trapped flag expecting a value, then set the value g::internal "Got value '$arg' for trapped flag '$trappedFlagName'" g::app::command::parse g::app::command::parse::register::flag "$trappedFlagIndex" "$arg" "$groupUuid" trappedFlag=no continue fi if [[ $isFlag == yes ]]; then local parsedFlag="$(g::str::offset "$arg" $flagOffset)" local parsedValue # Parse out the flag name (remove the -- or -) and split off the setter # (e.g. --flag=setter) if one exists local hasSetter=no local indexOfSetter="$(g::str::indexOf "$parsedFlag" '=')" if g::util::isNumeric $indexOfSetter; then hasSetter=yes parsedValue="$(g::str::offset "$parsedFlag" $(( $indexOfSetter + 1 )))" parsedFlag="$(g::str::substring "$parsedFlag" 0 $indexOfSetter)" fi g::internal "Parsed flag: $parsedFlag (has setter? $hasSetter)" g::app::command::parse if [[ $hasSetter == yes ]]; then g::internal "Parsed value: $parsedValue" g::app::command::parse fi # Look up the index of the flag in the command definition local flagIndex="$(g::app::command::flag::getIndexByName "$parsedFlag" "$cmdName")" g::internal "Flag index: ${flagIndex}" g::app::command::parse # Compute the flag variable names local flagVar="$(g::app::command::var "FLAG_${flagIndex}_NAME" "$cmdName")" local descriptionVar="$(g::app::command::var "FLAG_${flagIndex}_DESCRIPTION" "$cmdName")" local hasValueVar="$(g::app::command::var "FLAG_${flagIndex}_HAS_VALUE" "$cmdName")" g::internal "Flag var: $flagVar" g::app::command::parse # Make sure the flag is defined. Flag name will always exist. if ! g::app::has "$flagVar"; then g::error::throw "Invalid flag: ${parsedFlag}" fi # Check if we've already gotten this flag, to prevent duplicates if g::app::command::parse::has::flag "$flagIndex" "$groupUuid"; then g::error::throw "Duplicate flag specified: ${parsedFlag}" fi # Register the flag local flagHasValue="$(g::app::get "$hasValueVar")" if [[ "$flagHasValue" == yes ]]; then if [[ $hasSetter == yes ]]; then # Register the flag value g::app::command::parse::register::flag "$flagIndex" "$parsedValue" "$groupUuid" else # Set a trap to register the next value as the flag value trappedFlag=yes trappedFlagName="$parsedFlag" trappedFlagIndex="$flagIndex" fi else if [[ $hasSetter == yes ]]; then # Don't allow params for non-param flags g::error::throw "Flag '$parsedFlag' does not accept an argument." else # The flag doesn't accept a parameter, so mark it as true g::app::command::parse::register::flag "$flagIndex" true "$groupUuid" fi fi else # This is a positional argument local currentArgVar="$(g::app::command::var CURRENT_ARG_NUM "${cmdName}")" if ! g::app::has "$currentArgVar"; then g::app::set "$currentArgVar" 0 fi local currentArg="$(g::app::get $currentArgVar)" if (( $nextPositionalIndex > $currentArg )); then g::error::throw "Unexpected positional argument: $arg" fi g::app::command::parse::register::arg $nextPositionalIndex "$arg" "$groupUuid" nextPositionalIndex=$(($nextPositionalIndex + 1)) fi done if [[ $trappedFlag == yes ]]; then # We got a flag expecting a value as the last argument, # but never got an argument for it. g::error::throw "Missing required value for flag: ${trappedFlagName}" fi g::app::command::validate "$cmdName" "$groupUuid" g__APP_LAST_ARGPARSE="$groupUuid" } ## # Validate the parsed arguments for the given argument set. # # @param cmdName - the identifier of the command the arguments are for # @param groupUuid - the identifier of the argument set # function g::app::command::validate() { local cmdName="$1" local groupUuid="$2" # Set defauls for positional arguments and make sure all are present local currentArgVar="$(g::app::command::var CURRENT_ARG_NUM "${cmdName}")" if ! g::app::has "$currentArgVar"; then g::app::set "$currentArgVar" 0 fi local currentArg="$(g::app::get "$currentArgVar")" for (( i=1; i<=$currentArg; i++ )); do g::internal "Considering positional argument: $i" g::app::command::parse local nameVar="$(g::app::command::var ARG_${i}_NAME "$cmdName")" local hasDefaultVar="$(g::app::command::var ARG_${i}_HAS_DEFAULT "$cmdName")" local defaultVar="$(g::app::command::var ARG_${i}_DEFAULT "$cmdName")" if ! g::app::command::parse::has::arg $i "$groupUuid"; then local hasDefault="$(g::app::get "$hasDefaultVar")" if [[ $hasDefault == yes ]]; then local argDefault="$(g::app::get "$defaultVar")" g::app::command::parse::register::arg $i "$argDefault" "$groupUuid" else local argName="$(g::app::get "$nameVar")" g::error::throw "Missing required positional argument: $argName" fi fi done } ## # Print a usage message for the current application. # # @echo the message # function g::app::usage() { local header="$g__APP_CURRENT_NAME" local description="$(g::app::get DESCRIPTION)" if [[ "$(g::str::length "$description")" -gt 0 ]]; then header="${header}: ${description}" fi echo "$header" echo "" echo "Usage: $(basename "$g__CALLER_PATH") [...arguments]" echo "" local commandsString="" local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" for cmdName in $(g::eval "echo \"\${$cmdsVar[@]}\""); do local cmdDescVar="$(g::app::command::var DESCRIPTION "${cmdName}")" local cmdDescription="$(g::app::get "$cmdDescVar")" cmdString=" ${cmdName}: ${cmdDescription}" commandsString="${commandsString}${cmdString} " done commandsString="${commandsString} help: display usage information " echo "Available commands:" echo "" echo "$commandsString" } ## # Invoke a command in the current application with some arguments. # # @param commandName - the identifier of the command # @param ...args - the arguments # function g::app::invoke() { local commandName="${1:-}" shift 1 local args="$@" if [[ "$commandName" == "" ]] || [[ "$commandName" == "help" ]]; then g::app::usage return $(g::util::true) fi local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" for registeredCommandName in $(g::eval "echo \"\${$cmdsVar[@]}\""); do if [[ "$registeredCommandName" == "$commandName" ]]; then g::internal "Matched command: $registeredCommandName" if [[ $(type -t app::${g__APP_CURRENT_NAME}::${registeredCommandName}) != function ]]; then g::debug "No function found with name: app::${g__APP_CURRENT_NAME}::${registeredCommandName}" g::error::throw "No command handler registered for ${registeredCommandName}!" fi ( # parse MUST be called in the SAME subshell as the invoke # because argparse relies on global variables g::app::command::parse "$registeredCommandName" "$@" local groupUuid="$g__APP_LAST_ARGPARSE" g::args::buildAlias "$groupUuid" "app::${g__APP_CURRENT_NAME}::${registeredCommandName}" "$g__APP_LAST_ALIAS" ) return $(g::util::true) fi done g::error "Invalid command: ${commandName}" g::error "Run '$(basename $g__CALLER_PATH) help' to view usage." } ## WIP function g::args::buildAlias() { local uuid="$1" alias g::args::alias::${uuid}="g::args \"$uuid\"" alias g::args::alias::${uuid}::arg="g::arg \"$uuid\"" alias g::args::alias::${uuid}::arg::has="g::arg:has \"$uuid\"" alias g::args::alias::${uuid}::flag="g::flag \"$uuid\"" alias g::args::alias::${uuid}::flag::has="g::flag:has \"$uuid\"" g__APP_LAST_ALIAS="g::args::alias::${uuid}" } ## # Get the raw arguments for a given argument set. # This is akin to $@ in a normal invocation. # # @param uuid - the identifier of the argument set # @echo the raw argument string # function g::args() { local uuid="$1" local allVar="$(g::app::command::parse::var "ALL_ARGS" "$uuid")" if ! g::app::has "$allVar"; then g::error::throw "Invalid argument parse reference: ${uuid}" fi g::app::get "$allVar" } ## # Get the value of a given argument in an argument set. # # @param uuid - the identifier of the argument set # @param nameOrPosition - the argument name or index # @echo the argument value # function g::arg() { local uuid="$1" local nameOrPosition="$2" local cmdNameVar="$(g::app::command::parse::var "COMMAND_NAME" "$uuid")" local cmdName="$(g::app::get "$cmdNameVar")" if ! g::util::isNumeric "$nameOrPosition"; then nameOrPosition="$(g::app::command::arg::getIndexByName "$nameOrPosition" "$cmdName")" else nameOrPosition=$(( $nameOrPosition + 1 )) fi g::internal "Getting positional arg: $nameOrPosition for command: $cmdName" g::arg g::app::command::parse::get::arg "$nameOrPosition" "$uuid" } ## # Check if the given argument set has the given argument. # # @param uuid - the identifier of the argument set # @param nameOrPosition - the argument name or index # @return true if the argument is set # function g::arg::has() { local uuid="$1" local nameOrPosition="$2" local cmdNameVar="$(g::app::command::parse::var "COMMAND_NAME" "$uuid")" local cmdName="$(g::app::get "$cmdNameVar")" if ! g::util::isNumeric "$nameOrPosition"; then nameOrPosition="$(g::app::command::arg::getIndexByName "$nameOrPosition" "$cmdName")" else nameOrPosition=$(( $nameOrPosition + 1 )) fi g::internal "Checking positional arg: $nameOrPosition for command: $cmdName" g::arg return $(g::app::command::parse::has::arg "$nameOrPosition" "$uuid") } ## # Get the value of a given flag in an argument set. # # @param uuid - the identifier of the argument set # @param nameOrPosition - the flag name # @echo the flag value # function g::flag() { local uuid="$1" local nameOrPosition="$2" local cmdNameVar="$(g::app::command::parse::var "COMMAND_NAME" "$uuid")" local cmdName="$(g::app::get "$cmdNameVar")" if ! g::util::isNumeric "$nameOrPosition"; then nameOrPosition="$(g::app::command::flag::getIndexByName "$nameOrPosition" "$cmdName")" else nameOrPosition=$(( $nameOrPosition + 1 )) fi g::internal "Getting flag value: $nameOrPosition for command: $cmdName" g::arg g::app::command::parse::get::flag "$nameOrPosition" "$uuid" } ## # Check if the given argument set has a given flag. # # @param uuid - the identifier of the argument set # @param nameOrPosition - the flag name # @return true if the flag is set # function g::flag::has() { local uuid="$1" local nameOrPosition="$2" local cmdNameVar="$(g::app::command::parse::var "COMMAND_NAME" "$uuid")" local cmdName="$(g::app::get "$cmdNameVar")" if ! g::util::isNumeric "$nameOrPosition"; then nameOrPosition="$(g::app::command::flag::getIndexByName "$nameOrPosition" "$cmdName")" else nameOrPosition=$(( $nameOrPosition + 1 )) fi g::internal "Checking flag value: $nameOrPosition for command: $cmdName" g::arg return $(g::app::command::parse::has::flag "$nameOrPosition" "$uuid") } ## # Try to acquire the given lock. # # @param lockName # @return true if acquired # function g::lock::try() { local lockName="$1" if g::lock::holds "$lockName"; then return $(g::util::true) fi local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")" local tempFile="$(mktemp --suffix .lock)" if g::silence ln "$tempFile" "$lockFile"; then g__LOCKING_HOLDS+=("$lockName") return $(g::util::true) fi return $(g::util::false) } ## # Acquire the given lock, sleeping between retries. # # @param lockName # function g::lock::acquire() { local lockName="$1" while ! g::lock::try "$lockName"; do sleep "$g__LOCKING_INTERVAL" done } ## # Returns true if the current process holds the lock. # # @param lockName # @return true if held # function g::lock::holds() { local lockName="$1" return $(g::arr::includes "$lockName" "${g__LOCKING_HOLDS[@]}") } ## # Release a lock if held. Throw otherwise. # # @param lockName # function g::lock::release() { local lockName="$1" if ! g::lock::holds "$lockName"; then g::error::throw "Cannot release lock '$lockName': not held" fi local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")" rm -f "$lockFile" g__LOCKING_HOLDS=("${g__LOCKING_HOLDS[@]/$lockName}") } ## # Release all held locks before exit. # function g::lock::cleanupForExit() { for lockFile in "${g__LOCKING_HOLDS[@]}"; do g::internal "Releasing lockfile: $lockFile" rm -f "$lockFile" done } ## # Try to acquire the lock for this script's unique caller name. # Throw an error if we are unable to acquire it. # function g::lock::singleton() { if ! g::lock::try "$(g::lock::uniqueCallerName)"; then g::error::throw "Another instance of this script is already running." fi } ## # Acquire the lock for this script's unique caller name. # function g::lock::singleton::acquire() { g::lock::acquire "$(g::lock::uniqueCallerName)" } ## # Generate a file-safe name unique to the caller script. # # @echo the unique name # function g::lock::uniqueCallerName() { g::str::replace "$g__CALLER_PATH" '/' '__' } # dependency declarations # output helpers # exception handling # JSON parsing, INI parsing, rich data types (named parameters & arrays/maps) # sendmail support # Bootstrap the framework g::internals::bootstrap