diff --git a/CHANGELOG.md b/CHANGELOG.md index 9748df9..2a24093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and versions are tracked in the repo-root `VERSION` file. ### Added +- Added the `base-bash` launcher for standalone scripts that want the + base-bash-libs stdlib preloaded from a shebang. - Added `std_run --timeout`, `--max-attempts`, and `--retry-delay` execution policy options for timeout-only, retry-only, and timeout-plus-retry command execution. diff --git a/README.md b/README.md index f847385..4708086 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,22 @@ source "$base_bash_libs_prefix/libexec/lib/bash/std/lib_std.sh" printf 'base-bash-libs version: %s\n' "$BASE_BASH_LIBS_VERSION" ``` +Homebrew installs the standalone `base-bash` launcher on `PATH`. Use it when a +script should run with the stdlib preloaded from its shebang: + +```bash +#!/usr/bin/env base-bash + +import_base_bash_lib str/lib_str.sh + +main() { + local name=" Example " + str_trim name + log_info "Running with base-bash-libs $BASE_BASH_LIBS_VERSION" + std_run echo "$name" +} +``` + Load companion libraries with absolute imports from the same package: ```bash @@ -100,6 +116,12 @@ import "$base_bash_libs_dir/lib/bash/arg/lib_arg.sh" import "$base_bash_libs_dir/lib/bash/list/lib_list.sh" ``` +You can also run source-checkout scripts through the launcher: + +```bash +vendor/base-bash-libs/bin/base-bash ./scripts/tool.sh +``` + ### Vendored or Submodule Layout For projects that vendor dependencies or use git submodules, place this diff --git a/bin/base-bash b/bin/base-bash new file mode 100755 index 0000000..d978dbc --- /dev/null +++ b/bin/base-bash @@ -0,0 +1,223 @@ +#!/usr/bin/env bash + +# +# base-bash +# Standalone launcher for Bash scripts that use base-bash-libs. +# +# Responsibilities: +# - run a Bash script with lib/bash/std/lib_std.sh preloaded +# - preserve stdlib wrapper-flag behavior for the target script +# - provide package-relative imports without establishing Base runtime state +# + +base_bash_die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +base_bash_resolve_path() { + local source_path="${1:-}" + local link_dir + local target + + [[ -n "$source_path" ]] || return 1 + + if [[ "$source_path" != */* ]]; then + if [[ -e "$source_path" ]]; then + source_path="./$source_path" + else + source_path="$(command -v -- "$source_path" 2>/dev/null || true)" + [[ -n "$source_path" ]] || return 1 + fi + fi + + while [[ -L "$source_path" ]]; do + link_dir="$(cd -- "$(dirname -- "$source_path")" && pwd -P)" || return 1 + target="$(readlink "$source_path")" || return 1 + if [[ "$target" == /* ]]; then + source_path="$target" + else + source_path="$link_dir/$target" + fi + done + + link_dir="$(cd -- "$(dirname -- "$source_path")" && pwd -P)" || return 1 + printf '%s/%s\n' "$link_dir" "$(basename -- "$source_path")" +} + +base_bash_package_root() { + local source_path + local bin_dir + + source_path="$(base_bash_resolve_path "$0")" || return 1 + bin_dir="$(cd -- "$(dirname -- "$source_path")" && pwd -P)" || return 1 + cd -- "$bin_dir/.." && pwd -P +} + +base_bash_ensure_supported_bash() { + local candidate + local candidates=() + local current_version + local display_version + + current_version="${BASE_BASH_LIBS_TEST_BASH_VERSION:-${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}}" + ((current_version >= 42)) && return 0 + + if [[ -n "${BASE_BASH_LIBS_TEST_BASH_CANDIDATES:-}" ]]; then + IFS=: read -r -a candidates <<<"$BASE_BASH_LIBS_TEST_BASH_CANDIDATES" + else + candidates=(/opt/homebrew/bin/bash /usr/local/bin/bash) + fi + + for candidate in "${candidates[@]}"; do + [[ -x "$candidate" ]] || continue + [[ "$candidate" == "${BASH:-}" ]] && continue + exec "$candidate" "$0" "$@" + done + + if [[ -n "${BASE_BASH_LIBS_TEST_BASH_VERSION:-}" ]]; then + display_version="${BASE_BASH_LIBS_TEST_BASH_VERSION:0:1}.${BASE_BASH_LIBS_TEST_BASH_VERSION:1}" + else + display_version="${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}" + fi + + { + printf 'ERROR: base-bash requires Bash 4.2 or newer; current version is %s.\n' "$display_version" + printf 'A supported Bash was not found at /opt/homebrew/bin/bash or /usr/local/bin/bash.\n' + } >&2 + exit 1 +} + +base_bash_lib_dir_is_usable() { + local candidate="${1:-}" + + [[ -n "$candidate" ]] || return 1 + [[ -f "$candidate/std/lib_std.sh" ]] +} + +base_bash_resolve_lib_dir() { + local package_root="$1" + local explicit_dir="${BASE_BASH_LIBS_DIR:-}" + local candidate + + if [[ -n "$explicit_dir" ]]; then + base_bash_lib_dir_is_usable "$explicit_dir" || { + base_bash_die "BASE_BASH_LIBS_DIR '$explicit_dir' does not contain std/lib_std.sh." + } + cd -L -- "$explicit_dir" && pwd -L + return $? + fi + + for candidate in "$package_root/lib/bash" "$package_root/libexec/lib/bash"; do + if base_bash_lib_dir_is_usable "$candidate"; then + cd -L -- "$candidate" && pwd -L + return $? + fi + done + + base_bash_die "Unable to find base-bash-libs under '$package_root'. Set BASE_BASH_LIBS_DIR to a compatible lib/bash directory." +} + +base_bash_source_stdlib() { + local script_path="$1" + local stdlib_path="$BASE_BASH_LIBS_DIR/std/lib_std.sh" + shift + + BASE_BASH_BOOTSTRAP_SOURCE="$script_path" + # shellcheck source=/dev/null + source "$stdlib_path" "$@" || exit $? + unset BASE_BASH_BOOTSTRAP_SOURCE +} + +base_bash_filter_runtime_args() { + base_bash_runtime_args=() + + while (($#)); do + case "$1" in + --debug-wrapper|--verbose-wrapper|--utc-wrapper|--color) + ;; + *) + base_bash_runtime_args+=("$1") + ;; + esac + shift + done +} + +import_base_bash_lib() { + local relative_path="${1:-}" + local lib_path + + [[ -n "$relative_path" ]] || fatal_error "import_base_bash_lib: no library path provided." + [[ "$relative_path" != /* ]] || fatal_error "import_base_bash_lib: expected a path relative to '$BASE_BASH_LIBS_DIR', got '$relative_path'." + + case "$relative_path" in + ..|../*|*/..|*/../*) + fatal_error "import_base_bash_lib: refusing path outside base-bash-libs root: '$relative_path'." + ;; + esac + + lib_path="$BASE_BASH_LIBS_DIR/$relative_path" + [[ -f "$lib_path" ]] || fatal_error "base-bash-libs library '$relative_path' was not found at '$lib_path'." + + # shellcheck source=/dev/null + source "$lib_path" || fatal_error "Failed to import base-bash-libs library '$lib_path'." +} + +base_bash_run_script() { + local script_path="$1" + local package_root="$2" + local -a runtime_args=() + shift 2 + + [[ -f "$script_path" ]] || base_bash_die "Script '$script_path' was not found." + + BASE_BASH_LIBS_DIR="$(base_bash_resolve_lib_dir "$package_root")" || exit $? + export BASE_BASH_LIBS_DIR + + base_bash_filter_runtime_args "$@" + base_bash_source_stdlib "$script_path" "$@" + runtime_args=("${base_bash_runtime_args[@]}") + + unset -f main + + # shellcheck source=/dev/null + source "$script_path" "${runtime_args[@]}" || exit $? + + if ! declare -F main >/dev/null 2>&1; then + fatal_error "Script '$script_path' did not define main()." + fi + + main "${runtime_args[@]}" +} + +base_bash_usage() { + cat >&2 <<'EOF' +Usage: + base-bash