Line Transforms (design proposal)
Status: design proposal — not yet implemented. This document captures the agreed spec for a generic, reversible command-line rewrite feature that replaces the removed
__bu_bind_toggle_gdbtoggle.
1. Model
A transform is a rewrite rule match → replace over the current READLINE_LINE. Both directions are just transforms — “unwrap” is a transform whose match recognizes an already-wrapped line. Everything is line → line, so there is no toggle and no detection heuristic, and the sudo sudo apt update ambiguity does not arise: the user explicitly picks which transform to apply.
2. Placeholder vocabulary (v1)
| Placeholder (aliases) | Kind | Meaning |
|---|---|---|
{line} ({all}) | raw rest | the whole line, verbatim (spacing and quoting preserved) |
{prog} | token | the command word (first whitespace-delimited word) |
{args} ({@}, {...}) | token rest | all words after the first |
{line}is lossless;{prog}and{args}are lossy (tokenization collapses whitespace and drops quoting).- On the match side, placeholders capture. On the replace side, they emit the captured value — or, for
{prog}/{args}, a projection of a captured{line}(first word / rest words).
3. Rule shape
bu_preinit_register_line_transform <name> \
--match 'sudo {line}' \
--replace '{line}' \
--description 'Strip a leading sudo'
Both templates are literals plus placeholders. A match template is one of three forms:
- string mode:
P {line} S—{line}is the only placeholder; matches if the line starts withPand ends withS(raw prefix/suffix). - token mode: a whitespace-separated sequence of literal words,
{prog}, and a trailing{args}— matched against word-split tokens. - mixed:
token-seq + {line}(e.g.gdb {prog} --args {line}) — token match the front, then{line}captures the raw remainder after the last matched literal.
A replace template is a free mix of literals and the captured/projected placeholders.
4. Matching semantics
- Tokenization for
{prog}/{args}usesread -r -a-style splitting (IFS whitespace), matching the behavior of the old gdb toggle. Accepted for v1 — token-mode round-trips normalize whitespace and drop quoting. {line}always captures raw text, byte-for-byte.- In mixed mode,
{line}captures the verbatim remainder after the matched token sequence; exact whitespace boundary follows the same normalization and is an implementation detail.
5. Validity checker
Reject a registration if any rule fails:
- Whitelist — unknown
{...}tokens are a hard error (catches typos such as{lnie}). - Arity — at most one rest placeholder per side (
{line}/{all}or{args}/{@}/{...}, not both);{prog}at most once per side. - Position — a rest placeholder must be trailing on its side; nothing may follow it.
- Unquoted — no placeholder may sit inside single or double quotes in a template.
- Replace references — every replace-side placeholder must be available from the match side:
{line}requires{line}/{all}captured.{prog}requires{prog}captured, or{line}/{all}captured (→ first-word projection).{args}requires{args}captured, or{line}/{all}captured (→ rest-words projection).
Rules 2–3 make every match decidable; rule 5 makes every replace well-formed.
6. Auto-inverse
After validating a rule, swap match ↔ replace and re-run the checker. If the swapped pair is also valid, register the inverse automatically as its own transform:
wrap-<x>→unwrap-<x>; any other name →<name>-inverse.- Stamped
derived=truein the registry; an explicit registration with that name overrides the derived one.
Exactness: an inverse whose replace is {line} is lossless (round-trips byte-for-byte). An inverse that rebuilds from {prog}/{args} only is lossy (whitespace-normalized). The derived property records which.
7. Examples
| name | match | replace | auto-inverse | exact? |
|---|---|---|---|---|
wrap-sudo | {line} | sudo {line} | unwrap-sudo: sudo {line} → {line} | yes |
wrap-timeout | {line} | timeout 30s {line} | unwrap-timeout | yes |
wrap-gdb | {line} | gdb {prog} --args {line} | unwrap-gdb: gdb {prog} --args {line} → {line} | yes |
drop-log-level | LOG=INFO {args} | {args} | LOG=INFO {line} → … → {line} | lossy |
wrap-quoted | {line} | bash -c '{line}' | — (rejected: {line} inside quotes) | — |
wrap-gdb on apt update → gdb apt --args apt update; unwrap-gdb on that → apt update — the original gdb behavior, but registered, attributable, and listed.
8. Registry & provenance
Mirrors the key-binding registry:
declare -A -g BU_LINE_TRANSFORM_PROPERTIES=()
# [name,match] [name,replace] [name,description] [name,module] [name,derived]
bu_preinit_register_line_transformstamps[name,module]=$BU_CURRENT_MODULE(falling back tobufor core).bu get-transformlists transforms via the JSONL pipeline with aModulecolumn (same pattern asbu get-command).
9. Selector (opt-in)
- Chord:
bu_preinit_register_user_defined_key_binding '\et' '__bu_bind_transform_selector' "Pick a command-line transform"— not a new default binding. - Opens fzf over the registry. Each row shows
name + description; the preview pane shows the current line transformed (orno match). Rows whosematchdoes not hit are dimmed. - Enter applies the chosen transform to
READLINE_LINE/READLINE_POINT(single-apply, v1). - Reuses existing machinery (
__bu_fzf_compute_dimensions, the fzf completion impl’s preview pattern).
10. Scope
v1 (this spec): {line} / {prog} / {args}, the validity checker, auto-derived inverses, opt-in selector, single-apply.
Non-goals / v2 candidates: positional placeholders ({1}, {2}), multi-select composition in the selector, direct per-transform key bindings as a first-class registry concept (embedders can already register a key binding to __bu_bind_transform <name>), whitespace-preserving token mode.