From 9fe327a78a93270f0dc04f553f130764aae0fc44 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Thu, 12 Aug 2021 00:09:21 -0500 Subject: [PATCH] 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 --- src/g.bash | 305 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 208 insertions(+), 97 deletions(-) diff --git a/src/g.bash b/src/g.bash index daa8975..3ebc073 100644 --- a/src/g.bash +++ b/src/g.bash @@ -56,21 +56,29 @@ g__BOOTSTRAPPED=no # Array of file paths that should be deleted on exit g__FILES_TO_CLEANUP=() +# Names of registered app namespaces g__APP_NAMES=() +# The currently active app namespace g__APP_CURRENT_NAME="app" +# UUID of the last argument set parsed g__APP_LAST_ARGPARSE='' +# Prefix of the last argument set alias generated g__APP_LAST_ALIAS='' +# Path to the directory where lock files can be created g__LOCKING_FILE_DIR="/tmp" +# Array of lock files held by this process g__LOCKING_HOLDS=() +# Amount of time (in seconds) to sleep between failed lock attempts 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 } +## +# Override the top-level caller reference. +# +function g::internals::setCaller() { + local caller="$1" + g__CALLER_PATH="$(g::util::realpath $caller)" +} ## ====================== UTILITIES ====================== ## function g::eval() { - if [[ "$g__DEBUG_EVAL" == yes ]]; then - g::log::stderr "eval: $*" - fi + if [[ "$g__DEBUG_EVAL" == yes ]]; then + g::log::stderr "eval: $*" + fi - eval "$@" + eval "$@" } function g::silence() { - "$@" > /dev/null 2>&1 + "$@" > /dev/null 2>&1 } # @@ -128,7 +143,7 @@ function g::silence() { # @return 0 if numeric, 1 otherwise # function g::util::isNumeric() { - local value="$1" + local value="${1:-}" local rex='^[+-]?[0-9]+([.][0-9]+)?$' @@ -163,17 +178,17 @@ function g::arr::includes() { } function g::arr::assoc::hasKey() { - local key="$1" - shift - local array="$@" + local key="$1" + shift + local array="$@" - for possibleKey in "${!array[@]}"; do - if [[ "$key" == "$possibleKey" ]]; then - return $(g::util::true) - fi - done + for possibleKey in "${!array[@]}"; do + if [[ "$key" == "$possibleKey" ]]; then + return $(g::util::true) + fi + 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() { local name="$(mktemp)" g__FILES_TO_CLEANUP+=("$name") @@ -435,7 +455,10 @@ function g::path::tmpdir() { # function g::error::throw() { local message="$1" - echo "$message" 1>&2 + echo "" + echo "" + g::util::trace "ERROR: $message" 2 1>&2 + echo "" exit 1 } @@ -457,10 +480,11 @@ function g::now() { function g::util::trace() { local stack="" local i message="${1:-""}" + local skip="${2:-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 + for (( i=$skip; i<$stack_size; i++ )); do local func="${FUNCNAME[$i]}" [ x$func = x ] && func=MAIN @@ -470,7 +494,7 @@ function g::util::trace() { [ x"$src" = x ] && src=non_file_source - stack+=$'\n'" at: "$func" "$src" "$linen + stack+=$'\n'" at ($func) in $src (line $linen)" done stack="${message}${stack}" @@ -645,19 +669,19 @@ function g::str::indexOf() { } function g::str::replace() { - local string="$1" - local find="$2" - local replaceWith="${3:-}" + local string="$1" + local find="$2" + local replaceWith="${3:-}" - printf "${string//${find}/${replaceWith}}" + printf "${string//${find}/${replaceWith}}" } function g::str::replace::once() { - local string="$1" - local find="$2" - local replaceWith="${3:-}" + local string="$1" + local find="$2" + local replaceWith="${3:-}" - printf "${string/${find}/${replaceWith}}" + printf "${string/${find}/${replaceWith}}" } @@ -1256,34 +1280,42 @@ MATH function g::source::resolve() { local path="$1" + local up="${2:-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 + ( + local reldir="$(dirname ${BASH_SOURCE[$up]})" + g::internal "Changing to dir to resolve source: $reldir" + g::path::cd "$reldir" - 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() { 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")" if [[ "$length" -gt 0 ]]; then @@ -1295,9 +1327,10 @@ function g::source::exists() { function g::source::force() { local slug="$1" + local up="${2:-1}" - local resolved="$(g::source::resolve "$slug")" - if ! g::source::exists "$slug"; then + local resolved="$(g::source::resolve "$slug" $(( up + 1 )))" + if ! g::source::exists "$slug" $(( up + 1 )); then g::error::throw "Cannot resolve source to include: $slug" fi @@ -1319,8 +1352,10 @@ function g::source::force() { function g::source::has() { 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) fi @@ -1329,10 +1364,11 @@ function g::source::has() { function g::source() { local slug="$1" + local up="${2:-1}" - if ! g::source::has "$slug"; then - g::source::force "$slug" - g__IMPORTED_FILES+=("$slug") + if ! g::source::has "$slug" $(( up + 1 )); then + g::source::force "$slug" $(( up + 1 )) + g__IMPORTED_FILES+=("$resolved") fi } @@ -1448,9 +1484,10 @@ function g::app() { local description="${2:-}" g__APP_CURRENT_NAME="$name" - g__APP_NAMES+=("$name") - - g::app::set DESCRIPTION "$description" + if ! g::arr::includes "$name" "${g__APP_NAMES[@]}"; then + g__APP_NAMES+=("$name") + g::app::set DESCRIPTION "$description" + fi } function g::app::var() { @@ -1533,6 +1570,26 @@ function g::app::command() { 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() { local name="$1" local description="${2:-$name}" @@ -1544,6 +1601,10 @@ function g::app::command::arg() { defaultValue="$3" 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)" if ! g::app::has "$currentArgVar"; then g::app::set "$currentArgVar" 0 @@ -1685,6 +1746,24 @@ function g::app::command::parse::has::flag() { 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. # @@ -1758,12 +1837,19 @@ function g::app::command::parse() { g::app::set "$cmdNameVar" "$cmdName" local nextPositionalIndex=1 + local trapRestArgs=no + local restArgs=() local trappedFlag=no local trappedFlagName local trappedFlagIndex for arg in "${args[@]}"; do g::internal "Parsing argument: ${arg}" g::app::command::parse + if [[ $trapRestArgs == yes ]]; then + restArgs+=("$arg") + continue + fi + local isFlag=no local flagOffset=0 # 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)" 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 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}" fi + if [[ $trapRestArgs == yes ]]; then + g::app::command::parse::register::rest "$groupUuid" "${restArgs[@]}" + fi + g::app::command::validate "$cmdName" "$groupUuid" g__APP_LAST_ARGPARSE="$groupUuid" } @@ -1993,13 +2090,13 @@ function g::app::invoke() { 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\"" + alias ${uuid}="g::args \"$uuid\"" + alias ${uuid}::arg="g::arg \"$uuid\"" + alias ${uuid}::arg::has="g::arg:has \"$uuid\"" + alias ${uuid}::flag="g::flag \"$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") } +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. # @@ -2126,20 +2237,20 @@ function g::flag::has() { # @return true if acquired # function g::lock::try() { - local lockName="$1" + local lockName="$1" - if g::lock::holds "$lockName"; then - return $(g::util::true) - fi + 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 + 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) + return $(g::util::false) } ## @@ -2148,10 +2259,10 @@ function g::lock::try() { # @param lockName # function g::lock::acquire() { - local lockName="$1" - while ! g::lock::try "$lockName"; do - sleep "$g__LOCKING_INTERVAL" - done + local lockName="$1" + while ! g::lock::try "$lockName"; do + sleep "$g__LOCKING_INTERVAL" + done } ## @@ -2161,8 +2272,8 @@ function g::lock::acquire() { # @return true if held # function g::lock::holds() { - local lockName="$1" - return $(g::arr::includes "$lockName" "${g__LOCKING_HOLDS[@]}") + local lockName="$1" + return $(g::arr::includes "$lockName" "${g__LOCKING_HOLDS[@]}") } ## @@ -2171,26 +2282,26 @@ function g::lock::holds() { # @param lockName # function g::lock::release() { - local lockName="$1" + local lockName="$1" - if ! g::lock::holds "$lockName"; then - g::error::throw "Cannot release lock '$lockName': not held" - fi + 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" + local lockFile="$(g::path::resolve "$g__LOCKING_FILE_DIR" "${lockName}.lock")" + rm -f "$lockFile" - g__LOCKING_HOLDS=("${g__LOCKING_HOLDS[@]/$lockName}") + 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 + for lockFile in "${g__LOCKING_HOLDS[@]}"; do + g::internal "Releasing lockfile: $lockFile" + rm -f "$lockFile" + done } ## @@ -2198,16 +2309,16 @@ function g::lock::cleanupForExit() { # 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 + 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)" + g::lock::acquire "$(g::lock::uniqueCallerName)" } ## @@ -2216,7 +2327,7 @@ function g::lock::singleton::acquire() { # @echo the unique name # function g::lock::uniqueCallerName() { - g::str::replace "$g__CALLER_PATH" '/' '__' + g::str::replace "$g__CALLER_PATH" '/' '__' } # dependency declarations