lib_std.sh is the foundation library for Bash scripts in the Base family.
Bash is excellent glue, but raw Bash makes it too easy for every script to
invent its own logging, path handling, argument conventions, dry-run behavior,
error reporting, and import rules. lib_std.sh gives Bash code one shared
toolbox so scripts can stay small, readable, and consistent.
The library improves Bash-based scripting in a few practical ways:
- Consistent logs: every script can emit timestamped logs with level and source location.
- Logs stay on stderr: stdout remains available for real program output that 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:
std_runpreserves argument boundaries, supports dry-run mode, and can either exit or return a status. - Bounded command execution:
std_run_with_timeoutapplies 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
EXITtrap. - Portable temp state: scripts can create temp files or directories under
TMPDIRand 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 append intentionally.
- Batch validation: required variables, files, directories, commands, and integer ranges can be checked with one helper call.
- Safer filesystem helpers: common operations report all failures in one clear error.
- Base wrapper integration: wrapper flags are recognized once and removed before command-specific argument parsing begins.
The goal is not to hide Bash. The goal is to make scripts fail in ways a user or developer can understand.
Standalone scripts can source the library directly:
source "/absolute/path/to/lib/bash/std/lib_std.sh"Base entrypoints preload this library through Base's own runtime bootstrap. Standalone scripts should source it explicitly. Callers should run on Bash 4.2 or newer; the library has passive Bash version helpers, but sourcing it does not prompt, install packages, or re-exec the caller.
Sourcing lib_std.sh runs a small one-time initializer:
- initializes the logging level map
- 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
Caller-visible globals:
BASE_BASH_LIBS_VERSION: readonly package version read from the rootVERSIONfileBASE_BASH_LIBS_STDLIB_LOADED: readonly marker set to1afterlib_std.shhas initialized successfully__SCRIPT_ARGS__: original arguments before wrapper flags were stripped__SCRIPT_DIR__: absolute source directory for the script being bootstrapped
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.
Use base_bash_libs_require_version when a downstream script depends on APIs
added after the first public release:
base_bash_libs_require_version 1.1.0The 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.
Use structured logging for operational messages:
log_info "Installing package '$name'."
log_warn "Cache directory does not exist: $cache_dir"
log_error "Unable to read manifest '$manifest_path'."
log_debug "resolved_home=$resolved_home"
log_verbose "raw_response=$response"Available levels:
FATALERRORWARNINFODEBUGVERBOSE
Change the default logger's level with:
set_log_level DEBUGNamed loggers are also supported:
set_log_level -l artifact DEBUG
log_debug -l artifact "registry key: $key"For user-facing messages that should not include timestamps or source locations, use:
print_error "Invalid project name."
print_warn "Using default workspace."
print_info "Setup complete."
print_success "Done."
print_message "plain stdout message"log_*, print_error, print_warn, print_info, and print_success write to
stderr. print_bold and print_message write to stdout.
Colors are only enabled for terminal stderr when --color is passed. Set
NO_COLOR to disable colored output even when --color is present.
Use fatal_error when the script cannot continue:
[[ -f "$manifest_path" ]] || fatal_error "Manifest '$manifest_path' was not found."Use exit_if_error when checking a command's explicit status:
some_command
exit_if_error $? "some_command failed."Fatal failures log the message, dump a Bash stack trace, and exit with the original failing status when possible.
Not every user mistake should be fatal. Command-line usage errors should usually
print usage and return 2 rather than calling fatal_error, because the command
itself is fine and the user simply gave invalid arguments.
std_run is the preferred helper for simple external command execution:
std_run git status --short
std_run touch "file with spaces.txt"It improves on ad hoc command strings because it:
- executes commands as argument arrays, not through
eval - preserves spaces and special characters
- logs a copy-pastable command in dry-run mode
- exits through
exit_if_errorby default when a command fails
Dry-run mode:
DRY_RUN=true
std_run brew install jqDRY_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 std_run.
Handle a failing command yourself with --no-exit:
if ! std_run --no-exit grep "needle" "$file"; then
log_info "needle was not present; continuing"
fiFor expected probe failures where the caller handles the status, add --quiet
to suppress the warning:
if ! std_run --no-exit --quiet test -f "$optional_file"; then
log_debug "Optional file is absent."
fiUse 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.
Use std_run_with_timeout when a command must finish within a bounded number of
seconds:
std_run_with_timeout 30 curl -fsSL "$health_url"It accepts the same initial --no-exit and --quiet options as std_run:
if ! std_run_with_timeout --no-exit --quiet 5 nc -z localhost 5432; then
log_warn "database port did not open within 5 seconds"
fiTimeouts 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.
Use import to source helper libraries:
import file/lib_file.sh
import /absolute/path/to/another_lib.shRelative imports resolve from __SCRIPT_DIR__, which is the directory of the
script being bootstrapped.
Important Bash detail: imported files are sourced inside the import function.
If an imported library needs global variables, declare them with -g:
declare -gA MY_LOOKUP=()Without -g, Bash may create locals scoped to the import function.
Use add_to_path instead of hand-editing PATH:
add_to_path "/opt/tool/bin"
add_to_path -p "$HOME/.local/bin"
add_to_path -n "$maybe_created_later/bin"Options:
-p: prepend instead of append-n: do not require the directory to already exist
add_to_path de-duplicates PATH after adding entries. You can also call:
dedupe_path
print_pathThe safe filesystem helpers collect failures and report them clearly:
safe_mkdir -p "$state_dir" "$cache_dir"
safe_touch "$log_file"
safe_truncate "$log_file"
safe_cd "$project_root"These helpers are useful in setup scripts where a partially completed operation should fail loudly and explain which path could not be created, touched, or entered.
safe_mkdir accepts only -p as an option. Calling it without directory
arguments logs a warning and returns success without creating anything.
Use cleanup registration when a script creates transient state that should be removed on exit:
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:
cleanup_workspace() {
rm -rf -- "$workspace"
}
std_register_cleanup_hook cleanup_workspace
std_unregister_cleanup_hook cleanup_workspaceHooks 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.
Use temp helpers when a script needs a scratch file or directory and wants the path stored in a variable:
std_make_temp_file temp_file base
std_make_temp_dir temp_dir workspaceBoth 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:
std_make_temp_dir workspace_dir
printf 'payload\n' > "$workspace_dir/input.txt"Pass --keep when the caller intentionally owns cleanup:
std_make_temp_file --keep report_path reportThe 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.
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:
if std_command_path git_path git; then
std_run "$git_path" status --short
else
log_warn "git is not available; skipping repository status."
fiThe 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:
if std_function_exists cleanup_workspace; then
std_register_cleanup_hook cleanup_workspace
fiUse assert_function_exists when missing functions should be fatal:
assert_function_exists main cleanup_workspaceUse assertions near the top of functions to make assumptions explicit:
assert_arg_count "$#" 2
assert_variable_name result_var array_var
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"assert_not_null takes variable names, not expanded values. Use
assert_not_null TOKEN, not assert_not_null "$TOKEN". When an argument is not
a valid Bash variable name, assert_not_null reports likely misuse without
echoing the invalid value.
Use assert_variable_name when a helper accepts variable names but does not
require those variables to exist or contain values.
The assertions favor clear failure messages over scattered one-off tests. Some
helpers check all provided values and report all missing items together.
Use assert_executable for explicit paths to project-local tools or scripts;
use assert_command_exists for commands that should be discoverable through
PATH.
For interactive scripts:
if ask_yes_no "Continue?"; then
log_info "Continuing."
fi
wait_for_enter "Press Enter after reviewing the output."Use is_interactive before prompting from code paths that might run in CI,
cron, or another non-interactive environment:
if is_interactive; then
ask_yes_no "Install optional tools?" || return 0
fiA small Base-style Bash command should look like this:
#!/usr/bin/env bash
main() {
local project="${1:-}"
if [[ -z "$project" ]]; then
print_error "Project name is required."
return 2
fi
assert_command_exists git
log_info "Checking project '$project'."
std_run git status --short
}
main "$@"When the script runs through basectl, the Base runtime provides the stdlib and
calls main with wrapper flags already filtered out.
For standalone scripts that source the library directly:
#!/usr/bin/env bash
source "/path/to/base/lib/bash/std/lib_std.sh"
main() {
set_log_level DEBUG
std_run echo "hello"
}
main "$@"lib_std.sh should contain small, broadly useful primitives for Bash code:
- logging
- error handling
- path manipulation
- command execution
- 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 file library, and artifact setup behavior belongs in setup code.
BATS coverage lives in:
lib/bash/std/tests/lib_std.bats
When changing this library, run:
bats lib/bash/std/tests/lib_std.batsFor command-level changes that depend on stdlib behavior, also run the relevant command BATS tests.