Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ and versions are tracked in the repo-root `VERSION` file.

- Added `lib/bash/str/lib_str.sh` with string case, trim, predicate, split,
join, and membership helpers.
- Added a documented stdlib-loaded marker for companion-library dependency
guards.
- Added stdlib cleanup hook and cleanup path registration backed by a shared
`EXIT` trap.
- Added portable stdlib temporary file and directory helpers with default exit
cleanup.
- Added stdlib command path and function introspection helpers.
- Added `std_run_with_timeout` for bounded command execution with macOS/Linux
fallback behavior.

## [0.2.1] - 2026-06-18

Expand Down
2 changes: 1 addition & 1 deletion lib/bash/file/lib_file.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#

[[ -n "${__lib_file_sourced__:-}" ]] && return 0
if ! declare -F log_error >/dev/null || ! declare -F log_debug >/dev/null; then
if [[ "${BASE_BASH_LIBS_STDLIB_LOADED:-}" != "1" ]]; then
printf '%s\n' "Error: lib_file.sh requires lib_std.sh to be sourced first." >&2
return 1 2>/dev/null || exit 1
fi
Expand Down
8 changes: 8 additions & 0 deletions lib/bash/file/tests/lib_file.bats
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ file_mode() {
[[ "$output" != *"command not found"* ]]
}

@test "lib_file requires the stdlib loaded marker" {
bats_run bash -c 'log_error() { :; }; log_debug() { :; }; source "$1"; rc=$?; printf "source-rc=%s\n" "$rc"; exit "$rc"' bash "$BASE_BASH_DIR/file/lib_file.sh"

[ "$status" -eq 1 ]
[[ "$output" == *"lib_file.sh requires lib_std.sh to be sourced first"* ]]
[[ "$output" == *"source-rc=1"* ]]
}

@test "update_file_section writes option-like markers literally" {
local target="$TEST_TMPDIR/config.txt"
printf 'line-one' > "$target"
Expand Down
2 changes: 1 addition & 1 deletion lib/bash/git/lib_git.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#

[[ -n "${__lib_git_sourced__:-}" ]] && return 0
if ! declare -F log_error >/dev/null || ! declare -F log_debug >/dev/null; then
if [[ "${BASE_BASH_LIBS_STDLIB_LOADED:-}" != "1" ]]; then
printf '%s\n' "Error: lib_git.sh requires lib_std.sh to be sourced first." >&2
return 1 2>/dev/null || exit 1
fi
Expand Down
8 changes: 8 additions & 0 deletions lib/bash/git/tests/lib_git.bats
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ setup() {
[[ "$output" != *"command not found"* ]]
}

@test "lib_git requires the stdlib loaded marker" {
bats_run bash -c 'log_error() { :; }; log_debug() { :; }; source "$1"; rc=$?; printf "source-rc=%s\n" "$rc"; exit "$rc"' bash "$BASE_BASH_DIR/git/lib_git.sh"

[ "$status" -eq 1 ]
[[ "$output" == *"lib_git.sh requires lib_std.sh to be sourced first"* ]]
[[ "$output" == *"source-rc=1"* ]]
}

@test "git_get_current_branch returns the current branch name" {
local repo="$TEST_TMPDIR/repo"
local branch=""
Expand Down
123 changes: 123 additions & 0 deletions lib/bash/std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ The library improves Bash-based scripting in a few practical ways:
instead of a mysterious non-zero exit.
- **Safe command execution**: `std_run` preserves argument boundaries, supports
dry-run mode, and can either exit or return a status.
- **Bounded command execution**: `std_run_with_timeout` applies the same command
runner conventions with a timeout.
- **Shared dry-run behavior**: scripts do not need to reimplement "print what
would happen" logic.
- **Composable cleanup**: scripts can register exit cleanup without replacing
an already-installed `EXIT` trap.
- **Portable temp state**: scripts can create temp files or directories under
`TMPDIR` and register them for cleanup in one call.
- **Non-fatal introspection**: scripts can resolve command paths and check
function availability without turning every probe into a hard exit.
- **Simple library imports**: scripts can import helpers relative to their own
source directory.
- **Predictable PATH edits**: PATH additions avoid duplicates and can prepend or
Expand Down Expand Up @@ -56,6 +64,7 @@ Sourcing `lib_std.sh` runs a small one-time initializer:
- records the original script arguments in `__SCRIPT_ARGS__`
- derives the caller's source directory in `__SCRIPT_DIR__`
- exposes the package version in `BASE_BASH_LIBS_VERSION`
- exposes the successful stdlib load marker in `BASE_BASH_LIBS_STDLIB_LOADED`
- consumes Base wrapper flags such as `--debug-wrapper`, `--verbose-wrapper`,
`--utc-wrapper`, and `--color`
- resets the caller's positional parameters to the filtered argument list
Expand All @@ -64,6 +73,8 @@ Caller-visible globals:

- `BASE_BASH_LIBS_VERSION`: readonly package version read from the root
`VERSION` file
- `BASE_BASH_LIBS_STDLIB_LOADED`: readonly marker set to `1` after
`lib_std.sh` has initialized successfully
- `__SCRIPT_ARGS__`: original arguments before wrapper flags were stripped
- `__SCRIPT_DIR__`: absolute source directory for the script being bootstrapped

Expand Down Expand Up @@ -196,6 +207,26 @@ in the calling script so the code remains clear.
code should use `std_run` to avoid collisions with test frameworks and other
Bash libraries that define their own `run` helper.

Use `std_run_with_timeout` when a command must finish within a bounded number of
seconds:

```bash
std_run_with_timeout 30 curl -fsSL "$health_url"
```

It accepts the same initial `--no-exit` and `--quiet` options as `std_run`:

```bash
if ! std_run_with_timeout --no-exit --quiet 5 nc -z localhost 5432; then
log_warn "database port did not open within 5 seconds"
fi
```

Timeouts return status `124`. The helper prefers `timeout` or `gtimeout` when
available and otherwise uses a Bash fallback so scripts work on macOS and Linux.
As with `std_run`, command arguments are executed as an argument array and
dry-run mode logs without running the command.

## Importing Other Bash Libraries

Use `import` to source helper libraries:
Expand Down Expand Up @@ -257,6 +288,94 @@ entered.
`safe_mkdir` accepts only `-p` as an option. Calling it without directory
arguments logs a warning and returns success without creating anything.

## Cleanup Helpers

Use cleanup registration when a script creates transient state that should be
removed on exit:

```bash
workspace="$(mktemp -d)"
std_register_cleanup_path "$workspace"
```

Cleanup paths are removed with `rm -rf --` from a shared `EXIT` trap. Empty
paths, root paths, and current/parent directory traversal components are
rejected before registration.

For custom cleanup, register a function name:

```bash
cleanup_workspace() {
rm -rf -- "$workspace"
}

std_register_cleanup_hook cleanup_workspace
std_unregister_cleanup_hook cleanup_workspace
```

Hooks run in registration order and duplicate registrations are ignored. If an
`EXIT` trap already exists when the first cleanup hook or path is registered,
that existing trap is preserved and runs before the stdlib cleanup hooks.

## Temporary Path Helpers

Use temp helpers when a script needs a scratch file or directory and wants the
path stored in a variable:

```bash
std_make_temp_file temp_file base
std_make_temp_dir temp_dir workspace
```

Both helpers create paths under `${TMPDIR:-/tmp}` using `mktemp` templates that
work on macOS/BSD and GNU systems. The created path is registered for exit
cleanup by default:

```bash
std_make_temp_dir workspace_dir
printf 'payload\n' > "$workspace_dir/input.txt"
```

Pass `--keep` when the caller intentionally owns cleanup:

```bash
std_make_temp_file --keep report_path report
```

The optional prefix is a filename prefix, not a directory path. It must be
non-empty and must not contain `/`. Set `TMPDIR` before calling the helper when
the temp root should be somewhere other than `/tmp`.

## Introspection Helpers

Use `std_command_path` when a script needs the path to an external command but
wants to decide what to do if it is absent:

```bash
if std_command_path git_path git; then
std_run "$git_path" status --short
else
log_warn "git is not available; skipping repository status."
fi
```

The helper stores an executable path in the named result variable and returns
nonzero with an empty result when the command is not found.

Use `std_function_exists` for predicate-style checks:

```bash
if std_function_exists cleanup_workspace; then
std_register_cleanup_hook cleanup_workspace
fi
```

Use `assert_function_exists` when missing functions should be fatal:

```bash
assert_function_exists main cleanup_workspace
```

## Validation Helpers

Use assertions near the top of functions to make assumptions explicit:
Expand All @@ -267,6 +386,7 @@ assert_not_null BASE_HOME project_name
assert_integer retry_count
assert_integer_range retry_count 0 5
assert_command_exists git brew
assert_function_exists main cleanup_workspace
assert_file_exists "$manifest_path"
assert_executable "$project_root/bin/build"
assert_dir_exists "$project_root"
Expand Down Expand Up @@ -355,6 +475,9 @@ main "$@"
- imports
- validation
- simple filesystem safety wrappers
- exit cleanup registration
- temporary file and directory creation
- command and function introspection

Domain-specific behavior should live in other libraries or command modules. For
example, Git helpers belong in a Git library, file editing helpers belong in a
Expand Down
Loading
Loading