diff --git a/example.bash b/example.bash index c12b364..cc53214 100755 --- a/example.bash +++ b/example.bash @@ -2,16 +2,44 @@ source src/g.bash -g::log::enable file g.log g::log::setLevel internal +g::log::enableTarget g::arg -g::info "Starting try-catch..." +g::info "Defining application..." -try { - ls -lah baddir -} catch { - declare local error="$?" - g::error "Error code: $error" +g::app ex "A simple example application" + +g::app::command shell "Start an interactive shell" +g::app::command::arg name "The name of the shell" +g::app::command::flag no-tty "Disable TTY output" + +function app::ex::shell() { + echo "Starting shell: '$@'" +} + +g::app::command ls "List contents of the directory" +g::app::command::flag dir= "The directory to list" +g::app::command::flag fubar= "A fubar" +g::app::command::flag dry-run "Run dry" +g::app::command::arg name "Name of the ls" + +function app::ex::ls() { + local args="$1" + echo "Argcall: $args::arg" + echo "Arg1: $($args::arg name)" + + echo "Arg1: $(g::arg $g__APP_LAST_ARGPARSE 0)" + echo "Arg2: $(g::arg $g__APP_LAST_ARGPARSE name)" + + echo "Dir: $(g::flag $g__APP_LAST_ARGPARSE dir)" + echo "Fubar: $(g::flag $g__APP_LAST_ARGPARSE fubar)" + echo "dry-run: $(g::flag $g__APP_LAST_ARGPARSE dry-run)" + + if g::arg::has $g__APP_LAST_ARGPARSE 0; then + echo "Has arg0" + fi + + echo "All args: $(g::args $g__APP_LAST_ARGPARSE)" } -g::info "After try-catch..." +g::app::invoke "$@" diff --git a/src/g.bash b/src/g.bash index 0771c65..c0ce26a 100644 --- a/src/g.bash +++ b/src/g.bash @@ -3,19 +3,25 @@ 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]}" + # 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=true +g__LOGGING_TO_STDOUT=false # True if we should log messages to stderr -g__LOGGING_TO_STDERR=false +g__LOGGING_TO_STDERR=true # True if we should log messages to a file g__LOGGING_TO_FILE=false @@ -23,6 +29,70 @@ 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='' + + +## +# 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 +} @@ -69,6 +139,21 @@ function g::arr::includes() { return 1 } +## +# 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 @@ -101,6 +186,74 @@ function g::util::returnToEcho() { 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. # @@ -146,6 +299,99 @@ function g::file::truncate() { 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 @@ -194,6 +440,21 @@ function g::util::trace() { 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 }" +} + @@ -276,6 +537,14 @@ function g::str::substring() { 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" @@ -317,8 +586,27 @@ function g::str::endsWith() { fi } -# string helpers - # string split +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 + + printf 'none' + return $(g::util::false) +} @@ -333,6 +621,20 @@ function g::str::endsWith() { # 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. # @@ -534,6 +836,14 @@ function g::log::format() { 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)" @@ -549,7 +859,9 @@ function g::log() { # function g::error() { local output="$1" - g::log error "$output" + local target="${2:-}" + + g::log error "$output" "$target" } # @@ -559,7 +871,9 @@ function g::error() { # function g::warn() { local output="$1" - g::log warn "$output" + local target="${2:-}" + + g::log warn "$output" "$target" } # @@ -569,7 +883,9 @@ function g::warn() { # function g::info() { local output="$1" - g::log info "$output" + local target="${2:-}" + + g::log info "$output" "$target" } # @@ -579,7 +895,9 @@ function g::info() { # function g::debug() { local output="$1" - g::log debug "$output" + local target="${2:-}" + + g::log debug "$output" "$target" } # @@ -587,9 +905,11 @@ function g::debug() { # # @param output # -function g::internal() { +function g::verbose() { local output="$1" - g::log verbose "$output" + local target="${2:-}" + + g::log verbose "$output" "$target" } # @@ -599,7 +919,9 @@ function g::internal() { # function g::internal() { local output="$1" - g::log internal "$output" + local target="${2:-}" + + g::log internal "$output" "$target" } function g::log::rotate() { @@ -634,17 +956,1049 @@ function g::err::parse() { } -# math helpers -# dependency declarations -# package/script imports & singleton requires +## ====================== MATH CONSTANTS ====================== ## +function g::math::c::e() { + echo 0.577215665 +} -# output helpers +function g::math::c::ln2() { + echo 0.69314718056 +} -# argument parsing +function g::math::c::ln10() { + echo 2.30258509299 +} + +function g::math::c::pi() { + echo 3.14159265359 +} + +function g::math::c::sqrt05() { + echo 0.70710678119 +} + +function g::math::c::sqrt2() { + echo 1.41421356237 +} + + + + +## ====================== MATH HELPERS ====================== ## + +function g::math::abs() { + local input="$1" + echo "${input#-}" +} + +function g::math::cubeRoot() { + local input="$1" + echo "$input" | awk '{ print $1^(1/3) }' +} + +function g::math::squareRoot() { + local input="$1" + echo "$input" | awk '{ print $1^(1/2) }' +} + +function g::math::lessThan() { + local left="$1" + local right="$2" + + if (( $(echo "$left < $right" | bc -l) )); then + return $(g::util::true) + else + return $(g::util::false) + fi +} + +function g::math::greaterThan() { + local left="$1" + local right="$2" + + if (( $(echo "$left > $right" | bc -l) )); then + return $(g::util::true) + else + return $(g::util::false) + fi +} + +function g::math::equalTo() { + local left="$1" + local right="$2" + + if (( $(echo "$left == $right" | bc -l) )); then + return $(g::util::true) + else + return $(g::util::false) + fi +} + +function g::math::isPositive() { + local input="$1" + + if g::math::greaterThan "$input" 0; then + return $(g::util::true) + else + return $(g::util::false) + fi +} + +function g::math::isNegative() { + local input="$1" + + if g::math::lessThan "$input" 0; then + return $(g::util::true) + else + return $(g::util::false) + fi +} + +function g::math::signOf() { + local input="$1" + if g::math::isNegative "$input"; then + echo - + else + echo + + fi +} + +function g::math::mod() { + local input="$1" + local modulus="$2" + echo "oldscale=scale; scale=0; $input%$modulus; scale=oldscale; l(2)" | bc -l | head -n 1 +} + +function g::math::ceiling() { + local input="$1" + + local decimal="$(g::math::mod "$input" 1)" + if g::math::equalTo "$decimal" 0; then + echo "$input" + return + fi + + local plusOne="$(g::bc $input + 1)" + echo "$(g::math::truncate "$plusOne")" +} + +function g::math::truncate() { + local input="$1" + echo "${input%.*}" +} + +function g::math::floor() { + local input="$1" + echo "$(g::math::truncate "$input")" +} + +function g::math::exponent() { + local base="$1" + local exponent="$2" + echo "$(g::awk $base^$exponent)" +} + +function g::math::cos() { + local input="$1" + echo "$(g::awk cos\($input\))" +} + +function g::math::sin() { + local input="$1" + echo "$(g::awk sin\($input\))" +} + +function g::math::tan() { + local input="$1" + echo "$(g::awk tan\($input\))" +} + +function g::math::ln() { + local input="$1" + echo "$(g::bc "l($input)")" +} + +function g::math::log10() { + local input="$1" + echo "$(g::bc "l($input)/l(10)")" +} + +function g::math::log2() { + local input="$1" + echo "$(g::bc "l($input)/l(2)")" +} + +function g::math::log() { + local input="$1" + local base="${2:-10}" + echo "$(g::bc "l($input)/l($base)")" +} + +function g::math::random() { + echo "$(g::bc "scale=16 ; ${RANDOM}/32767")" +} + +function g::math::max() { + local array=("$@") + + local max="${array[0]}" + for num in "${array[@]}"; do + if g::math::greaterThan "$num" "$max"; then + max="$num" + fi + done + + echo "$max" +} + +function g::math::min() { + local array=("$@") + + local min="${array[0]}" + for num in "${array[@]}"; do + if g::math::lessThan "$num" "$min"; then + min="$num" + fi + done + + echo "$min" +} + +## +# Round a number to the specified precision. +# +# @param input - the number to round +# @param [toScale=0] - the number of decimals to round +# @echo the result +# +function g::math::round() { + local input="$1" + local toScale="${2:-0}" + bc < 0) h=0.5 + +a=(m * t + h) + +scale=$toScale +a / t +MATH +} + + + + +function g::source::resolve() { + local path="$1" + + local fsPath="$(realpath "${path}.bash")" + if g::file::exists "$fsPath"; then + echo "$fsPath" + return $(g::util::true) + elif g::file::directoryExists "$fsPath"; then + echo "$fsPath" + return $(g::util::true) + else + for dir in "${g__SOURCE_DIRS[@]}"; do + local resolved="$(g::path::resolve "$dir" "${path}.bash")" + if g::file::exists "$resolved"; then + echo "$resolved" + return $(g::util::true) + elif g::file::directoryExists "$resolved"; then + echo "$resolved" + return $(g::util::true) + fi + done + fi + + return $(g::util::false) +} + +function g::source::exists() { + local path="$1" + + local resolved="$(g::source::resolve "$path")" + local length="$(g::str::length "$resolved")" + + if [[ "$length" -gt 0 ]]; then + return $(g::util::true) + fi + + return $(g::util::false) +} + +function g::source::force() { + local slug="$1" + + local resolved="$(g::source::resolve "$slug")" + if ! g::source::exists "$slug"; then + g::error::throw "Cannot resolve source to include: $slug" + fi + + if g::file::exists "$resolved"; then + source "$resolved" + return $(g::util::true) + fi + + if g::file::directoryExists "$resolved"; then + for file in "$(g::path::resolve "$resolved" './*.bash')"; do + source "$file" + done + + return $(g::util::true) + fi + + return $(g::util::false) +} + +function g::source::has() { + local slug="$1" + + if g::arr::includes "$slug" "${g__IMPORTED_FILES[@]}"; then + return $(g::util::true) + fi + + return $(g::util::false) +} + +function g::source() { + local slug="$1" + + if ! g::source::has "$slug"; then + g::source::force "$slug" + g__IMPORTED_FILES+=("$slug") + fi +} + + + + +## +# Set the default config file name. +function g::config::setDefault() { + local name="$1" + g__CONFIG_DEFAULT_NAME="${name}" +} + +## +# Get the default config file name. +function g::config::getDefault() { + echo "$g__CONFIG_DEFAULT_NAME" +} + +## +# Initialize a config file with the given name. +function g::config::init() { + local filename="${1:-$g__CONFIG_DEFAULT_NAME}" + + if ! g::file::directoryExists "$g__CONFIG_DIR"; then + mkdir -p "$g__CONFIG_DIR" + fi + + if ! g::file::exists "$(g::path::resolve "$g__CONFIG_DIR" "$filename")"; then + g::file::touch "$(g::path::resolve "$g__CONFIG_DIR" "$filename")" + fi +} + +## +# Get a value in the default config file. +function g::config::get() { + local name="$1" + local defaultValue="${2:-}" + + g::config::init + + ( + local varname="g__config__${name}" + source "$(g::path::resolve "$g__CONFIG_DIR" "$g__CONFIG_DEFAULT_NAME")" + echo "${!varname:-$defaultValue}" + ) +} + +## +# Get a value in a config, by file. +function g::config::get::forFile() { + local file="$1" + shift 1 + local args="$@" + + local oldDefault="$(g::config::getDefault)" + g::config::setDefault "$file" + + local value="$(g::config::get "$@")" + + g::config::setDefault "$oldDefault" + echo "$value" +} + +## +# Set a value in the default config file. +function g::config::set() { + local name="$1" + local value="$2" + + g::config::init + + local filepath="$(g::path::resolve "$g__CONFIG_DIR" "$g__CONFIG_DEFAULT_NAME")" + local contents="$(cat "$filepath")" + local newlines=() + + ( + cat "$filepath" | readarray -t lines + for line in "${lines[@]}"; do + if ! g::str::startsWith "$line" "g__config__$name"; then + newlines+=("$line") + fi + done + ) + + local escaped="$(g::util::escape "$value")" + newlines+=("g__config__$name=$escaped") + + local newcontents="$(g::arr::join '\n' "${newlines[@]}")" + echo "$newcontents" > "$filepath" +} + +## +# Set a value in a config, by file. +function g::config::set::forFile() { + local file="$1" + shift 1 + local args="$@" + + local oldDefault="$(g::config::getDefault)" + g::config::setDefault "$file" + + g::config::set "$@" + + g::config::setDefault "$oldDefault" +} + + + + +function g::app() { + local name="${1:-app}" + local description="${2:-}" + + g__APP_CURRENT_NAME="$name" + g__APP_NAMES+=("$name") + + g::app::set DESCRIPTION "$description" +} + +function g::app::var() { + local varname="$1" + printf "g__appnamespace__${g__APP_CURRENT_NAME}_${varname}" +} + +function g::app::get() { + local varname="$1" + local defaultValue="${2:-}" + + local resolved="$(g::app::var "$varname")" + g::internal "[g::app::get] $resolved" g::app::vars + echo "${!resolved:-$defaultValue}" +} + +function g::app::set() { + local varname="$1" + local value="$2" + + local resolved="$(g::app::var "$varname")" + local escaped="$(g::util::escape "$value")" + g::internal "[g::app::set] $resolved" g::app::vars + 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" $(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 + eval "export ${cmdsVar}=()" + fi + + 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) +} + +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" +} + +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" +} + +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) +} + +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" +} + +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 +} + +function g::app::usage() { + local header="$g__APP_CURRENT_NAME" + local description="$(g::app::get DESCRIPTION)" + if [[ "$(g::str::length "$description")" -gt 0 ]]; then + header="${header}: ${description}" + fi + + echo "$header" + echo "" + echo "Usage: $(basename "$g__CALLER_PATH") [...arguments]" + echo "" + + local commandsString="" + local cmdsVar="$(g::app::var AVAILABLE_COMMANDS)" + + for cmdName in $(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" +} + +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 $(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." +} + +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}" +} + +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" +} + +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" +} + +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") +} + +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" +} + +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") +} + +# logging/debug flags + +# dependency declarations + +# output helpers + +# argument parsing + +# exception handling + +# JSON parsing, INI parsing, rich data types (named parameters & arrays/maps) + +# sendmail support -# try-catch & exception handling +# locking -# named parameters & arrays/maps +# application definition -# JSON parsing, INI parsing, rich data types +# Bootstrap the framework +g::internals::bootstrap