@ -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