diff --git a/README.md b/README.md index 97d63a4..a2ae5f4 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,12 @@ import "$base_bash_libs_dir/lib/bash/str/lib_str.sh" After `lib_std.sh` is sourced, `BASE_BASH_LIBS_VERSION` contains the package version from the repository/package `VERSION` file. Downstream scripts can use -that readonly constant when they need to require a minimum library version. +that readonly constant when they need to display the loaded library version. +Use `base_bash_libs_require_version` to require a minimum library version: + +```bash +base_bash_libs_require_version 1.1.0 +``` See `examples/std-usage.sh` for a small standalone script that sources the stdlib, imports the file helpers, logs progress, and runs a checked command. diff --git a/lib/bash/std/README.md b/lib/bash/std/README.md index 993ef94..d46f3dd 100644 --- a/lib/bash/std/README.md +++ b/lib/bash/std/README.md @@ -82,6 +82,19 @@ When a Base wrapper preloads the stdlib for another command, it can set `BASE_BASH_BOOTSTRAP_SOURCE` so `__SCRIPT_DIR__` still points at the command script rather than the wrapper. +## Version Requirements + +Use `base_bash_libs_require_version` when a downstream script depends on APIs +added after the first public release: + +```bash +base_bash_libs_require_version 1.1.0 +``` + +The helper compares dotted numeric versions, returns silently when the loaded +library is new enough, and exits with a clear fatal error when the loaded +`BASE_BASH_LIBS_VERSION` is too old. + ## Logging Use structured logging for operational messages: diff --git a/lib/bash/std/lib_std.sh b/lib/bash/std/lib_std.sh index 464ed2f..4579231 100644 --- a/lib/bash/std/lib_std.sh +++ b/lib/bash/std/lib_std.sh @@ -41,6 +41,8 @@ # std_command_path var cmd # Resolve an external command path without exiting. # std_function_exists fn # Predicate for defined Bash functions. # assert_variable_name name # Validate Bash variable-name arguments. +# base_bash_libs_require_version min_version +# # Exit clearly if the loaded library is too old. # add_to_path [-n] [-p] dir # Append/prepend unique PATH entries. # set_log_level [LEVEL] # Adjust default logger (FATAL..VERBOSE). # log_info/debug/... msgs # Structured logging (color in interactive shells). @@ -121,6 +123,64 @@ BASE_BASH_LIBS_VERSION="$(__lib_std_read_package_version__ "$__BASE_BASH_LIBS_RO readonly BASE_BASH_LIBS_VERSION unset -f __lib_std_read_package_version__ +__base_bash_libs_is_dotted_numeric_version__() { + local version="${1-}" version_re='^[0-9]+([.][0-9]+)*$' + [[ "$version" =~ $version_re ]] +} + +__base_bash_libs_version_at_least__() { + local actual_version="$1" minimum_version="$2" + local -a actual_parts=() minimum_parts=() + local index max_parts actual_part minimum_part actual_number minimum_number + + IFS=. read -r -a actual_parts <<<"$actual_version" + IFS=. read -r -a minimum_parts <<<"$minimum_version" + + max_parts="${#actual_parts[@]}" + if ((${#minimum_parts[@]} > max_parts)); then + max_parts="${#minimum_parts[@]}" + fi + + for ((index = 0; index < max_parts; index++)); do + actual_part="${actual_parts[$index]:-0}" + minimum_part="${minimum_parts[$index]:-0}" + actual_number=$((10#$actual_part)) + minimum_number=$((10#$minimum_part)) + + if ((actual_number > minimum_number)); then + return 0 + fi + if ((actual_number < minimum_number)); then + return 1 + fi + done + + return 0 +} + +# +# base_bash_libs_require_version - Requires a minimum base-bash-libs version. +# +# Usage: +# base_bash_libs_require_version 1.1.0 +# +base_bash_libs_require_version() { + local minimum_version="${1-}" + + assert_arg_count "$#" 1 + + if ! __base_bash_libs_is_dotted_numeric_version__ "$minimum_version" || + ! __base_bash_libs_is_dotted_numeric_version__ "$BASE_BASH_LIBS_VERSION"; then + fatal_error "base_bash_libs_require_version expects dotted numeric versions." + fi + + if ! __base_bash_libs_version_at_least__ "$BASE_BASH_LIBS_VERSION" "$minimum_version"; then + fatal_error "base-bash-libs $minimum_version or newer is required; loaded version is $BASE_BASH_LIBS_VERSION." + fi + + return 0 +} + # # Memorize the original script arguments at the very beginning. # This allows the library to parse global options before the main script does. diff --git a/lib/bash/std/tests/lib_std.bats b/lib/bash/std/tests/lib_std.bats index ca6f150..ec77f3b 100644 --- a/lib/bash/std/tests/lib_std.bats +++ b/lib/bash/std/tests/lib_std.bats @@ -174,6 +174,46 @@ EOF readonly -p BASE_BASH_LIBS_VERSION >/dev/null } +@test "base_bash_libs_require_version accepts the loaded version and older versions" { + local stdout_file="$TEST_TMPDIR/version-check.out" + + base_bash_libs_require_version "$BASE_BASH_LIBS_VERSION" >"$stdout_file" + base_bash_libs_require_version "0.1.0" >>"$stdout_file" + + [ ! -s "$stdout_file" ] +} + +@test "base_bash_libs_require_version exits when the loaded version is too old" { + local script="$TEST_TMPDIR/version-too-old.sh" + + create_script "$script" </dev/null