Bash Libraries
August 5, 2026 ยท View on GitHub
Reusable Bash libraries for command wrappers and other Bash tooling.
Layout
std/Foundation library with logging, error handling, PATH helpers, and other shared Bash primitives.git/Git-related helpers built on top of the stdlib.gh/GitHub CLI helpers built on top of the stdlib.file/File-editing helpers built on top of the stdlib.str/String helpers built on top of the stdlib.arg/Argument parsing helpers built on top of the stdlib.list/Indexed-array helpers built on top of the stdlib.cli/Declarative command contracts with parsing, validation, help, and completion.app/Optional application policy for typed configuration, standard options, prompts, and exactly-once lifecycle hooks.tests/Common BATS helpers for Bash library test suites.base-bash-libs.releaseEmbedded release identity copied with installed, archived, vendored, and standalonelib/bashartifacts.
The Base runtime shell files and Base version helpers remain in
basefoundry/base. This repository carries only sourceable reusable library
modules.
Caller Runtime Contract
All public modules support Bash 4.2 or newer with every combination of caller-
selected errexit, nounset, and pipefail. Sourcing a module is passive: it
does not change those settings, any other set or shopt option, IFS,
OPTIND, the working directory, the umask, traps, exports, or ordinary
positional arguments. After sourcing lib_std.sh, callers explicitly invoke
base_init to initialize runtime state and receive wrapper-filtered
arguments in a caller-owned array.
Public API calls preserve the same process state unless their documented
purpose is to change it. Examples of intentional mutation include PATH helpers,
base_std_safe_cd, caller-owned output variables, file-editing helpers, and cleanup
registrations while a hook or path remains active. Transient internal cleanup
registrations restore the caller's preexisting EXIT trap when the operation
finishes.
Required arity is checked before a public helper expands a required positional
parameter, so a usage error remains diagnosable with caller nounset enabled.
Predicates and recoverable failures intentionally return nonzero; callers using
errexit should invoke expected nonzero results in if, while, &&, or
another Bash conditional context.
The standalone tests/bash-option-contract.sh matrix sources every module and
exercises success, usage, predicate, and recoverable-failure paths in all eight
option combinations. CI runs that matrix on the current macOS and Ubuntu Bash
runtimes and in the digest-pinned, networkless Bash 4.2.53 compatibility image.
Naming Contract
The v2 base_ namespace is part of the sourceable-library contract:
- Public functions use
base_<module>_<name>. The stdlib lifecycle functions arebase_initandbase_require_version, while the remaining stdlib functions use thebase_std_segment. - Framework-owned globals, configuration variables, metadata, and load guards
use
BASE_BASH_LIBS_.... - Implementation-only functions use
__base_bash_libs_<module>_...__and are not a supported call surface.
Libraries do not define generic aliases for the old v1 names. This keeps
default loading safe for applications that already have functions such as
import, log_info, or str_trim. See
docs/v2-api-contract.md for the complete
status/effects charter, and docs/v2-symbol-map.md for the complete mapping
and scripts/migrate-v2-symbols for a
mechanical migration aid. The single-file boundary remains unchanged: each
public library is still one physical .sh file.
Public helpers that accept caller-supplied variable or array names reserve the
__ prefix for library-internal state. Passing a caller-owned source or result
name that begins with __ fails before the helper changes caller state. Use a
regular Bash variable name for public input and output values and arrays.
base_std_assert_variable_name is the syntax-only exception: it validates whether any
identifier is legal Bash syntax, including names in the reserved namespace,
without reading or writing the named variable.
When one API accepts multiple caller-owned inputs or outputs, names that would alias an input with an output are rejected before mutation. The module README for that API documents the required distinct-name relationships.
Import contract
Source the stdlib once, then load every companion module with the same package-relative API:
source "/path/to/base-bash-libs/lib/bash/std/lib_std.sh"
base_std_import str/lib_str.sh file/lib_file.sh
base_std_import resolves paths from the loaded package's lib/bash root, so
the caller's cwd, script directory, Homebrew libexec layout, vendored tree,
and symlinked installation path do not change the import. Absolute paths,
.. traversal, and symlinks that escape the package are rejected. Repeated
loads are no-ops; dependency cycles and mixed-major package graphs fail with
diagnostics. The launcher's base_launcher_import_base_bash_lib name is only
a thin process-boundary adapter to this same loader.
The loader promotes top-level declare statements in a module to global
declarations while it is sourced. Module authors can therefore write normal
module-level declarations without knowing that the loader itself is a
function; declarations executed later inside public functions retain normal
Bash scope rules. Every module remains a single physical .sh file.
Standalone launcher contract
bin/base-bash provides the application boundary for scripts that use the
stdlib. base-bash --help and --version are successful stdout commands;
malformed invocations use stderr and status 2. base-bash check is a
non-mutating diagnostic for Bash support, installation health, package identity,
imports, and external tools. See the normative v2 launcher contract
for the complete stream, status, lifecycle, signal, cleanup, and wrapper-flag
mapping rules.
The launcher initializes once, sources the application once, calls main once,
and preserves application argv boundaries and the primary application status.
It does not enable strict mode. Use -- before a script path that begins with
a dash; the launcher separator itself is not forwarded. The standard cleanup
registry composes application hooks with existing traps and preserves 130 for
INT and 143 for TERM.