Initial commit

This commit is contained in:
Garrett Mills 2021-08-04 18:11:17 -05:00
parent 773ab23f22
commit 7dfd4a8cd5
No known key found for this signature in database
GPG Key ID: 35F6925363BCE548
3 changed files with 669 additions and 0 deletions

2
.gitignore vendored
View File

@ -161,3 +161,5 @@ Network Trash Folder
Temporary Items
.apdisk
g.log
g.log*

17
example.bash Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
source src/g.bash
g::log::enable file g.log
g::log::setLevel internal
g::info "Starting try-catch..."
try {
ls -lah baddir
} catch {
declare local error="$?"
g::error "Error code: $error"
}
g::info "After try-catch..."

650
src/g.bash Normal file
View File

@ -0,0 +1,650 @@
#!/bin/bash -e
set -u
set +o histexpand
shopt -qs failglob
shopt -s expand_aliases
## ====================== GLOBALS ====================== ##
export g__EXISTS=true
# 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
# True if we should log messages to stderr
g__LOGGING_TO_STDERR=false
# 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'
## ====================== UTILITIES ====================== ##
#
# 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
}
#
# 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 eval "$cmd"; then
echo 1
else
echo 0
fi
}
#
# 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
}
#
# 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}"
}
## ====================== STRINGS ====================== ##
function g::str::quote() {
local string="$1"
echo "'$string'"
}
function g::str::quote::eval() {
local cmd="$@"
local str="$(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::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
}
# string helpers
# string split
## ====================== LOGGING ====================== ##
# output functions and logging
# capture output
# silence output
# colors
# powerline
# tables
# width-fill
#
# 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 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"
g::log error "$output"
}
#
# Log a warning message.
#
# @param output
#
function g::warn() {
local output="$1"
g::log warn "$output"
}
#
# Log an informational message.
#
# @param output
#
function g::info() {
local output="$1"
g::log info "$output"
}
#
# Log a debugging message.
#
# @param output
#
function g::debug() {
local output="$1"
g::log debug "$output"
}
#
# Log a verbose message.
#
# @param output
#
function g::internal() {
local output="$1"
g::log verbose "$output"
}
#
# Log a verbose message.
#
# @param output
#
function g::internal() {
local output="$1"
g::log internal "$output"
}
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 helpers
# dependency declarations
# package/script imports & singleton requires
# output helpers
# argument parsing
# try-catch & exception handling
# named parameters & arrays/maps
# JSON parsing, INI parsing, rich data types