Files
bot-bottle/docs/research/bash-vs-python-vs-go.md
2026-05-28 17:56:14 -04:00

129 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation language: bash vs. Python vs. Go
Research into which runtime bot-bottle should be implemented in, given
where the project is today (~1250 lines, Python, mostly orchestration of
`docker` / `flyctl` / `ssh`). The project started in bash and was rewritten
to Python; this note evaluates whether either of the other two options
would be a better fit going forward.
## Summary
Stay on Python. Switch to Go if and when distribution friction becomes the
dominant pain — i.e., when bug reports about Python interpreter / venv
behavior start outweighing bug reports about bot-bottle itself. Bash is
not the right tool at the project's current size; reverting would be a
regression.
The single thing worth doing *now* regardless of language is keeping the
backend abstraction (local-docker / generic-remote / fly) clean enough that
a future Go rewrite would be mechanical. If the abstraction is right, the
language choice is reversible. If it isn't, the cost of switching balloons
because you're rewriting *and* redesigning at once.
## Axes that matter for this project
The relevant criteria, in roughly the order they bite:
1. **Distribution friction** — how easy is it to install the tool on a
new dev machine.
2. **Orchestration ergonomics** — 90% of the work is shelling out to
`docker`, `flyctl`, and `ssh`, so impedance match with subprocess
invocation matters.
3. **JSON manifest handling** — the manifest is structured config with
nested fields, validation rules, and per-agent overrides.
4. **Cross-platform behavior** — must work the same on macOS and Linux,
ideally without per-platform shims.
5. **Test-matrix burden** — local-mac × local-linux × generic-remote × fly
is already a wide test surface. The language choice should minimize
what it adds to that matrix, not expand it.
6. **Onboarding new contributors** — single maintainer today, but the
project is published and may attract drive-by PRs.
## Comparison
| | Bash | Python | Go |
|---|---|---|---|
| Distribution | Zero runtime — `chmod +x && run` | `uv run` is good; bare `python3` is "which one" | Single static binary, `go install` |
| Orchestration ergonomics | Native — pipes, `set -e`, no marshaling | Verbose (`subprocess.run(...)` × 50) | Verbose (`if err != nil { return err }` × 50) |
| JSON manifest | Painful past trivial — `jq` for nested writes is ugly | Excellent — `json` + dataclasses/pydantic | Excellent — struct tags, compile-time schema |
| Cross-platform | macOS bash 3.2 vs Linux 5.x + BSD/GNU userland = real hazard | Mostly a non-issue with `uv` | Best — same binary everywhere |
| Testability | Hard. No good unit story | Mature (pytest, subprocess mocking) | Mature (table-driven, `exec.Command` is mockable) |
| Startup time | ~5ms | ~100-200ms cold | ~10ms |
| Onboarding new contributors | High barrier past ~500 lines | Largest pool | Smaller but technical pool |
| Rewrite cost from current state | Already done once, regretted | Sunk cost — zero | ~1 focused week for ~1200 lines |
## Bash
Right tool *if the project stays under ~500 lines*. bot-bottle has
already crossed that threshold (~1250 lines), and the orchestration is no
longer "stitch CLIs together" — it has manifest validation, env-var
resolution, network and sidecar lifecycle, and SSH provisioning. Bash
scales badly for all four:
- **macOS bash 3.2 ceiling.** No associative arrays, no `wait -n`, no
`${var,,}`, no namerefs. Anything written for Linux bash 5.x has to be
back-ported by hand.
- **BSD vs GNU userland divergence.** `sed -i`, `date`, `readlink`, `mktemp`
flags all behave differently. Every script grows portability shims.
- **JSON via `jq` is workable but ugly.** Nested writes and per-agent
override merges become unreadable.
- **No real test story.** Black-box integration tests only.
- **Silent failure modes.** `set -euo pipefail` does not catch every case
(e.g. `null` propagating through a pipeline into a `docker run` flag),
and command substitutions can lose `set -e`. Subtle, hard to audit.
The original project was written in bash and rewritten to Python for
exactly these reasons. Reverting would not be a portability win; it would
be a portability loss.
## Python
Right tool *for where the project is now*. The sunk cost is real and
accurate: it's testable, the orchestration code reads cleanly, the
manifest layer benefits from structured types, and `uv` plus PEP 723
inline metadata makes the historical "which python3 with which deps"
complaint mostly historical.
Real costs that remain:
- **Startup latency** — ~100-200ms cold is noticeable but not bad for an
interactive tool that runs a single command and hands off to `docker
exec -it`.
- **Distribution to non-developer audiences** — has rough edges if the
user doesn't have `uv`. Acceptable for the current audience (developers
who already have a Python).
- **Interpreter version drift** — Ubuntu 22.04 ships 3.10, fresh distros
ship 3.13. Behavior deltas between minor versions exist but are rare in
the standard library surface this project uses.
## Go
Right tool *if and when distribution becomes the dominant pain*. Single
static binary works identically across macOS arm64, macOS amd64, Linux
amd64, Linux arm64 — which neutralizes most of the cross-platform leg of
the test matrix. Startup is fast enough that the tool feels native.
Costs:
- **Rewrite cost** — roughly one focused week for ~1200 lines of mostly
mechanical orchestration. Not interesting work.
- **Verbosity** — `if err != nil { return err }` is similar in volume to
Python's `subprocess.run(..., check=True)` plumbing. No win on terseness.
- **Smaller AI-tooling ecosystem** — most Claude Code-adjacent helpers
and skills are Python or JS. Drive-by contributors are a smaller pool.
Any future "import a third-party Python skill package" idea gets harder.
- **Iteration loop** — no "edit the script, rerun" — you build, then run.
Minor; not load-bearing for a single maintainer.
## Recommendation
Stay on Python. The signal to watch for, before reconsidering, is bug
reports about Python interpreter or venv behavior outnumbering bug reports
about bot-bottle's actual logic. Until that pattern shows up, the Go
rewrite isn't paying for itself.
Independent of language: invest in the backend abstraction now. A clean
`Backend` interface (with `run`, `exec`, `cp`, `build`, `network_*`)
makes the language choice reversible. A leaky abstraction makes it
expensive in any direction.