From c5141fd19e96066f154937f2d401d76560660d36 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 30 Jul 2024 01:43:01 -0400 Subject: [PATCH] Start better documentation + fix bug in g::source --- README.md | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/g.bash | 1 + 2 files changed, 241 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2470b6a..aebe404 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,242 @@ # g.bash -A Bash framework. \ No newline at end of file +A Bash framework. + +## Documentation (WIP) + +### Global Helpers + +- `g::eval [...command]` - Execute a command, but w/ in-framework logging +- `g::silence [...command]` - Execute a command, squashing all output +- `g::now` - Get a timestamp in ISO8061 +- `g::bc [...args]` - Execute a `bc` math statement +- `g::awk [...inputs]` - Execute an `awk` print command + + +### Utilties (`g::util`) + +- `isNumeric [value]` - Checks if a given value is a number with optional decimal and positive/negative +- `true` - A successful exit code/return value (`0`) +- `false` - A failure exit code/return value (`1`) +- `returnToEcho [...command]` - Execute a command that returns `true`/`false` and instead echo `1` on success, `0` otherwise +- `uuid` - Generate a UUID +- `uuid::underscore` - Generate a UUID (underscore-separated) +- `escape [value]` - Bash-escape a value for safe use in `eval` strings +- `realpath [path]` - A non-GNU real path resolver +- `trace [?message] [?skip = frames to exclude]` - Generate a stack trace with an optional message + + +### Strings (`g::str`) + +- `quote [str]` - Surround a string in quotes +- `eval [...args]` - Execute a command and quote the output +- `length [str]` - Get the number of chars in a string +- `padLeft [str] [length] [pad character=' ']` - Left-pad a string to the given length +- `padRight [str] [length] [pad character=' ']` - Right-pad a string to the given length +- `padCenter [str] [length] [pad character=' ']` - Center-pad a string to the given length +- `substring [str] [startAt] [length]` - Get a substring +- `offset [str] [startAt]` - Drop the first `n` characters of a string +- `reverse [str]` - Reverse a string +- `startsWith [haystack] [needle]` - Check if the haystack starts with the needle +- `endsWith [haystack] [needle]` - Check if the haystack ends with the needle +- `trim [str]` - Trim the whitespace from either end of the string +- `indexOf [str] [substring]` - Print the character index where the `substring` appears, if any (returns `g::util::false` if no match) +- `replace [str] [find] [replace]` - Find-and-replace all occurrences in a string +- `replace::once [str] [find] [replace]` - Find-and-replace the first occurrence in a string + + +### Arrays (`g::arr`) + +> Note: When passing an array as an argument to a function, it should be passed as `"${array[@]}"` + +- `includes [value] [array]` - Checks if a given value exists in the array +- `assoc::hasKey [key] [array]` - Checks if an associative array has a given key +- `join [delimiter] [array]` - Implodes a list of values to a string using the given delimiter + + +### Math (`g::math`) + +- Constants (`g::math::c`) + - `e` - Euler's constant + - `ln2` - Natural log of 2 + - `ln10` - Natural log of 10 + - `pi` - Pi + - `sqrt05` - Square root of 1/2 + - `sqrt2` - Square root of 2 +- `abs [num]` - Absolute value of a number +- `cubeRoot [num]` - Cube root of a number +- `squareRoot [num]` - Square root of a number +- `lessThan [left] [right]` - Check if `left < right` +- `greaterThan [left] [right]` - Check if `left > right` +- `equalTo [left] [right]` - Check if `left == right`, numerically +- `isPositive [num]` - Check if `num > 0` +- `isNegative [num]` - Check if `num < 0` +- `signOf [num]` - Prints the leading sign of a number (either `+` or `-`) +- `mod [num] [modulus]` - Compute the modulus of a number +- `ceiling [num]` - Round the number up to the nearest int +- `truncate [num]` - Truncate the number to an int +- `floor [num]` - Round the number down to the nearest int +- `exponent [num] [power]` - Compute `num^power` +- `cos [num]` - Cosine of a number +- `sine [num]` - Sine of a number +- `tan [num]` - Tangent of a number +- `ln [num]` - Natural log of a number +- `log10 [num]` - Log base 10 of a number +- `log2 [num]` - Log base 2 of a number +- `log [num] [base]` - Arbitrary base logarithm +- `random` - Get a random float +- `max [...nums]` - Get the max value of some numbers +- `min [...nums]` - Get the min value of some numbers +- `round [num] [precision]` - Round a number to the specified number of decimal points + +### Files (`g::file`) + +- `appendString [path] [string]` - Append the string contents to the end of a file +- `exists [path]` - Checks if a file exists +- `directoryExists [path]` - Checks if a directory exists +- `touch [path]` - Create a file if it does not exist +- `truncate [path]` - If a file exists, empty its contents + +### Source Code (`g::source`) + +`g::source` allows you to organize your scripts into multiple files, and perform filesystem-relative source imports. Similarly, it will prevent the same source file from being re-imported multiple times. + +Example: + +```shell +# index.bash +g::source src/setup +g::source src/logging + +# src/setup.bash +g::source logging + +# src/logging.bash +# This file is included once +``` + +- `g::source [path] [?up=1]` - Load a source file relative to the current script (must end in `.bash` and will only be loaded once) +- `resolve [path] [?up=1]` - Resolve the path of a source file relative to the current script +- `exists [path] [?up=1]` - Check if a source file exists relative to the current script +- `force [path] [?up=1]` - Resolve and load a source file, bypassing the cache +- `has [path] [?up=1]` - Check if the given source file has been loaded + +### Configuration File (`g::config`) + +Provides a simple key-value way of storing persistent configuration. + +Example: + +```shell +echo "Last run: $(g::config::get last-run Never)" +g::config::set 'last-run' "$(g::now)" +``` + +- `setDefault [name]` - Set the default name of the config file +- `getDefault` - Get the default name of the config file +- `init [?name]` - Make sure a config file exists +- `get [name] [?default value]` - Get a value from the default config file +- `get::forFile [file] [name] [?default value]` - Get a value from a non-default config file +- `set [name] [value]` - Store a value in the default config file +- `set::forFile [file] [name] [value]` - Store a value in a non-default config file + +### Paths (`g::path`) + +- `concat [path] [...parts]` - Combine path segments into a path, accounting for leading and trailing slashes +- `resolve [...parts]` - Concat path parts and determine the real path +- `resolveFrom [base dir] [...parts]` - Concat path parts and determine the real path relative to some base directory +- `cd [...parts]` - Resolve path parts and change directories +- `tmp` - Get a temp file path (and mark it for cleanup) +- `tmpdir` - Get a temp dir path (and mark it for cleanup) + +### Errors (`g::error`) + +- `throw [message]` - Throw an error +- (global) `try` +- (global) `catch` + +### Logging (`g::log`) + +`g::log` provides level- and target-aware logging output. The `internal` level is used by `g.bash` itself. + +Available levels: `error` | `warn` | `info` | `debug` | `verbose` | `internal` + +- `enableTarget [name]` - Enable log outputs for the given target +- `enableAllTargets` - Enable log outputs for ALL targets +- `all` - Shorthand to enable the highest verbosity for ALL targets +- `getLevel` - Get the current logging level +- `setLevel [name]` - Set a new logging level +- `enable [target=stdout|stderr|file] [?param]` - Enable logging to the specified target +- `disable [target=stdout|stderr|file]` - Disable logging to the specified target +- `g::error [output] [?target]` - Write a log message at the `error` level, optionally for a specific target + - Same format for `g::warn`, `g::info`, `g::debug`, `g::verbose`, and `g::internal` +- `g::log::rotate` - Archive the log file to a timestamped version and start a new one + +### Locks (`g::lock`) + +- `try [name]` - Try to acquire a lock, returning `g::util::true` if successful +- `acquire [name]` - Acquire a lock, sleeping until it is available +- `holds [name]` - Check if we currently hold the given lock +- `release [name]` - Release the given lock if held +- `singleton` - Throw an error if this script is being executed concurrently +- `singleton::acquire` - Wait for other instances of this script to finish + +### App Framework (`g::app`) + +`g::app` is a way of defining multi-directive CLI applications with argument/option parsing. + +Each application is broken up into "commands," and each command can have multiple arguments/flags. + +Your source code can provide multiple applications (switching between them using `g::app`) with app-scoped storage. + +- `g::app [?name=app] [?description]` - Create a new top-level application +- `get [name] [?default value]` - Get the value of an app-scoped variable +- `set [name] [value]` - Set the value of an app-scoped variable +- `has [name]` - Check if the app-scope has a variable with the given name +- `usage` - Print usage information for the current application +- `invoke [command] [...args]` - Invoke a command in the current application + +#### Defining Commands (`g::app::command`) + +A command is an invokable sub-command function for an application, which can have multiple flags and positional arguments associated with it. + +When a command is executed, its arguments are parsed and a function `app::::` is called. Basic example: + +```shell +g::app myapp "An example application" + +g::app::command greet "Greeting command" +g::app::command::arg name "Name of the person to greet" "World" +g::app::command::flag greeting= "Override the greeting" + +function app::myapp::greet() { + local args="$1" + name="$(g::arg "$args" name)" + greeting="$(g::flag "$args" greeting)" + if [ -z "$greeting" ]; then + greeting="Hello," + fi + + echo "$greeting $name" +} +``` + +- `g::app::command [name] [?description]` - Start defining a new command in the current app +- `exists [name]` - Check if a given command exists in the current app +- `description [name]` - Get the description for a command in the current app +- `arg [name] [?description] [?default value]` - Add a positional argument to the current command +- `flag [name] [?description]` - Add a position-less flag option. If `name` trails with a `=`, then a value is expected +- `rest [name] [?description]` - Register a rest-argument (`[...args]`) for the current command + +#### Accessing Arguments + +When arguments for a command are parsed, they are stored in an "argument set." The argument set is a series of data structures containing the parsed/validated flags & positional arguments. + +The argument set ID is passed to the command handler as the only parameter (`$1`) and can be used to look up the values of flags and arguments. + +- `g::args [uuid]` - Get the raw arguments from a parsed set (akin to `$@`) +- `g::arg [uuid] [name]` - Get the value of a positional argument +- `g::arg::has [uuid] [name]` - Checks if a positional argument was provided +- `g::arg::rest [uuid]` - Get all the unmatched CLI arguments/flags +- `g::flag [uuid] [name]` - Get the value of a position-less flag +- `g::flag::has [uuid] [name]` - Checks if a position-less flag was specified diff --git a/src/g.bash b/src/g.bash index 3ebc073..78671c8 100644 --- a/src/g.bash +++ b/src/g.bash @@ -1366,6 +1366,7 @@ function g::source() { local slug="$1" local up="${2:-1}" + local resolved="$(g::source::resolve "$slug" $(( up + 1 )))" if ! g::source::has "$slug" $(( up + 1 )); then g::source::force "$slug" $(( up + 1 )) g__IMPORTED_FILES+=("$resolved")