Joining

Joining has two halves, kept apart.

Loading brings the definitions into a shell: source <dir>/prelude.bash for the protocol's words, then source <dir>/rig.bash for the words the rig adds. Both are inert. Initiation opens the channel: one line of client code, BC_JOIN <label> <dir> [word…], or the rig's init function wrapping it. A shell that loaded and never initiated has the words and is not part of the run; a shell that initiates without loading fails at an unknown command.

Both laid files define aliases as well as functions, so the source has to be a command of its own. Anything parsed in the same unit as it — a { …; } group holding both — will not see the words yet (scoping.md).

The exception is stated rather than implied. A run may provision <dir>/bash_env.bash, and whoever provisions it states whether that file initiates, with Provision::Joining putting the rig's joining line at the end, or only defines, with Provision::Definitions. BASH_ENV names that file, so it reaches every non-interactive bash in the subject's tree as it starts. This is how a subject that has never heard of the session comes to join, and it is the only place where initiation happens automatically.

Every way in reaches the same end state, the words defined and the channel open, from a different starting situation. Each is one whole script below, and since the body of work is the same in each, the prologues carry the difference.

bashprof stands in for any program built on the core. Its --reach bash-env|by-hand flags are that tool's spelling of the two provisioning choices, and each tool prints this same list in its own words under run --help and serve --help. The three bashprof-driven scripts also live in bashprof/__fixtures/book/, where its cli suite runs them as printed, and the two tool-free ones are the shapes tests/proofs/serving.rs proves.

Driven, provisioned to join

The subject knows nothing about the session.

#!/usr/bin/env bash
# Started as:  bashprof run --into build.times -- bash provisioned.bash
#
# The provisioned bash_env.bash defined the words and said the join in
# every shell of this tree as it started. Nothing of the protocol appears
# here — this is the way in for programs that never heard of the session.
set -euo pipefail

build() { sleep 0.1; }
BASHPROF_TIMETHIS build build

Driven, definitions only

Under a joining provision every shell of the tree joins, helpers included, so a dependency fetch or a ./configure fills the reading with shells nobody asked about. Here the tool provisions a Definitions file instead. Every shell still gets the words at startup, since BASH_ENV reaches them all, and no shell is joined until its own code says so. The script below joins itself and leaves its helper out.

#!/usr/bin/env bash
# Started as:  bashprof run --reach by-hand --into build.times -- bash by-hand.bash
set -euo pipefail
declare -- workspace="${BASHPROF_SESSION:?the workspace, from the tool}"

# fetch-deps.bash is an ordinary helper of this build — not part of the
# protocol. Like every shell in the tree it wakes up with the words
# defined; nobody initiates in it, so it stays outside the session: it
# runs exactly as it would unwrapped, and nothing it does is heard.
bash "${BASH_SOURCE[0]%/*}/fetch-deps.bash"

# From here on, THIS shell is part of the run.
BASHPROF_INIT "$workspace"

build() { sleep 0.1; }
BASHPROF_TIMETHIS build build

Staying outside the session holds for a shell that does not call the tool's words. If fetch-deps.bash said BASHPROF_TIMETHIS itself, that word would refuse with label BASHPROF is not joined, status 125, at its own call site and before running the wrapped command, so under set -e the helper would stop there. A call site asked for a measurement, and measuring into nowhere would be the worse answer. A helper that shares the tool's words joins too, or is left as it is.

A coprocess client

This script owns the session. It starts the server itself, on a workspace it names and makes, and holds the session open for as long as it runs. Everything here is bash's own — coproc is a keyword and the probe is one file test — and the only files sourced are the two the session laid.

#!/usr/bin/env bash
# Owns the session: names the workspace, starts the server, probes, loads,
# initiates — and leaves by closing the handle coproc left it.
set -euo pipefail

declare -- workspace="$PWD/prof.d"   # an address is absolute — initiation refuses else
mkdir -p "$workspace"

coproc SERVER { bashprof serve --at "$workspace" --into build.times; }
until [[ -p "$workspace/join" ]]; do sleep 0.01; done   # up exactly while serving

source "$workspace/prelude.bash"    # the protocol's words
source "$workspace/rig.bash"        # the rig's words
BASHPROF_INIT "$workspace"

build() { sleep 0.1; }
BASHPROF_TIMETHIS build build

declare -- handle="${SERVER[1]}"
exec {handle}>&-    # let go: what was held is the server's standard input
wait "$SERVER_PID"  # it sees the session out; this script exits with its status

The handle is the write end of the server's standard input, which coproc left in ${SERVER[1]}. Serving::serve_coprocess watches that descriptor, and the session lasts as long as somebody holds it, a subshell that inherited it included. Closing it is the act of leaving, and wait then collects a server that has seen the session out. The convention's fine print is in serving.md.

From the pieces

The workspace arrives as an argument.

#!/usr/bin/env bash
# Started as:  bash join-and-speak.bash <workspace>
#
# No environment and no words of our own: the two laid files are
# everything, and the coordinate arrives as argv. The same load as above, without a
# server to start — and the rig's init function is a raw BC_JOIN here.
set -euo pipefail
declare -- workspace="${1:?the session workspace}"

source "$workspace/prelude.bash"
source "$workspace/rig.bash"
BC_JOIN TELL "$workspace"

declare -- BC_SAY__ARG_LABEL=TELL
BC_SAY STEP joined-from-the-pieces

Publishing to child processes

The client authors its own startup file.

#!/usr/bin/env bash
# Already joined (any way above); wants the processes it starts joined
# too. No laid file initiates, so it writes its own startup file —
# %q is bash's own quoting — and points BASH_ENV at it: bash sources
# that file in every non-interactive child as it starts.
set -euo pipefail
declare -- workspace="${1:?the session workspace}"

declare -- own="${BASH_SOURCE[0]%/*}/own.bash"
printf 'source %q\nsource %q\nBC_JOIN TELL %q\n' \
    "$workspace/prelude.bash" "$workspace/rig.bash" "$workspace" > "$own"
export BASH_ENV="$own"

bash child.bash            # a fresh bash: sources $BASH_ENV, joins, speaks

Which shells the session reaches is always a decision with an author: the run, in its environment closure; the provisioning caller, in its stated Provision; the script, at its own init line. The core runs no initiation.

The proofs behind each way are tests/proofs/starting.rs for provisioned, both ways, and by-hand, and tests/proofs/serving.rs for the coprocess, from the pieces, the client-authored startup file, and an interactive shell typing the same.