Skip to content

feat(alias): forward positional args to templates via {{ args }}#2280

Merged
max-sixty merged 2 commits intomainfrom
alias-args-forward
Apr 18, 2026
Merged

feat(alias): forward positional args to templates via {{ args }}#2280
max-sixty merged 2 commits intomainfrom
alias-args-forward

Conversation

@max-sixty
Copy link
Copy Markdown
Owner

@max-sixty max-sixty commented Apr 18, 2026

Summary

Non-flag tokens after an alias name are forwarded to the template as {{ args }} — a space-joined, shell-escaped sequence. wt s some-branch with s = "wt switch {{ args }}" expands to wt switch some-branch.

Before: wt s some-branch errored with Unexpected argument 'some-branch' for alias 's'.

Why

Users want to pass arguments through to aliased commands — wt s foowt switch foo — without configuring --var foo=… for every possible parameter. Positional args are the natural fit.

Behavior

{{ args }} is a minijinja sequence. All four access patterns work:

Template Render
{{ args }} space-joined, per-element shell-escaped
{{ args[0] }} first arg (escaped)
{{ args | length }} count
{% for a in args %} iteration

Each element is individually shell-escaped, so wt run 'a b' 'c;d' splices in as 'a b' 'c;d' and can't inject shell syntax. Positionals interleave freely with flags — wt deploy foo --dry-run bar collects ["foo", "bar"] into args.

--dry-run, --yes, -y, --var KEY=VALUE, and --KEY=VALUE parsing are unchanged.

Navigating the diff

  • src/config/expansion.rs — new ShellArgs struct wraps Vec<String> and implements minijinja's Object trait with ObjectRepr::Seq. Its render() writes shell_escape::unix::escape of each element, space-joined. The shell-escape formatter in setup_template_env detects ShellArgs via downcast_object_ref and passes its Display through unmodified — so bare {{ args }} isn't double-escaped by the generic per-value escape. Iteration and indexing yield plain Value::from(String) that still flow through the generic formatter. New ALIAS_ARGS_KEY constant ("args") is the reserved context key carrying the JSON-encoded list.
  • src/commands/alias.rsAliasOptions gains a positional_args: Vec<String> field. AliasOptions::parse collects non-flag tokens into it instead of erroring. run_alias inserts the JSON-encoded list into context_map under ALIAS_ARGS_KEY right after build_hook_context. The sole special-case lives in expand_template, which rehydrates the key as a ShellArgs object; all other call sites (hooks, for-each, eval) are untouched and never see the key.
  • validate_template — injects an empty ShellArgs so templates referencing {{ args }} pass pre-flight validation. args is added to TEMPLATE_VARS.
  • docs/content/extending.md — new "Forwarding positional arguments" subsection under Aliases with a shell-safety guarantee and access-pattern examples.

Tests

3233 tests pass, lints clean. New:

  • src/commands/alias.rstest_parse snapshots extended with positional cases including interleaved flags and metacharacter-laden args. test_parse_errors no longer expects "Unexpected argument".
  • src/config/expansion.rstest_expand_template_args_sequence covers indexing, iteration, and length. test_expand_template_args_empty confirms empty renders to empty string. test_expand_template_args_shell_metachar_safety asserts the exact output for ['; rm -rf /', '$(whoami)', "a'b"]. test_validate_template_valid now covers {{ args }}, {{ args | length }}, and iteration.
  • tests/integration_tests/step_alias.rs — end-to-end snapshots for wt step <alias> positionals, wt <alias> some-branch --dry-run, empty positionals, and sequence-style access.

Do-nots

  • No changes to --dry-run, --yes, --var, or --KEY=VALUE parsing — the broader flag-handling question (whether alias-level flags should move pre-name) is being designed separately.
  • No KEY=value bare-token parsing for vars — deferred.
  • No change to wt step <alias> semantics beyond inheriting positional support via shared AliasOptions::parse.

This was written by Claude Code on behalf of Maximilian

🤖 Generated with Claude Code

max-sixty and others added 2 commits April 17, 2026 23:41
Non-flag tokens after the alias name are collected into a new
positional_args field on AliasOptions and exposed in templates as a
ShellArgs sequence. Bare {{ args }} renders as a space-joined,
shell-escaped string ready to splice into a command line; indexing,
iteration, and length behave like any minijinja sequence.

ShellArgs implements minijinja's Object trait with ObjectRepr::Seq; the
shell-escape formatter in setup_template_env detects it via
downcast_object_ref and passes its Display through unmodified so the
joined result isn't double-escaped. Args flow through the existing
HashMap<String, String> context as a JSON-encoded list (ALIAS_ARGS_KEY)
that expand_template rehydrates into a ShellArgs object.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the redundant shell-safety restatement (already in the section
intro), the flag-interleave example (obvious once positionals work),
and the niche stdin JSON note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@max-sixty max-sixty force-pushed the alias-args-forward branch from b92b12f to bb096cf Compare April 18, 2026 06:41
@max-sixty max-sixty merged commit 4ec7023 into main Apr 18, 2026
26 checks passed
@max-sixty max-sixty deleted the alias-args-forward branch April 18, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants