# bash-interop
Run a bash program, hear every shell in its process tree, and answer the
questions those shells ask — while the program behaves exactly as it does when
nothing is listening.
The usual ways of watching a shell script are indirect. `set -x` produces a
trace that no two people parse the same way, exported variables only travel
downward, and wrapping the interpreter tells you nothing about what happened
inside a function. This crate takes the other route. The script states what it
wants observed, and a Rust program on the other end of a pipe hears it and can
reply.
## What a script does
Once a script has joined a session, it can do two things.
`BC_SAY` ships a list of words and returns immediately, because nothing is
waiting on it:
```bash
declare -- BC_SAY__ARG_LABEL=BUILD
BC_SAY STARTED phase compile
```
`BC_ASK` ships a list of words and blocks until your Rust code replies:
```bash
declare -- BC_ASK__ARG_LABEL=BUILD
declare -a BC_ASK__ARGS=(cache-lookup "$sha")
BC_ASK
```
The words are yours in both cases. There is no schema and no reserved
vocabulary — the wire moves an argument list of any width, and the protocol
reads none of its positions.
A rig normally gives scripts a word of its own over these, so a call site is
one line and reads like the domain rather than the protocol:
```bash
alias STARTED='BC_SAY__ARG_LABEL=BUILD BC_SAY STARTED'
```
## What an answer is
Understand this part first. Everything your Rust code can do to a running
shell follows from it.
When your reaction answers an `ask`, it returns a list of words. Those words
travel back to the shell that asked, which parses them with bash's own array
syntax and then runs them as a command, in the frame that asked:
```bash
declare -ga __BC__ANSWER="$__bc_line" # the reply, read as a bash array literal
"${__BC__ANSWER[@]}" # and invoked, right where you asked
```
There is no `eval` anywhere in that. Bash parses an array literal — the same
notation `declare -p` prints — and calls the result.
Because it runs where the call was written, an answer's `declare` binds in the
function that asked and dies with it. Nothing has to reach for `-g` to be seen,
and a reply can shape the caller's own scope.
Handing back a command rather than a value is what makes the channel general.
One command, run in the frame that asked, already covers the things you would
otherwise design a protocol around:
| `Answer::of("echo", [path])` | prints it, so `x=$(BC_ASK)` captures a value |
| `Answer::of("declare", ["target=staging"])` | binds a variable in the frame that asked |
| `Answer::status(3)` | gives the ask status 3, so `if BC_ASK` branches on the reply |
| `Answer::returning(3)` | returns 3 from the function that asked, ending it |
| `Answer::of("source", [path])` | runs a file of any length that you just wrote |
| `Answer::of("exit", ["9"])` | ends the subject |
The `ask` exits with the status of whatever ran, so a reply that says no is an
ordinary shell failure the script can test.
The vocabulary is not limited to builtins either. A rig injects bash of its own
into every shell that joins, so you can define a helper there and answer by
calling it:
```rust
// the rig's bash, sourced by every shell in the tree
fn bash(&self, _at: &Layout) -> String {
"use_toolchain() { export CC=$1 CXX=$2; hash -r; }\n".to_string()
}
// ... and later, deciding what an ask gets back
Ok(Answer::of("use_toolchain", ["clang", "clang++"]))
```
Your Rust program decides; a function you wrote carries the decision out inside
the shell that asked.
## The Rust side
You implement two traits. `Rig` describes the arrangement — what bash to inject
and how to build a counterpart for each shell that turns up. `Reacting` is that
counterpart:
```rust
impl Reacting for Watching {
type Kept = Vec<Message>;
async fn hear(&mut self, said: Message) -> Result<(), Failure>;
async fn answer(&mut self, asked: Message) -> Result<Answer, Failure>;
async fn finish(self) -> Result<Self::Kept, Failure>;
}
```
Every bash process that joins gets its own pipe, its own task and its own
`Reacting` value, so a subshell and its parent never get confused for each
other, and a slow reply holds up only the shell waiting on it. The runtime is
single-threaded, which is why nothing here asks you for a `Send` bound.
Two things matter before you put a call site into code you ship.
A call site is a real dependency, and a loud one on purpose. Run the script
with no session anywhere and `BC_SAY` is simply a command that does not exist:
status 127, and nothing else happens. Load the prelude but join no session and
it reports `label … is not joined` at your call site and returns 125. Neither case fails quietly, because a script that asked to be observed
and silently was not is the worse outcome. If a script has to run both ways,
say so in one line at the top:
```bash
declare -F __bc_say >/dev/null || alias BC_SAY=:
```
Second, nothing is timed, counted or inferred on the bash side. A message
carries the sending shell's own clock and its own view of the call stack, so
what your program reports is what a shell said about itself at the moment it
spoke, rather than something reconstructed afterwards from ordering.
## Reading on
[The overview](https://bashmgmt.github.io/bash-interop/overview.html) walks the
whole model once. The module documentation for `rig` carries a worked example
that compiles, and [the book](https://bashmgmt.github.io/bash-interop/) is the
reference: the wire and its message forms, what a shell reports about itself,
the frame walk, and the measurements the transport rests on.
Built on [bash-strings](https://github.com/bashmgmt/bash-strings).
[bashcap](https://github.com/bashmgmt/bashcap) and
[bashprof](https://github.com/bashmgmt/bashprof) are small rigs over this
crate, and read as examples of one.
Licensed under the MIT licence.