You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2234 lines
46 KiB

#!/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 <<MATH
h=0
scale=0
/* the magnitude of the result scale */
t=(10 ^ $toScale)
/* work with an extra digit */
scale=$toScale + 1
/* your math into var: m */
m=($input)
/* rounding and output */
if (m < 0) h=-0.5
if (m > 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") <command> [...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