diff --git a/src/shell/snapshots/worktrunk__shell__tests__init_nu.snap b/src/shell/snapshots/worktrunk__shell__tests__init_nu.snap index 10ddca5fd..4bb5824be 100644 --- a/src/shell/snapshots/worktrunk__shell__tests__init_nu.snap +++ b/src/shell/snapshots/worktrunk__shell__tests__init_nu.snap @@ -81,6 +81,16 @@ export def --env --wrapped wt [...args: string@"nu-complete wt"] { ($external | get 0.path) } + # Strip surrounding quotes from `--flag="value"` / `-x='value'` tokens + # before forwarding. Nushell's `def --wrapped` re-tokenizes the + # space-separated form (`--flag "value"` → ["--flag", "value"]) but keeps + # the equals form as a single literal token (`--flag="value"` → + # ['--flag="value"']), so without this the binary receives the quote + # characters in argv and clap parses them as part of the value. See #2410. + let args = ($args | each {|a| + $a | str replace -r '^(-{1,2}[^=]+)=(["''])(.*)["'']$' '${1}=${3}' + }) + # `list` is the only command that benefits from streaming stdout (progressive # table rendering). It never emits directives, so we skip directive processing # and let the binary be the last expression — stdout flows through pipes. diff --git a/templates/nushell.nu b/templates/nushell.nu index 4b9cd6ea4..9eee414f4 100644 --- a/templates/nushell.nu +++ b/templates/nushell.nu @@ -77,6 +77,16 @@ export def --env --wrapped {{ cmd }} [...args: string@"nu-complete {{ cmd }}"] { ($external | get 0.path) } + # Strip surrounding quotes from `--flag="value"` / `-x='value'` tokens + # before forwarding. Nushell's `def --wrapped` re-tokenizes the + # space-separated form (`--flag "value"` → ["--flag", "value"]) but keeps + # the equals form as a single literal token (`--flag="value"` → + # ['--flag="value"']), so without this the binary receives the quote + # characters in argv and clap parses them as part of the value. See #2410. + let args = ($args | each {|a| + $a | str replace -r '^(-{1,2}[^=]+)=(["''])(.*)["'']$' '${1}=${3}' + }) + # `list` is the only command that benefits from streaming stdout (progressive # table rendering). It never emits directives, so we skip directive processing # and let the binary be the last expression — stdout flows through pipes. diff --git a/tests/integration_tests/shell_wrapper.rs b/tests/integration_tests/shell_wrapper.rs index c518d03ef..9663dda9d 100644 --- a/tests/integration_tests/shell_wrapper.rs +++ b/tests/integration_tests/shell_wrapper.rs @@ -833,6 +833,46 @@ mod unix_tests { }); } + /// Regression test for #2410. + /// + /// Nushell's `def --wrapped wt [...args]` re-tokenizes the space-separated + /// form (`--base "value"`) into two tokens but keeps the equals form + /// (`--base="value"`) as a single literal token, including the surrounding + /// quote characters. Without quote stripping in the wrapper, the worktrunk + /// binary sees `--base="releases/4.x.x"` in argv and clap parses the value + /// as the literal string `"releases/4.x.x"` (with quotes), so branch + /// resolution fails. + /// + /// nu-only — bash/zsh/fish strip quotes in their own tokenizer before the + /// wrapper function ever runs. + #[test] + fn test_wrapper_nu_strips_quoted_equals_flag() { + let repo = TestRepo::new(); + repo.commit("Initial commit"); + repo.create_branch("releases/4.x.x"); + + let output = exec_through_wrapper( + "nu", + &repo, + "switch", + &["--create", "feature", "--base=\"releases/4.x.x\""], + ); + + assert_eq!( + output.exit_code, 0, + "Quote-stripping should let `--base=\"...\"` succeed.\nOutput:\n{}", + output.combined + ); + output.assert_no_directive_leaks(); + // ANSI codes wrap branch names in bold, so check for the surrounding text. + assert!( + output.combined.contains("Created branch") + && output.combined.contains("releases/4.x.x"), + "Should show success message referencing both branches.\nOutput:\n{}", + output.combined + ); + } + #[rstest] #[case("bash")] #[case("zsh")]