mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
96c6c846f7
## 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>
80 lines
3.3 KiB
Bash
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
|