Files
max-sixty__worktrunk/templates/bash.sh
Worktrunk Bot 96c6c846f7 fix(shell): register completions under the --cmd name, not clap's (#3817)
## Problem

`wt config shell init <shell> --cmd <name>` renames the shell wrapper
and its lazy completion loader, but the registration that loader evals
comes from clap, which derives every identifier in it from its own
compile-time `Command` name (`wt`) — not from `argv[0]` and not from
`--cmd`. The two halves never agreed:

```console
$ wt config shell init zsh --cmd wot | grep _clap
        if ! (( $+functions[_clap_dynamic_completer_wot] )); then
        _clap_dynamic_completer_wot "$@"

$ COMPLETE=zsh wt | grep -oE '_clap_dynamic_completer_[a-z_]*' | sort -u
_clap_dynamic_completer_wt
```

Nothing completed, and because the guard never became true the
completion script was regenerated and re-evaluated on *every* TAB. Same
shape in bash (`_clap_complete_*`); PowerShell emitted
`Register-ArgumentCompleter -Native -CommandName wt`, so the `--cmd`
name was never registered at all. The documented `--cmd=git-wt` case
(the Windows Terminal conflict) was broken too — including for a binary
genuinely installed under that name, since clap's name comes from the
declaration rather than `argv[0]`.

There is a second, sharper edge: zsh's registration ends with `compdef
<completer> <cmd>`, so the first TAB on `wot` also bound worktrunk's
completer to plain `wt` — handing completions to the *other* `wt` that
`--cmd` exists to step around.

fish and nushell were unaffected. Both register a completer that shells
out to the binary rather than depending on a clap-emitted identifier, so
the reporter's "unverified" row for fish is a pass.

## Solution

The bash, zsh, and PowerShell loaders now pass the name they bind in
`WORKTRUNK_COMPLETE_NAME`, and `registration_name()` in
`src/completion.rs` emits the registration under that name (validated
through the same `validate_shell_command_name` guard `--cmd` uses, since
the value lands verbatim in generated shell code). The fallback is
`binary_name()`, which covers a binary installed as `git-wt` and invoked
directly. The templates apply clap's own `-` → `_` escaping to the
function they call, so `--cmd git-wt` guards on `_clap_complete_git_wt`
rather than the invalid `_clap_complete_git-wt`.

That fixes all four shells and the stray `compdef` in one place, rather
than pinning the templates to clap's internal naming:

```console
$ WORKTRUNK_COMPLETE_NAME=wot COMPLETE=zsh wt | grep -oE '_clap_dynamic_completer_[a-z_]*|compdef .*' | sort -u
_clap_dynamic_completer_wot
compdef _clap_dynamic_completer_wot wot
```

## Testing

Two reproduction tests in `tests/integration_tests/completion.rs`, both
failing before the change:

- `test_init_custom_cmd_defines_clap_completer_in_bash` drives the whole
chain through a real bash — generate the init script with `--cmd`, call
the loader it defines, then assert clap's completer function exists
afterwards. Printed `MISSING` before, `DEFINED` after. Cases for `wot`
and `git-wt`.
- `test_completion_registration_uses_shell_integration_cmd_name` covers
zsh and PowerShell, which CI can't drive: the identifier the init script
references must be the one the registration defines, and the `compdef` /
`-CommandName` target must be the `--cmd` name.

`cargo test --lib --bins` and `cargo test --test integration` are
otherwise green (one unrelated failure locally,
`test_copy_ignored_preserves_file_executable_permissions`, from this
sandbox's `umask 0002`), and `cargo clippy --all-targets --all-features`
/ `cargo fmt --check` are clean.

---
Closes #3816 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-13 16:44:02 -07:00

80 lines
3.3 KiB
Bash

# worktrunk shell integration for {{ shell_name }}
# Only initialize if {{ cmd }} is available (in PATH or via WORKTRUNK_BIN)
if command -v {{ cmd }} >/dev/null 2>&1 || [[ -n "${WORKTRUNK_BIN:-}" ]]; then
# Override {{ cmd }} command with split directive passing.
# Creates two temp files: one for cd (raw path) and one for exec (shell).
# WORKTRUNK_BIN can override the binary path (for testing dev builds).
{{ cmd }}() {
local use_source=false
local args=()
for arg in "$@"; do
if [[ "$arg" == "--source" ]]; then use_source=true; else args+=("$arg"); fi
done
# Completion mode: call binary directly, no directive files needed.
# This check MUST be here (not in the binary) because clap's completion
# handler runs before argument parsing.
if [[ -n "${COMPLETE:-}" ]]; then
command "${WORKTRUNK_BIN:-{{ cmd }}}" "${args[@]}"
return
fi
local cd_file exec_file exit_code=0
cd_file="$(mktemp)"
exec_file="$(mktemp)"
# --source: use cargo run (builds from source)
if [[ "$use_source" == true ]]; then
WORKTRUNK_DIRECTIVE_CD_FILE="$cd_file" WORKTRUNK_DIRECTIVE_EXEC_FILE="$exec_file" \
cargo run --bin {{ cmd }} --quiet -- "${args[@]}" || exit_code=$?
else
WORKTRUNK_DIRECTIVE_CD_FILE="$cd_file" WORKTRUNK_DIRECTIVE_EXEC_FILE="$exec_file" \
command "${WORKTRUNK_BIN:-{{ cmd }}}" "${args[@]}" || exit_code=$?
fi
# cd file holds a raw path (no shell escaping needed).
# `builtin cd` bypasses any user `cd` alias or function (e.g. zoxide's
# `alias cd=__zoxide_z`) that would otherwise be substituted into this
# function body when it's defined. See #2643.
if [[ -s "$cd_file" ]]; then
builtin cd -- "$(<"$cd_file")"
local cd_exit=$?
if [[ $exit_code -eq 0 ]]; then
exit_code=$cd_exit
fi
fi
# exec file holds arbitrary shell (e.g. from --execute)
if [[ -s "$exec_file" ]]; then
source "$exec_file"
local src_exit=$?
if [[ $exit_code -eq 0 ]]; then
exit_code=$src_exit
fi
fi
command rm -f "$cd_file" "$exec_file"
return "$exit_code"
}
# Lazy completions - generate on first TAB, then delegate to clap's completer
_{{ cmd }}_lazy_complete() {
# Generate completions function once (check if clap's function exists)
if ! declare -F _clap_complete_{{ cmd_ident }} >/dev/null; then
# Use `command` to bypass the shell function and call the binary directly.
# Without this, `{{ cmd }}` would call the shell function which evals
# the completion script internally but doesn't re-emit it.
# WORKTRUNK_COMPLETE_NAME emits the registration under the name bound
# below; clap would otherwise name everything after its own command
# name and the call below would hit an undefined function (#3816).
eval "$(WORKTRUNK_COMPLETE_NAME="{{ cmd }}" COMPLETE=bash command "${WORKTRUNK_BIN:-{{ cmd }}}" 2>/dev/null)" || return
fi
_clap_complete_{{ cmd_ident }} "$@"
}
complete -o nospace -o bashdefault -F _{{ cmd }}_lazy_complete {{ cmd }}
fi