Native Builtins
argsh ships with optional Bash loadable builtins compiled from Rust. When the shared library is available, the core parsing commands run as native code inside the Bash process — zero fork overhead, zero subshell cost.
Overview
Bash supports loadable builtins via enable -f <path.so> <name>. argsh compiles its core functions (:args, :usage, type converters, introspection helpers) into a single .so that can be loaded at runtime. This replaces the pure-Bash implementations with native code while maintaining identical behavior.
The key benefits are:
- Performance: No subshell forks for type checking, field parsing, or help generation
- Transparent fallback: If the
.sois not found, everything works as before with the pure-Bash implementation - Identical behavior: The builtin output is byte-for-byte identical to the Bash implementation (validated by the test suite)
Available Builtins
| Builtin | Purpose |
|---|---|
:args | CLI argument parser with type checking |
:usage | Subcommand router with help generation |
is::array | Test if a variable is declared as an array |
is::uninitialized | Test if a variable is uninitialized |
is::set | Test if a variable is set (has a value) |
is::tty | Test if stdout is a terminal |
args::field_name | Extract variable name from a field definition |
to::int | Validate and pass through integer values |
to::float | Validate and pass through float values |
to::boolean | Convert values to boolean (0 or 1) |
to::file | Validate that a file path exists |
to::string | Identity conversion (pass through) |
import | Import modules with selective imports and aliasing |
import::clear | Clear the import cache |
Building
The builtin crate requires a Rust toolchain. From the repository root:
The shared library is output to builtin/target/release/libargsh.so. Copy it as argsh.so to a directory in your search path.
The .so is compiled for the current platform. Cross-compilation requires the appropriate Rust target and compatible bash headers.
Loading
Automatic (recommended)
args.sh auto-detects and loads argsh.so at source time. It searches in order:
ARGSH_BUILTIN_PATH— explicit full path to the.soPATH_LIB/argsh.so— project library directoryPATH_BIN/argsh.so— project binary directoryLD_LIBRARY_PATH— standard system library path (colon-separated)BASH_LOADABLES_PATH— standard bash loadable builtins path (colon-separated)
# Option 1: Set explicit path
export ARGSH_BUILTIN_PATH="/path/to/argsh.so"
source libraries/args.sh
# Option 2: Copy to PATH_BIN (used by .envrc)
cp builtin/target/release/libargsh.so .bin/argsh.so
source libraries/args.sh # finds it via PATH_BIN
# Option 3: Install system-wide
cp builtin/target/release/libargsh.so /usr/local/lib/argsh.so
export BASH_LOADABLES_PATH="/usr/local/lib"
source libraries/args.sh
When builtins are loaded, args.sh sets ARGSH_BUILTIN=1 and skips the pure-Bash function definitions. The is and to libraries are still sourced, but their function definitions are removed via unset -f, allowing the faster builtin implementations from the .so to take effect.
Manual
You can load the builtins manually with enable -f:
To verify builtins are loaded:
How It Works
Bash loadable builtins are shared libraries that export a specific struct per builtin name. When enable -f is called, bash loads the .so via dlopen and looks up the <name>_struct symbol for each builtin name.
The argsh .so is compiled from Rust using the bash_builtins crate for FFI bindings. Each builtin:
- Receives the argument word list from bash
- Converts it to Rust
Vec<String> - Performs parsing/validation in native code
- Sets shell variables directly via bash FFI (
find_variable,set,array_set) - Returns an exit code
All builtins are wrapped in std::panic::catch_unwind to prevent Rust panics from crashing the bash process.
Priority and Function Overrides
Bash resolves commands in this order: aliases > functions > builtins. When args.sh auto-loads the .so, it wraps the pure-Bash function definitions in if ! (( ARGSH_BUILTIN )); then ... fi, so they are never defined when builtins are active. This way the builtins take effect without needing to unset -f anything.
If you load builtins manually after sourcing args.sh, the bash functions will shadow them. Use unset -f :args :usage to let the builtins take precedence.
Testing
The builtin implementation shares the same test suite as the pure-Bash implementation. Set ARGSH_BUILTIN_TEST=1 to run with native builtins:
Both modes produce byte-for-byte identical snapshot output, ensuring the builtin implementation matches the Bash implementation exactly.
Benchmark
Measured with bash bench/usage-depth.sh — 50 iterations per cell.
Subcommand dispatch (cmd x x ... x -h)
| Depth | Pure Bash | Builtin | Speedup |
|---|---|---|---|
| 10 | 1188 ms | 21 ms | 57x |
| 25 | 2686 ms | 53 ms | 51x |
| 50 | 5434 ms | 155 ms | 35x |
Argument parsing (cmd --flag1 v1 ... --flagN vN)
| Flags | Pure Bash | Builtin | Speedup |
|---|---|---|---|
| 10 | 5405 ms | 4 ms | 1351x |
| 25 | 13986 ms | 9 ms | 1554x |
| 50 | 29603 ms | 20 ms | 1480x |
Real-world (:usage + :args at every level, depth 10)
| Scenario | Pure Bash | Builtin | Speedup |
|---|---|---|---|
| 10 levels | 567 ms | 43 ms | 13x |
Each level parses 2 flags via :usage, then dispatches a subcommand. This reflects a typical CLI with nested commands where each level accepts its own options.
Inline .so Variant (argsh-so)
CI produces a second artifact, argsh-so, that embeds the .so as a base64 blob directly inside the minified argsh script. On startup it decodes the blob to a temp file, loads it via enable -f, then removes the temp file. This gives you a single self-contained script with native performance — no external .so needed.
The inline .so is platform-specific (linux/amd64). The regular argsh script with pure-Bash fallback remains the portable default.
Shebang Parameters
When using argsh as a shebang interpreter, you can pass flags before the script path:
| Flag | Description |
|---|---|
--builtin [path] | Require native builtins. If path given and not found, fail. |
--no-builtin | Skip builtin loading entirely. |
-i, --import <lib> | Import additional libraries (repeatable). |
--version | Print argsh version and exit. |
Custom Types
Custom to:: types defined as bash functions continue to work when builtins are loaded. The builtin :args command calls custom type functions via parse_and_execute internally. Only the built-in types (int, float, boolean, file, string) run as native code.