Skip to content

feat(shell): $VAR in mixed words and double quotes (segment model)#49

Merged
hartsock merged 1 commit into
mainfrom
issue-46/var-segments
Jun 25, 2026
Merged

feat(shell): $VAR in mixed words and double quotes (segment model)#49
hartsock merged 1 commit into
mainfrom
issue-46/var-segments

Conversation

@hartsock

Copy link
Copy Markdown
Member

Part of #46. Makes $VAR/${VAR} expansion work in mixed words ($HOME/config, pre$X.log) and inside double quotes ("$HOME/x"), not just whole-word.

What

  • parse.rs — a Seg { Lit | Var } type; Arg::Var now carries Vec<Seg>, so a word interleaves literal runs and variable references. read_var_name drops the whole-word restriction; double-quote $VAR expands. A word that is both a glob and variable-bearing ($DIR/*.rs) is refused (deferred).
  • shell_tool.rsinvoke allowlists every Seg::Var name before any spawn; expand_stage_argv concatenates segments (literals as-is, allowlisted vars from the env) into one literal.

Security (unchanged shape)

The secret-free allowlist is still a name-only check in invoke before any spawn — so even a mixed word like pre$AWS_SECRET_KEY is denied. Value substitution stays in the real spawner; single-literal, no re-split / no re-glob of the value (no re-injection). No new seam — same mockability (allowlist mocked in invoke; value read integration-tested).

Testing

  • Parser: mixed/quoted segments; ${X} braces; glob+var refused; invalid/bare-$/$( refused; escaped \$ literal.
  • Mocked: allowlisted var reaches the spawner as segments; non-allowlisted denied (incl. inside a mixed word); var-as-program denied.
  • Integration (real env): echo $HOME/sub and echo "prefix-$HOME" expand to the env value.

Remaining on #46 (smaller follow-ups, left open)

  • $VAR in redirect targets (> $TMPDIR/out) — needs the redirect path to become segments + an env seam to keep the resolved-path leash mockable.
  • glob and variable in one word ($DIR/*.rs).

Test plan

just check green (fmt + clippy all-features & no-default-features + workspace tests).

🤖 Generated with Claude Code

WHAT: $VAR/${VAR} expansion is no longer whole-word-only. parse.rs gains a Seg{Lit|Var} and Arg::Var now carries Vec<Seg>, so a word can interleave literals and variables ($HOME/config, pre$X.log) — and a $VAR inside double quotes now expands too. read_var_name drops the whole-word restriction; a word that is BOTH a glob and variable-bearing is refused (deferred).

WHY: paths like $HOME/config and quoted "$HOME/x" are the common real forms; whole-word-only was too thin. Security is unchanged: the secret-free allowlist is still a NAME-only check in invoke before any spawn (each Seg::Var name must be allowlisted — so even a mixed word like pre$SECRET is denied), and value substitution stays in the real spawner (no re-split/re-glob of the value → no re-injection). No new seam needed — same mockability as before (allowlist mocked in invoke; value read integration-tested).

TEST: parser (mixed/quoted segments; ${X} braces; glob+var refused; invalid/bare/$( refused; escaped \$ literal) + mocked (allowlisted var reaches spawner as segments; non-allowlisted denied incl. inside a mixed word; var-as-program denied) + real (echo $HOME/sub and "prefix-$HOME" expand to env values). just check green; all prior tests pass. Partial #46 — redirect-target $VAR and glob+var remain (smaller follow-ups).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01HMGPEApE4XfwgMhgFbRn6c
@hartsock hartsock added the risk:low Low-risk change label Jun 25, 2026
@hartsock hartsock merged commit 4f212d8 into main Jun 25, 2026
1 check passed
@hartsock hartsock deleted the issue-46/var-segments branch June 25, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:low Low-risk change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant