From 127a318a45fbc0e732cb2c2334a6a205de7f9562 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Thu, 18 Jun 2026 10:55:04 -0700 Subject: [PATCH] Add std_run command runner API --- examples/std-usage.sh | 2 +- lib/bash/std/README.md | 30 ++++++++++++---------- lib/bash/std/lib_std.sh | 30 ++++++++++++++-------- lib/bash/std/tests/lib_std.bats | 45 +++++++++++++++++++++++---------- 4 files changed, 70 insertions(+), 37 deletions(-) diff --git a/examples/std-usage.sh b/examples/std-usage.sh index 03b20e9..18a549c 100755 --- a/examples/std-usage.sh +++ b/examples/std-usage.sh @@ -14,5 +14,5 @@ printf 'example\n' > "$example_file" update_file_section "$example_file" "# BEGIN base-bash-libs" "# END base-bash-libs" "managed=true" log_info "Validated standalone Base Bash library usage." -run --no-exit --quiet test -f "$example_file" +std_run --no-exit --quiet test -f "$example_file" print_message "example_file=$example_file" diff --git a/lib/bash/std/README.md b/lib/bash/std/README.md index 136c40b..eeee5af 100644 --- a/lib/bash/std/README.md +++ b/lib/bash/std/README.md @@ -17,7 +17,7 @@ The library improves Bash-based scripting in a few practical ways: another command may pipe or capture. - **Readable failures**: fatal errors include a message and Bash stack trace instead of a mysterious non-zero exit. -- **Safe command execution**: `run` preserves argument boundaries, supports +- **Safe command execution**: `std_run` preserves argument boundaries, supports dry-run mode, and can either exit or return a status. - **Shared dry-run behavior**: scripts do not need to reimplement "print what would happen" logic. @@ -140,11 +140,11 @@ itself is fine and the user simply gave invalid arguments. ## Running Commands Safely -`run` is the preferred helper for simple external command execution: +`std_run` is the preferred helper for simple external command execution: ```bash -run git status --short -run touch "file with spaces.txt" +std_run git status --short +std_run touch "file with spaces.txt" ``` It improves on ad hoc command strings because it: @@ -158,17 +158,17 @@ Dry-run mode: ```bash DRY_RUN=true -run brew install jq +std_run brew install jq ``` `DRY_RUN` and `dry_run` both accept `true`, `1`, `yes`, and `on`. Use `is_dry_run` when a script needs to branch on the same normalized dry-run state -without executing a command through `run`. +without executing a command through `std_run`. Handle a failing command yourself with `--no-exit`: ```bash -if ! run --no-exit grep "needle" "$file"; then +if ! std_run --no-exit grep "needle" "$file"; then log_info "needle was not present; continuing" fi ``` @@ -177,14 +177,18 @@ For expected probe failures where the caller handles the status, add `--quiet` to suppress the warning: ```bash -if ! run --no-exit --quiet test -f "$optional_file"; then +if ! std_run --no-exit --quiet test -f "$optional_file"; then log_debug "Optional file is absent." fi ``` -Use `run` for commands plus arguments. Keep shell features such as pipelines, -redirection, process substitution, and complex conditionals explicit in the -calling script so the code remains clear. +Use `std_run` for commands plus arguments. Keep shell features such as +pipelines, redirection, process substitution, and complex conditionals explicit +in the calling script so the code remains clear. + +`run` remains available as a compatibility wrapper for existing callers, but new +code should use `std_run` to avoid collisions with test frameworks and other +Bash libraries that define their own `run` helper. ## Importing Other Bash Libraries @@ -308,7 +312,7 @@ main() { assert_command_exists git log_info "Checking project '$project'." - run git status --short + std_run git status --short } main "$@" @@ -325,7 +329,7 @@ source "/path/to/base/lib/bash/std/lib_std.sh" main() { set_log_level DEBUG - run echo "hello" + std_run echo "hello" } main "$@" diff --git a/lib/bash/std/lib_std.sh b/lib/bash/std/lib_std.sh index 584a0c7..3c40ee6 100644 --- a/lib/bash/std/lib_std.sh +++ b/lib/bash/std/lib_std.sh @@ -24,7 +24,7 @@ # __SCRIPT_DIR__ Absolute path to the script that sourced the library. # # Core helpers: -# run [--no-exit] [--quiet] cmd ... +# std_run [--no-exit] [--quiet] cmd ... # # Safe command runner with dry-run & failure handling. # exit_if_error rc msg... # Log + exit when rc != 0 (preserves original status). # fatal_error msg... # Convenience wrapper: exit with last status or 1. @@ -35,7 +35,7 @@ # assert_* utilities # Validation helpers (assert_not_null / assert_integer / ...). # # Patterns: -# run some_cmd # exits on failure; DRY_RUN=true/1/yes/on prints instead. +# std_run some_cmd # exits on failure; DRY_RUN=true/1/yes/on prints instead. # some_cmd || fatal_error ... # preserves failing exit code before terminating. # add_to_path -p "/opt/tools" # inject directories without duplicates. # @@ -632,7 +632,7 @@ is_dry_run() { } # -# run - Safely executes a simple command with its arguments. +# std_run - Safely executes a simple command with its arguments. # # This function is designed to be a secure and robust replacement for using # `eval` or simple command execution. It correctly handles arguments with @@ -652,7 +652,7 @@ is_dry_run() { # failures and is most useful with `--no-exit`. # # Usage: -# run [options] command [arg1] [arg2] ... +# std_run [options] command [arg1] [arg2] ... # # Options: # --no-exit If provided as an initial argument, the script will not @@ -663,22 +663,24 @@ is_dry_run() { # # Examples: # # Run a simple command. Exits if `ls` fails. -# run ls -l /tmp +# std_run ls -l /tmp # # # Run a command with spaces in an argument. -# run touch "a file with spaces.txt" +# std_run touch "a file with spaces.txt" # # # Run a command but don't exit the script on failure. -# if ! run --no-exit grep "not_found" /etc/hosts; then +# if ! std_run --no-exit grep "not_found" /etc/hosts; then # log "INFO" "The text was not found, but we are continuing." # fi # # # In a script where DRY_RUN=true, this will only print the command. # DRY_RUN=true -# run rm -rf /some/important/path +# std_run rm -rf /some/important/path # ################################################################################ -run() { +__std_run_impl__() { + local helper_name="$1" + shift local exit_on_failure=1 quiet=0 # Parse optional run flags before the command. @@ -704,7 +706,7 @@ run() { # Check if the command is empty. if [[ $# -eq 0 ]]; then - log_error "run: No command provided." + log_error "$helper_name: No command provided." return 1 fi @@ -741,6 +743,14 @@ run() { return 0 } +std_run() { + __std_run_impl__ std_run "$@" +} + +run() { + __std_run_impl__ run "$@" +} + ############################################## FILE AND DIRECTORY HANDLING ############################################ # diff --git a/lib/bash/std/tests/lib_std.bats b/lib/bash/std/tests/lib_std.bats index f52bf7f..8ed0f33 100644 --- a/lib/bash/std/tests/lib_std.bats +++ b/lib/bash/std/tests/lib_std.bats @@ -745,31 +745,36 @@ EOF [[ "$output" == *"fatal boom"* ]] } -@test "run returns an error when no command is provided" { +@test "std_run is the preferred command runner and run remains compatible" { + [ "$(type -t std_run)" = "function" ] + [ "$(type -t run)" = "function" ] +} + +@test "std_run returns an error when no command is provided" { local stderr_file="$TEST_TMPDIR/run-empty.err" local rc - if run 2>"$stderr_file"; then + if std_run 2>"$stderr_file"; then rc=0 else rc=$? fi [ "$rc" -eq 1 ] - [[ "$(cat "$stderr_file")" == *"run: No command provided."* ]] + [[ "$(cat "$stderr_file")" == *"std_run: No command provided."* ]] } -@test "run honors dry-run mode without executing the command" { +@test "std_run honors dry-run mode without executing the command" { local target="$TEST_TMPDIR/dry-run.txt" DRY_RUN=true - run touch "$target" + std_run touch "$target" [ "$?" -eq 0 ] [ ! -e "$target" ] } -@test "run treats common truthy dry-run values as dry-run mode" { +@test "std_run treats common truthy dry-run values as dry-run mode" { local case_name target value var_name for case_name in \ @@ -787,18 +792,18 @@ EOF export "$var_name" target="$TEST_TMPDIR/dry-run-${var_name}-${value}.txt" - run touch "$target" + std_run touch "$target" [ "$?" -eq 0 ] [ ! -e "$target" ] done } -@test "run --no-exit returns the underlying failure status" { +@test "std_run --no-exit returns the underlying failure status" { local stderr_file="$TEST_TMPDIR/run-no-exit.err" local rc - if run --no-exit bash -c 'exit 7' 2>"$stderr_file"; then + if std_run --no-exit bash -c 'exit 7' 2>"$stderr_file"; then rc=0 else rc=$? @@ -808,11 +813,11 @@ EOF [[ "$(cat "$stderr_file")" == *"continuing"* ]] } -@test "run --no-exit --quiet suppresses failure warning" { +@test "std_run --no-exit --quiet suppresses failure warning" { local stderr_file="$TEST_TMPDIR/run-no-exit-quiet.err" local rc - if run --no-exit --quiet bash -c 'exit 7' 2>"$stderr_file"; then + if std_run --no-exit --quiet bash -c 'exit 7' 2>"$stderr_file"; then rc=0 else rc=$? @@ -822,13 +827,13 @@ EOF [ ! -s "$stderr_file" ] } -@test "run exits the script on failure by default" { +@test "std_run exits the script on failure by default" { local script="$TEST_TMPDIR/run-fail.sh" create_script "$script" <"$stderr_file"; then + rc=0 + else + rc=$? + fi + + [ "$rc" -eq 7 ] + [ ! -s "$stderr_file" ] +} + @test "safe_mkdir creates directories and tolerates existing paths with -p" { local first="$TEST_TMPDIR/a" local second="$TEST_TMPDIR/b/c"