General fixes & improvements:

- Make isNumeric accept optional first argument
- Support rest params for commands to collect all remaining inputs
- Support switching apps if the app name already exists for g::app
- Make g::source look for files relative to the source file where it was
  called
- Add ability to override $g__CALLER_PATH
- Make g::source store resolved file paths, not file slugs
- Print a full stack trace on g::error::throw
master
Garrett Mills 3 years ago
parent f2884d4606
commit 9fe327a78a
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246

@ -56,21 +56,29 @@ g__BOOTSTRAPPED=no
# Array of file paths that should be deleted on exit # Array of file paths that should be deleted on exit
g__FILES_TO_CLEANUP=() g__FILES_TO_CLEANUP=()
# Names of registered app namespaces
g__APP_NAMES=() g__APP_NAMES=()
# The currently active app namespace
g__APP_CURRENT_NAME="app" g__APP_CURRENT_NAME="app"
# UUID of the last argument set parsed
g__APP_LAST_ARGPARSE='' g__APP_LAST_ARGPARSE=''
# Prefix of the last argument set alias generated
g__APP_LAST_ALIAS='' g__APP_LAST_ALIAS=''
# Path to the directory where lock files can be created
g__LOCKING_FILE_DIR="/tmp" g__LOCKING_FILE_DIR="/tmp"
# Array of lock files held by this process
g__LOCKING_HOLDS=() g__LOCKING_HOLDS=()
# Amount of time (in seconds) to sleep between failed lock attempts
g__LOCKING_INTERVAL=0.25 g__LOCKING_INTERVAL=0.25
g__DEBUG_EVAL=yes # If 'yes' eval calls will be logged to the console
g__DEBUG_EVAL=no
@ -105,20 +113,27 @@ function g::internals::cleanup() {
g::lock::cleanupForExit g::lock::cleanupForExit
} }
##
# Override the top-level caller reference.
#
function g::internals::setCaller() {
local caller="$1"
g__CALLER_PATH="$(g::util::realpath $caller)"
}
## ====================== UTILITIES ====================== ## ## ====================== UTILITIES ====================== ##
function g::eval() { function g::eval() {
if [[ "$g__DEBUG_EVAL" == yes ]]; then if [[ "$g__DEBUG_EVAL" == yes ]]; then
g::log::stderr "eval: $*" g::log::stderr "eval: $*"
fi fi
eval "$@" eval "$@"
} }
function g::silence() { function g::silence() {
"$@" > /dev/null 2>&1 "$@" > /dev/null 2>&1
} }
# #
@ -128,7 +143,7 @@ function g::silence() {
# @return 0 if numeric, 1 otherwise # @return 0 if numeric, 1 otherwise
# #
function g::util::isNumeric() { function g::util::isNumeric() {
local value="$1" local value="${1:-}"
local rex='^[+-]?[0-9]+([.][0-9]+)?$' local rex='^[+-]?[0-9]+([.][0-9]+)?$'
@ -163,17 +178,17 @@ function g::arr::includes() {
} }
function g::arr::assoc::hasKey() { function g::arr::assoc::hasKey() {
local key="$1" local key="$1"
shift shift
local array="$@" local array="$@"
for possibleKey in "${!array[@]}"; do for possibleKey in "${!array[@]}"; do
if [[ "$key" == "$possibleKey" ]]; then if [[ "$key" == "$possibleKey" ]]; then
return $(g::util::true) return $(g::util::true)
fi fi
done done
return $(g::util::false) return $(g::util::false)
} }
## ##
@ -417,6 +432,11 @@ function g::path::resolveFrom() {
) )
} }
function g::path::cd() {
local resolved="$(g::path::resolve "$@")"
cd "$resolved"
}
function g::path::tmp() { function g::path::tmp() {
local name="$(mktemp)" local name="$(mktemp)"
g__FILES_TO_CLEANUP+=("$name") g__FILES_TO_CLEANUP+=("$name")
@ -435,7 +455,10 @@ function g::path::tmpdir() {
# #
function g::error::throw() { function g::error::throw() {
local message="$1" local message="$1"
echo "$message" 1>&2 echo ""
echo ""
g::util::trace "ERROR: $message" 2 1>&2
echo ""
exit 1 exit 1
} }
@ -457,10 +480,11 @@ function g::now() {
function g::util::trace() { function g::util::trace() {
local stack="" local stack=""
local i message="${1:-""}" local i message="${1:-""}"
local skip="${2:-1}"
local stack_size=${#FUNCNAME[@]} local stack_size=${#FUNCNAME[@]}
# to avoid noise we start with 1 to skip the trace function # to avoid noise we start with 1 to skip the trace function
for (( i=1; i<$stack_size; i++ )); do for (( i=$skip; i<$stack_size; i++ )); do
local func="${FUNCNAME[$i]}" local func="${FUNCNAME[$i]}"
[ x$func = x ] && func=MAIN [ x$func = x ] && func=MAIN
@ -470,7 +494,7 @@ function g::util::trace() {
[ x"$src" = x ] && src=non_file_source [ x"$src" = x ] && src=non_file_source
stack+=$'\n'" at: "$func" "$src" "$linen stack+=$'\n'" at ($func) in $src (line $linen)"
done done
stack="${message}${stack}" stack="${message}${stack}"
@ -645,19 +669,19 @@ function g::str::indexOf() {
} }
function g::str::replace() { function g::str::replace() {
local string="$1" local string="$1"
local find="$2" local find="$2"
local replaceWith="${3:-}" local replaceWith="${3:-}"
printf "${string//${find}/${replaceWith}}" printf "${string//${find}/${replaceWith}}"
} }
function g::str::replace::once() { function g::str::replace::once() {
local string="$1" local string="$1"
local find="$2" local find="$2"
local replaceWith="${3:-}" local replaceWith="${3:-}"
printf "${string/${find}/${replaceWith}}" printf "${string/${find}/${replaceWith}}"
} }
@ -1256,34 +1280,42 @@ MATH
function g::source::resolve() { function g::source::resolve() {
local path="$1" local path="$1"
local up="${2:-1}"
local fsPath="$(realpath "${path}.bash")" (
if g::file::exists "$fsPath"; then local reldir="$(dirname ${BASH_SOURCE[$up]})"
echo "$fsPath" g::internal "Changing to dir to resolve source: $reldir"
return $(g::util::true) g::path::cd "$reldir"
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) 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() { function g::source::exists() {
local path="$1" local path="$1"
local up="${2:-1}"
local resolved="$(g::source::resolve "$path")" local resolved="$(g::source::resolve "$path" $(( up + 1 )))"
local length="$(g::str::length "$resolved")" local length="$(g::str::length "$resolved")"
if [[ "$length" -gt 0 ]]; then if [[ "$length" -gt 0 ]]; then
@ -1295,9 +1327,10 @@ function g::source::exists() {
function g::source::force() { function g::source::force() {
local slug="$1" local slug="$1"
local up="${2:-1}"
local resolved="$(g::source::resolve "$slug")" local resolved="$(g::source::resolve "$slug" $(( up + 1 )))"
if ! g::source::exists "$slug"; then if ! g::source::exists "$slug" $(( up + 1 )); then
g::error::throw "Cannot resolve source to include: $slug" g::error::throw "Cannot resolve source to include: $slug"
fi fi
@ -1319,8 +1352,10 @@ function g::source::force() {
function g::source::has() { function g::source::has() {
local slug="$1" local slug="$1"
local up="${2:-1}"
if g::arr::includes "$slug" "${g__IMPORTED_FILES[@]}"; then local resolved="$(g::source::resolve "$slug" $(( up + 1 )))"
if g::arr::includes "$resolved" "${g__IMPORTED_FILES[@]}"; then
return $(g::util::true) return $(g::util::true)
fi fi
@ -1329,10 +1364,11 @@ function g::source::has() {
function g::source() { function g::source() {
local slug="$1" local slug="$1"
local up="${2:-1}"
if ! g::source::has "$slug"; then if ! g::source::has "$slug" $(( up + 1 )); then
g::source::force "$slug" g::source::force "$slug" $(( up + 1 ))
g__IMPORTED_FILES+=("$slug") g__IMPORTED_FILES+=("$resolved")
fi fi
} }
@ -1448,9 +1484,10 @@ function g::app() {
local description="${2:-}" local description="${2:-}"
g__APP_CURRENT_NAME="$name" g__APP_CURRENT_NAME="$name"
g__APP_NAMES+=("$name") if ! g::arr::includes "$name" "${g__APP_NAMES[@]}"; then
g__APP_NAMES+=("$name")
g::app::set DESCRIPTION "$description" g::app::set DESCRIPTION "$description"
fi
} }
function g::app::var() { function g::app::var() {
@ -1533,6 +1570,26 @@ function g::app::command() {
g::app::set "$(g::app::command::var DESCRIPTION)" "$description" g::app::set "$(g::app::command::var DESCRIPTION)" "$description"
} }
function g::app::command::rest() {
local name="$1"
local description="${2:-$name}"
local restVarName="$(g::app::command::var REST_ARG_NAME)"
local restVarDescription="$(g::app::command::var REST_ARG_DESCRIPTION)"
if ! g::app::has "$restVarName"; then
g::app::set "$restVarName" "$name"
g::app::set "$restVarDescription" "$description"
else
g::error::throw "Cannot register rest argument '$name': command already has rest argument"
fi
}
function g::app::command::hasRest() {
local restVarName="$(g::app::command::var REST_ARG_NAME)"
return $(g::app::has "$restVarName")
}
function g::app::command::arg() { function g::app::command::arg() {
local name="$1" local name="$1"
local description="${2:-$name}" local description="${2:-$name}"
@ -1544,6 +1601,10 @@ function g::app::command::arg() {
defaultValue="$3" defaultValue="$3"
fi fi
if g::app::command::hasRest; then
g::error::throw "Cannot register positional argument '$name' after rest argument."
fi
local currentArgVar="$(g::app::command::var CURRENT_ARG_NUM)" local currentArgVar="$(g::app::command::var CURRENT_ARG_NUM)"
if ! g::app::has "$currentArgVar"; then if ! g::app::has "$currentArgVar"; then
g::app::set "$currentArgVar" 0 g::app::set "$currentArgVar" 0
@ -1685,6 +1746,24 @@ function g::app::command::parse::has::flag() {
return $(g::util::false) return $(g::util::false)
} }
function g::app::command::parse::register::rest() {
local uuid="$1"
shift
local value="$@"
local varName="$(g::app::command::parse::var REST_ARGS "$uuid")"
g::internal "Registering rest args: $varName" g::app::command::parse
g::app::set "$varName" "$value"
}
function g::app::command::parse::get::rest() {
local uuid="$1"
local varName="$(g::app::command::parse::var REST_ARGS "$uuid")"
g::internal "Getting rest args" g::app::command::parse
g::app::get "$varName"
}
## ##
# Register the value of a positional argument in an argument set. # Register the value of a positional argument in an argument set.
# #
@ -1758,12 +1837,19 @@ function g::app::command::parse() {
g::app::set "$cmdNameVar" "$cmdName" g::app::set "$cmdNameVar" "$cmdName"
local nextPositionalIndex=1 local nextPositionalIndex=1
local trapRestArgs=no
local restArgs=()
local trappedFlag=no local trappedFlag=no
local trappedFlagName local trappedFlagName
local trappedFlagIndex local trappedFlagIndex
for arg in "${args[@]}"; do for arg in "${args[@]}"; do
g::internal "Parsing argument: ${arg}" g::app::command::parse g::internal "Parsing argument: ${arg}" g::app::command::parse
if [[ $trapRestArgs == yes ]]; then
restArgs+=("$arg")
continue
fi
local isFlag=no local isFlag=no
local flagOffset=0 local flagOffset=0
# If the string starts with -- or -, then it is a flag parameter # If the string starts with -- or -, then it is a flag parameter
@ -1852,7 +1938,14 @@ function g::app::command::parse() {
local currentArg="$(g::app::get $currentArgVar)" local currentArg="$(g::app::get $currentArgVar)"
if (( $nextPositionalIndex > $currentArg )); then if (( $nextPositionalIndex > $currentArg )); then
g::error::throw "Unexpected positional argument: $arg" local restVarName="$(g::app::command::var REST_ARG_NAME "${cmdName}")"
if g::app::has "$restVarName"; then
trapRestArgs=yes
restArgs+=("$arg")
continue
else
g::error::throw "Unexpected positional argument: $arg"
fi
fi fi
g::app::command::parse::register::arg $nextPositionalIndex "$arg" "$groupUuid" g::app::command::parse::register::arg $nextPositionalIndex "$arg" "$groupUuid"
@ -1866,6 +1959,10 @@ function g::app::command::parse() {
g::error::throw "Missing required value for flag: ${trappedFlagName}" g::error::throw "Missing required value for flag: ${trappedFlagName}"
fi fi
if [[ $trapRestArgs == yes ]]; then
g::app::command::parse::register::rest "$groupUuid" "${restArgs[@]}"
fi
g::app::command::validate "$cmdName" "$groupUuid" g::app::command::validate "$cmdName" "$groupUuid"
g__APP_LAST_ARGPARSE="$groupUuid" g__APP_LAST_ARGPARSE="$groupUuid"
} }
@ -1993,13 +2090,13 @@ function g::app::invoke() {
function g::args::buildAlias() { function g::args::buildAlias() {
local uuid="$1" local uuid="$1"
alias g::args::alias::${uuid}="g::args \"$uuid\"" alias ${uuid}="g::args \"$uuid\""
alias g::args::alias::${uuid}::arg="g::arg \"$uuid\"" alias ${uuid}::arg="g::arg \"$uuid\""
alias g::args::alias::${uuid}::arg::has="g::arg:has \"$uuid\"" alias ${uuid}::arg::has="g::arg:has \"$uuid\""
alias g::args::alias::${uuid}::flag="g::flag \"$uuid\"" alias ${uuid}::flag="g::flag \"$uuid\""
alias g::args::alias::${uuid}::flag::has="g::flag:has \"$uuid\"" alias ${uuid}::flag::has="g::flag:has \"$uuid\""
g__APP_LAST_ALIAS="g::args::alias::${uuid}" g__APP_LAST_ALIAS="${uuid}"
} }
## ##
@ -2068,6 +2165,20 @@ function g::arg::has() {
return $(g::app::command::parse::has::arg "$nameOrPosition" "$uuid") return $(g::app::command::parse::has::arg "$nameOrPosition" "$uuid")
} }
function g::arg::rest() {
local uuid="$1"
local cmdNameVar="$(g::app::command::parse::var "COMMAND_NAME" "$uuid")"
local cmdName="$(g::app::get "$cmdNameVar")"
local restVarName="$(g::app::command::var REST_ARG_NAME "$cmdName")"
if ! g::app::has "$restVarName"; then
g::error::throw "Unable to get rest arguments for command: $cmdName: command has no rest argument configured"
fi
g::app::command::parse::get::rest "$uuid"
}
## ##
# Get the value of a given flag in an argument set. # Get the value of a given flag in an argument set.
# #
@ -2126,20 +2237,20 @@ function g::flag::has() {
# @return true if acquired # @return true if acquired
# #
function g::lock::try() { function g::lock::try() {
local lockName="$1" local lockName="$1"
if g::lock::holds "$lockName"; then if g::lock::holds "$lockName"; then
return $(g::util::true) return $(g::util::true)
fi fi
local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")" local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")"
local tempFile="$(mktemp --suffix .lock)" local tempFile="$(mktemp --suffix .lock)"
if g::silence ln "$tempFile" "$lockFile"; then if g::silence ln "$tempFile" "$lockFile"; then
g__LOCKING_HOLDS+=("$lockName") g__LOCKING_HOLDS+=("$lockName")
return $(g::util::true) return $(g::util::true)
fi fi
return $(g::util::false) return $(g::util::false)
} }
## ##
@ -2148,10 +2259,10 @@ function g::lock::try() {
# @param lockName # @param lockName
# #
function g::lock::acquire() { function g::lock::acquire() {
local lockName="$1" local lockName="$1"
while ! g::lock::try "$lockName"; do while ! g::lock::try "$lockName"; do
sleep "$g__LOCKING_INTERVAL" sleep "$g__LOCKING_INTERVAL"
done done
} }
## ##
@ -2161,8 +2272,8 @@ function g::lock::acquire() {
# @return true if held # @return true if held
# #
function g::lock::holds() { function g::lock::holds() {
local lockName="$1" local lockName="$1"
return $(g::arr::includes "$lockName" "${g__LOCKING_HOLDS[@]}") return $(g::arr::includes "$lockName" "${g__LOCKING_HOLDS[@]}")
} }
## ##
@ -2171,26 +2282,26 @@ function g::lock::holds() {
# @param lockName # @param lockName
# #
function g::lock::release() { function g::lock::release() {
local lockName="$1" local lockName="$1"
if ! g::lock::holds "$lockName"; then if ! g::lock::holds "$lockName"; then
g::error::throw "Cannot release lock '$lockName': not held" g::error::throw "Cannot release lock '$lockName': not held"
fi fi
local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")" local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")"
rm -f "$lockFile" rm -f "$lockFile"
g__LOCKING_HOLDS=("${g__LOCKING_HOLDS[@]/$lockName}") g__LOCKING_HOLDS=("${g__LOCKING_HOLDS[@]/$lockName}")
} }
## ##
# Release all held locks before exit. # Release all held locks before exit.
# #
function g::lock::cleanupForExit() { function g::lock::cleanupForExit() {
for lockFile in "${g__LOCKING_HOLDS[@]}"; do for lockFile in "${g__LOCKING_HOLDS[@]}"; do
g::internal "Releasing lockfile: $lockFile" g::internal "Releasing lockfile: $lockFile"
rm -f "$lockFile" rm -f "$lockFile"
done done
} }
## ##
@ -2198,16 +2309,16 @@ function g::lock::cleanupForExit() {
# Throw an error if we are unable to acquire it. # Throw an error if we are unable to acquire it.
# #
function g::lock::singleton() { function g::lock::singleton() {
if ! g::lock::try "$(g::lock::uniqueCallerName)"; then if ! g::lock::try "$(g::lock::uniqueCallerName)"; then
g::error::throw "Another instance of this script is already running." g::error::throw "Another instance of this script is already running."
fi fi
} }
## ##
# Acquire the lock for this script's unique caller name. # Acquire the lock for this script's unique caller name.
# #
function g::lock::singleton::acquire() { function g::lock::singleton::acquire() {
g::lock::acquire "$(g::lock::uniqueCallerName)" g::lock::acquire "$(g::lock::uniqueCallerName)"
} }
## ##
@ -2216,7 +2327,7 @@ function g::lock::singleton::acquire() {
# @echo the unique name # @echo the unique name
# #
function g::lock::uniqueCallerName() { function g::lock::uniqueCallerName() {
g::str::replace "$g__CALLER_PATH" '/' '__' g::str::replace "$g__CALLER_PATH" '/' '__'
} }
# dependency declarations # dependency declarations

Loading…
Cancel
Save