Bash authoring kit

afdata shell bash prints a sourceable Bash library. It gives executable scripts AFDATA-style arguments, config reads, events, and child-process status without changing the child program’s output or interaction model.

#!/usr/bin/env bash
set -euo pipefail

_AFDATA_BASH_SOURCE="$("${AFDATA_BIN:-afdata}" shell bash)"
source /dev/stdin <<<"$_AFDATA_BASH_SOURCE"
unset _AFDATA_BASH_SOURCE

afdata_args_begin "deploy.sh [OPTIONS] PROJECT [-- WRANGLER_ARG ...]"
afdata_args_option config_path --config PATH "Configuration file" config.toml
afdata_args_flag dry_run --dry-run "Prepare without deploying"
afdata_args_positional project PROJECT "Project to deploy"
afdata_args_rest WRANGLER_ARG "Arguments forwarded to wrangler"
afdata_args_parse "$@"

account_id="$(afdata_config_get "$config_path" cloudflare.account_id)"
afdata_log info "Preparing ${project} for account ${account_id}"

if [ "$dry_run" = false ]; then
  afdata_run wrangler deploy ${AFDATA_ARGS_REST[@]+"${AFDATA_ARGS_REST[@]}"}
fi

afdata_result "Deployment complete"

The library supports Bash 3.2 and later. Sourcing it is silent and does not change errexit, nounset, pipefail, or any other caller option.

Bash 3.2 — still the stock macOS shell — aborts on "${arr[@]}" when the array is empty and nounset is set. AFDATA_ARGS_REST is empty whenever the caller passes no trailing arguments, so forward it with the guarded expansion ${AFDATA_ARGS_REST[@]+"${AFDATA_ARGS_REST[@]}"} shown above, and use the same form for any array of your own that may be empty.

Loading and pinning

Installing the CLI is sufficient; the Bash library is embedded in the binary:

cargo install --path . --force
afdata shell bash >/dev/null

For scripts that use the installed afdata version directly:

_AFDATA_BASH_SOURCE="$("${AFDATA_BIN:-afdata}" shell bash)"
source /dev/stdin <<<"$_AFDATA_BASH_SOURCE"
unset _AFDATA_BASH_SOURCE

The assignment deliberately runs as its own command: under set -e, failure to execute afdata shell bash stops the script instead of silently sourcing an empty stream. /dev/stdin plus a here-string works with macOS Bash 3.2; process substitution (source <(...)) is not portable across all Bash 3.2 builds.

For a reviewed, repository-pinned copy:

mkdir -p scripts/lib
afdata shell bash > scripts/lib/afdata.sh

Then source that file relative to the calling script. bash/afdata.sh is the canonical source and is embedded into the afdata binary at compile time, so cargo install, Homebrew, Scoop/Git Bash, and release archives all expose the same afdata shell bash command without installing a separate shared-data directory.

Set AFDATA_BIN to an alternate executable path when necessary. Event helpers use AFDATA_OUTPUT (json by default) and AFDATA_OUTPUT_TO (split by default).

After loading, AFDATA_BASH_API_VERSION contains the integer API version of the helper surface (currently 1). A script that depends on a pinned helper contract can check this value immediately after sourcing; incompatible helper changes increment it. afdata_cli is the public escape hatch for invoking the selected AFDATA_BIN without duplicating its path lookup.

Arguments

Start a declaration, add options, flags, and positional arguments, then parse:

afdata_args_begin "build.sh [OPTIONS] PACKAGE [-- CARGO_ARG ...]"
afdata_args_option config_path --config PATH "Configuration file" config.toml
afdata_args_flag release --release "Build release artifacts"
afdata_args_positional package PACKAGE "Package to build"
afdata_args_rest CARGO_ARG "Arguments forwarded to cargo"
afdata_args_parse "$@"

Declarations assign directly to their named Bash variables. Flags become true or false; options and positional arguments are strings; trailing arguments are preserved byte-for-byte in AFDATA_ARGS_REST. Both --config value and --config=value work, and -- ends option parsing. Application variable names may not start with the reserved _afdata_, _AFDATA_, or AFDATA_ prefixes.

Every parser automatically supports:

Only long kebab-case application flags are accepted. Help exits with status 0; malformed arguments emit a cli_error event and exit with status 2. The selected AFDATA_OUTPUT and AFDATA_OUTPUT_TO values are exported so a child Bash script using this authoring kit keeps the same event format and routing.

Config and events

afdata_config_get FILE KEY [DEFAULT] delegates to afdata value, so the same format detection, dot-path grammar, empty-stdout-on-failure behavior, and secret gate apply:

host="$(afdata_config_get config.toml server.host localhost)"
token_secret="$(afdata_cli value config.toml service.token_secret --reveal-secret)"

The event helpers are thin wrappers around afdata emit:

afdata_log info "Starting build"       # log event, stderr under split routing
afdata_result "Build complete"         # result event, stdout
afdata_error build_failed "Build failed" "Inspect the compiler output"

afdata_error returns status 1. AFDATA deliberately does not scan free-form messages for secrets, so never interpolate a secret into a message. Keep sensitive values in _secret-named structured data or out of output entirely.

The equivalent CLI commands are available without sourcing the library:

afdata emit log info "Starting build"
afdata emit result "Build complete"
afdata emit error build_failed "Build failed" --hint "Inspect compiler output"

Child processes

afdata_run [--quiet] COMMAND [ARG ...] emits a start log, runs the command, then emits a completion log or a terminal child_process_failed error. It returns the child’s exact exit status even though the terminal error emitter itself uses status 1.

The child’s stdin, stdout, stderr, signals, colors, prompts, progress bars, and TTY access pass through unchanged. This is intentional: output from cargo, npm, wrangler, and similar programs is not relabeled as AFDATA log events. Only messages owned by the Bash script are structured. Command arguments are also omitted from wrapper logs because they may contain secrets.

afdata_run cargo test --all-features
afdata_run npm publish
afdata_run wrangler login   # remains interactive

# Token-efficient noninteractive mode: discard successful child output, but
# replay the combined output on stderr when the child fails.
afdata_run --quiet cargo test --all-features

Use afdata_run for a raw child that is required for the enclosing operation. A child failure is terminal, so run an expected or recoverable probe directly inside if and describe the recovery with afdata_log instead. Also invoke a command directly when its output is captured or piped as data: the enclosing redirection applies to the wrapper too, so lifecycle events could enter the captured stream.

When one AFDATA Bash script orchestrates another and the parent will emit the final result, use afdata_call:

afdata_call "$SCRIPT_DIR/build-assets.sh"
afdata_call "$SCRIPT_DIR/deploy.sh"
afdata_result "Release complete"

afdata_call [--] COMMAND [ARG ...] preserves the child’s live logs, errors, TTY, and exit status, but marks the child as participating in its parent’s finite event stream. A successful child afdata_result becomes an info log; the outermost script therefore remains the only owner of the terminal result. A child error remains terminal. This cooperation applies only to children that load this Bash kit. For another AFDATA CLI whose successful result is merely an internal check, capture or suppress that result explicitly; for a raw program, use afdata_run.

--quiet is an explicit child-output policy, independent of AFDATA log format and routing. It buffers combined stdout/stderr in memory, discards it after a successful command, and is intended only for noninteractive work. Buffering in memory rather than a temporary file means an interrupted or signalled script leaves nothing to clean up. Successful output is discarded to save tokens; failed output is replayed to stderr before the terminal structured error. Do not use it for prompts, progress that must remain visible, servers, or commands whose successful output is a result the caller needs.

Passthrough mode deliberately permits native child bytes alongside AFDATA events. Use --quiet, afdata_call, or explicit capture when a consumer needs a protocol-only stream with exactly one terminal event.