fix: request completions with "__complete" so a double dash can't run the command - #2397
Draft
suzuki-shunsuke wants to merge 38 commits into
Draft
fix: request completions with "__complete" so a double dash can't run the command#2397suzuki-shunsuke wants to merge 38 commits into
suzuki-shunsuke wants to merge 38 commits into
Conversation
A completion request appended to the end of the command line cannot be told apart from a positional argument that looks like one, and after "--" that is exactly what it is. The two readings are both required: $ aqua exec -- foo --generate-shell-completion # pass it on to foo (#1932) $ aqua exec -- foo <TAB> # complete, run nothing (#1993) The shell sends the same argv either way, so no reading of it can serve both, and #1932 was fixed at the cost of #1993: a command line holding "--" runs instead of completing. Pressing tab on 'app exec -- git push origin main' runs 'git push origin' today. The request is now named in the first argument, where "--" cannot reach it: <cmd> __complete <word>... <word being completed> Everything the shell has typed follows, and the word under the cursor comes last, empty or not. That word was guesswork until now, since the scripts sent it only when it started with "-", which is why "cmd --<TAB>" and "cmd -- <TAB>" arrived as the same request. The word being completed after a "--" gets no suggestion, because it is a positional argument of whatever the command runs, but the request is still a completion, so nothing is run. The deprecated form keeps working for scripts generated before this change, with the ambiguity it cannot escape: a command line holding "--" is answered as an ordinary run. It no longer swallows the flag while doing so, which is what #1932 asked for and what #2316 regressed by stripping it. Regenerating the script and sourcing it again is what closes #1993 for good. The request form shadows nothing: an app with a command of that name is left to answer for itself. Fixes #1993
The word being completed and the "--" before it were recorded on the root command by the request that carried them, and by nothing else, so a Command answering a second request still held what the first one said. A shell runs one request per process and never sees it; a test, a REPL or an embedded use answers several through one Command and does: a request past a "--" made every later request answer as though it were past one too. The two fields become one, which every run replaces, so there is one place that has to be right rather than two that have to agree.
The rule that the words after a "--" belong to whatever the command runs was kept by DefaultCompleteWithFlags alone, so a command carrying a ShellComplete of its own went on suggesting past the terminator. That left the rule as something every app had to reimplement, from a "--" it cannot see: the parsed arguments no longer say where the terminator was. Answering with nothing before the completion func runs applies it to every command instead. The action still does not run, which is what a shell asking for completions needs either way.
Bash splits the command line on COMP_WORDBREAKS, which holds "=" and ":", so COMP_WORDS turns "--opt=value" into three words. __init_completion is called with -n "=:" precisely to put those back together, and the candidates are filtered against the cur it returns, but the request was built from the raw COMP_WORDS: pressing tab on "app --opt=va" asked the app about "--opt", "=" and "va" as three arguments while filtering the answer against "--opt=va". zsh, which does not split there, sent one word for the same line. The words also arrive quoted, as typed: "app run \"hello world\" " asked the app about a word holding the two quote characters. eval used to take those off as a side effect of re-parsing the line, and dropping eval dropped that too. They are now taken off by walking the word, so a line holding $(...) is still not executed by pressing the tab key.
The words arrive as typed, so "app run \"hello world\" " asked the app about a word holding the two quote characters. (Q) takes one level of quoting off each of them, which is what the shell would do before handing a word to a command. The command's stderr went to the terminal, where it lands in the middle of the prompt: the other three scripts redirect it, and this one now does too.
The word under the cursor was found by comparing each element of the command line with $wordToComplete, which PowerShell hands over normalized: an unfinished "hello arrives as "hello", matches no element as written, and was therefore sent twice, once as a word of its own and once as the word being completed. The command was asked about a command line one argument longer than the one on screen, which is the opposite of what naming the word is for. The cursor says which element it is, and reading it also leaves out whatever follows: completing in the middle of a line asked about the words after the cursor too. The words come from the parsed element rather than from its source text, so one level of quoting comes off, the way the shell would take it off before handing a word to a command. Nothing is expanded while doing so: the value of a $(...) or a $env:VAR is no longer worked out to answer a completion, which Invoke-Expression used to do by re-parsing the whole line.
A word whose quote is still open, as in 'app run "hello wo', has no closing quote for (Q) to take off with it, so zsh sent the opening quote along with the word. bash and PowerShell send "hello wo" without it for the same line, and a command filtering candidates by what has been typed finds nothing for a word that begins with a quote character.
The script tests asserted on the text of the generated script, which says that a line was written, not what the shell does with it. Every difference found while reviewing this branch lived in that gap: a word bash splits on COMP_WORDBREAKS, a word arriving with its quotes, a word PowerShell hands over normalized and so sent twice. Each script is now sourced in its shell and asked to complete a command line, with the command it asks recording the arguments it receives. A shell that is not installed skips, so this costs nothing on a machine without it, and -short skips all of them. It found one more difference while being written: fish sent the word being completed with the quote the user had opened, where the other three take it off.
Tests in this package add flags of their own to the standard flag set, so testing.Short finds it unparsed and panics. It passed under -run because nothing had touched the flag set by then.
Setting EnableShellCompletion takes the first argument for a completion request, which an app taking free-form positional arguments no longer receives. Only an app declaring a command of that name was said to be affected, which is the guard, not the reservation. A script generated before this change also behaves differently on a command line holding a "--": the request stays on the line as a positional argument, so the run it starts usually ends in an Incorrect Usage message.
Nothing said that an app which has not enabled shell completion receives "__complete" as the positional argument it is, which is the other half of reserving it. Nothing reached past one level of subcommands either. The last case there is the one the arguments alone cannot answer: a flag being completed after a positional argument, where the deprecated request form looks one word too far back and suggests commands instead.
The driver built COMP_WORDS with eval, which is not what bash does with a command line and got all three of the cases that matter wrong. eval took the quotes off before the script saw them, so the case named "a quoted word" never reached the script's dequoting and would pass with it deleted. eval joined "--opt=va" into one word, so the reassembly the case is named after was never exercised. And eval ran the command substitution the case is there to prove is not run, creating the file the assertion looks for: on a machine with bash-completion, that case failed. The words are now written out as bash produces them, measured with a completion function that dumps COMP_WORDS, so what the driver feeds the script is what bash would.
Nothing installed bash-completion, zsh or fish, so the tests that run the generated scripts skipped, and a skip reads the same as a pass. The ubuntu job installs them and sets CLI_SHELL_TESTS_REQUIRED, which turns "this shell is not here" into a failure, so the coverage cannot go away without anyone noticing. Elsewhere the variable is unset and a missing shell still skips.
Every word but the first went through the dequoting, so a command whose path holds a space, and is therefore typed quoted, was run under a name with the quote characters in it: nothing ran and the completion returned empty. bash takes them off words[0] the same way.
The completion func stopped being run past the terminator, but the Before chain in front of it did not: a Before with a side effect fired on every tab key there, to produce an answer that is always empty. There is nothing to prepare for a completion that is not going to happen.
Not running the completion func past the terminator makes "no suggestion there" a guarantee of the library rather than what the default func happened to do, and it means a command wrapping another cannot hand its completions on. Neither was written down.
CLI_SHELL_TESTS_REQUIRED turned a missing shell into a failure, whatever it was, and the ubuntu job installs three of the four: pwsh comes with the runner image, so a change of image would have failed the job over a shell nobody had asked it to cover. It now names the shells it requires.
…e required names Two holes in the harness, both of which report something other than what is true. The bash driver looked for a bash-completion file and assumed that finding one meant it could be used. bash-completion 2.12 and later need bash 4.2, so on macOS, where /bin/bash is 3.2 and the file usually comes from Homebrew, sourcing it fails, the word-splitting helpers are never defined, and the script returns without asking anything. What the contributor saw was a syntax error from inside bash-completion and a failing assertion, for a change that has nothing to do with either. It now asks that bash whether the helpers end up defined, and skips saying so when they do not. CLI_SHELL_TESTS_REQUIRED matched names against the shells driven here and ignored anything else, so a typo in it required nothing at all and the CI guarantee it exists to provide would have gone away quietly. Names that match no driver now fail.
Skipping inside the loop ended the search at the first file that was there but unusable, so a machine holding both a bash-completion its bash cannot source and an older one it can would have skipped over the one that works. The unusable ones are collected instead, and named in the skip when none of them turned out to be usable.
eval expanded the tilde as a side effect of re-parsing the command line, and dropping eval dropped that too: the completion looked for a command whose name begins with a tilde, found none, and offered nothing. bash still calls the function, since it falls back to the compspec for the last path component. The tilde is put back on its own rather than by evaluating anything, so a "$(...)" on the line is still not run: it is the one expansion worth having, and the raw word decides, because a quoted "~" is not a home directory to the shell either.
The note said that pressing tab on a command line holding a "--" usually ends in an Incorrect Usage message. It does not: after a "--" the request is a positional argument, so it reaches the action along with the rest of the line and the run completes as though enter had been pressed. Saying that it usually errors reads as though the danger were mostly theoretical, when it is the one #1993 is about. The Customization heading no longer had any customization under it, since what it introduced is now the request form, which an app cannot rename: the script and the app have to agree on the name. Two comments were claiming more than they check: what "--" means when a flag takes it as its value, which is not knowable before the flags are parsed, and what the zsh driver fills words and CURRENT with, which is zsh's tokenizer standing in for a completion system that needs a terminal to drive.
The assertion that the first word of the request is the dequoted one went away with the tilde fix, and nothing else looked at it: every case typed the command as a bare "app", so a script sending the quotes along with the name would have passed. A case types it as 'app' instead, which all four shells answer. PowerShell needs the call operator to read a quoted word as a command at all, so that shell gets the same line written the way it would be typed there.
The exported GenerateShellCompletionFlag is referenced by nothing but a test: the request was found by a hardcoded name then, as it is now, so replacing it never changed what was detected. The old wording said it "may be redefined if desired", and the rewrite carried that premise over as a contrast. The note about what happens on an old script also said the run completes as though enter had been pressed, which holds unless the command declares how many arguments it takes, in which case the extra one is rejected.
Declaring how many arguments a command takes was named as the case where the flag left on the line by an old script is rejected. It is not one: an argument too many is not an error, so the run completes with the extra one in cmd.Args(). Reading that there is a shape of app the danger does not reach is worse than reading nothing, since the shape is the one many apps have.
The quoted command word was written into bashWords as though bash had been measured producing it. It cannot: a command word holding a quote matches no compspec, so bash completes it as a file name and never calls the function. Measured with a completion function that records being called, 'app' su and "app" su are not dispatched, while ~/bin/app su is, which is what the tilde case relies on. Leaving the field nil skips that shell, and says why in the case, so the comment promising measured values stays true of every value there.
Skipping bash when a case has no COMP_WORDS made "left out on purpose" and "forgot to measure them" the same thing. A case that forgot them used to fail loudly, on a bad array subscript; after that change it quietly covered one shell fewer, which is what CLI_SHELL_TESTS_REQUIRED exists to prevent a level up. The reason is a field now, so the skip has to be asked for, and a case with neither the words nor a reason fails as it did before.
The paragraph said that only the app's own action can turn the flag down. A typed argument list turns it down first: an IntArgs takes the flag as a number and reports that it is not one. Since neither the count nor the type nor the action can be relied on to stop it, the paragraph now says that much and no more.
It opened with "nothing rejects the request" and closed with a typed argument list rejecting it. The first sentence was about the flag alone until the qualifier came off while removing an exception that did not exist, and the exception that does exist came back to a sentence that no longer allowed for one. The flag is what the first sentence is about again, and what happens after it reaches the action is the app's to decide.
CLI_SHELL_TESTS_REQUIRED is read in this file and set in the workflow, neither of which is "one directory up" from here. What the comment is comparing is what each one covers: a shell against a case.
A function name may hold a "-" or a "."; a variable name may not. Putting the app name in the two new variables therefore broke every app named like docker-compose or golangci-lint, and broke it completely: sourcing stops at the syntax error, before any function is defined or any completion registered. main puts the name in function names only, so this was a regression. The variables go back to fixed names and are declared local by the entry point, which is what keeps two apps from sharing them — the reason the name was put there in the first place. The two arrays that were already global get the same treatment. A syntax check over the generated scripts covers the names that broke it: a test app has always been called "app" here, so nothing looked at a name a shell reads specially.
The linter wants the multi-line argument list wrapped.
The tilde was put back for bash, where dropping eval had taken it away, and left alone everywhere else. All three of the others dispatch the completion for such a line and then fail to run the command: measured for zsh and fish by driving them, and for PowerShell by the review, where Invoke-Expression used to expand it as a side effect of re-parsing the line. zsh and fish are checked by driving them with HOME pointed at the app. The PowerShell line is not: pwsh on this machine crashes before running anything at the moment, so that one shell is reasoned about rather than measured.
fish answers with nothing when string unescape cannot read a word, such as one ending in a lone backslash, so the request arrived with an empty word and the command answered as though a fresh one had been started. The word as typed is a better answer than no word; the other three pass a word they cannot take apart through unchanged. PowerShell drops an empty argument on the way to a native command unless the argument passing mode is the one 7.3 made the default, which turns the same line into the same silent misreading. The script asks for that mode where the variable exists. Windows PowerShell 5.1 has none, and is documented instead, together with the "--" a flag takes as its value, which no shell can answer.
The assertions still named the variables after the app, which is what broke for an app named with a "-", and one of them is now what says that the name stays out of them.
Marking an unexported constant deprecated tells no user and no linter anything. What is deprecated is the request form the scripts send, which is what the comment describes now.
The tilde was put back in all four scripts on the strength of a measurement that does not show what it looked like: "& '~/no-such'" reports the name it was given, tilde and all, because there is no such command, not because the tilde went unresolved. With a command that exists, PowerShell finds "~/bin/app" on its own. Measured by taking the block out again and completing "~/bin/app sub ": the command still runs. bash, zsh and fish all fail the same test without their own handling, which is why they keep it.
The tilde was put back for zsh and fish with nothing but a hand-run to say so, right after a blocker that came from a test never seeing an app name a shell reads specially. The bash-only test becomes a loop over the four, which the drivers already express: the command sits under a moved HOME and nowhere on PATH, so a shell that passes the word on as written finds nothing and the request never arrives. PowerShell is in it as the other kind of check, that its script needs to do nothing for the same line. A shell that cannot start with HOME moved skips, saying so: a machine can get its shells from a tool manager keeping what it needs under the real home.
The probe skipped through t.Skip, which walks past the one thing this file has for a silent skip: a shell named in CLI_SHELL_TESTS_REQUIRED is supposed to fail rather than disappear. Named or not, every shell went quiet and the run said ok. Measured by making the probe fail on purpose: without the variable all four skip, and with bash,zsh,fish required the run fails.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What type of PR is this?
What this PR does / why we need it:
A completion request appended to the end of the command line cannot be told apart from a positional argument that happens to look like one, and after
--that is exactly what it is. Both readings are required, and they contradict each other:The shell sends the same argv either way, so no reading of that argv can serve both. #1932 was fixed by choosing the first reading, which is why #1993 is still open: on a command line holding
--, pressing tab runs the command. With today'smain(v3.10.1-10-gc6f4cf7e), typingapp exec -- git push origin main<TAB>runsgit push origin.This PR moves the request to where
--cannot reach it — the first argument:completion.go: addscompletionCommandRequest(__complete) and markscompletionFlagdeprecated.help.go:checkShellCompleteFlagbecomesparseShellCompleteRequest. It understands both forms and records, on the root command, the word being completed and whether a--precedes it. Every run replaces that record, so aCommandanswering several requests carries nothing from one into the next. An app declaring a command named__completekeeps it, and stops being completable in exchange.help.go: nothing is suggested past a--, since those words are positional arguments of whatever the command runs. The rule lives inrunCompletionrather than inDefaultCompleteWithFlags, so a command carrying aShellCompleteof its own keeps it without having to know about a--it cannot see. Suggesting nothing is not the same as declining the request: the run is still a completion, so the action never executes. Where the request names the word being completed, that word is used instead of guessing it from the position of the arguments.help.go: the deprecated form no longer strips the flag off a command line holding--. Leaving it in place is what Shell completion works wrongly even if double dash--are included in arguments #1932 asked for, and fix: show flag completions when completing a double-dash prefix #2316 regressed it by returningarguments[:pos]while declining the request.command.go,command_run.go: the root command holds one*completionRequest, replaced by every run, and the call site. ABeforeis not run for a request past a--either: there is nothing to prepare for a completion that is not going to happen.autocomplete/*: all four scripts send the new request, including the word under the cursor, empty or not. That word was guesswork until now, because the scripts sent it only when it started with-, which is whycmd --<TAB>andcmd -- <TAB>arrived as the same request.autocomplete/bash_autocomplete: builds the request as an array and runs it directly instead ofevaling a joined string, so a word holding a space reaches the command as the single word it is, and a line holding a$(...)is no longer executed by pressing the tab key. The words come from thewords/cwordthat_init_completionreassembles rather than fromCOMP_WORDS, which splits--opt=valueinto three onCOMP_WORDBREAKSwhile the candidates are filtered against the whole of it. A command typed as~/bin/appis still run as the path it stands for:evalexpanded that as a side effect, and it is the one expansion worth keeping, so it is put back without evaluating anything.autocomplete/{bash,zsh,fish}: a command typed as~/bin/appis run as the path it stands for.evalexpanded it as a side effect of re-parsing the line; all three dispatch the completion for such a line and then fail to run the command without it. PowerShell resolves the tilde when it looks the command up, so its script does nothing there.autocomplete/*: one level of quoting comes off each word, the way the shell takes it off before handing a word to a command.evalandInvoke-Expressionused to do that as a side effect of re-parsing the line; dropping them dropped it, andapp sub "hello world"asked the app about a word holding the two quote characters. A word whose quote is still open is sent without the opening quote, which all four shells now agree on.autocomplete/powershell_autocomplete.ps1: uses the real-Nativecompleter signature. The old parameter list was shifted by one ($commandNamewas the word being completed and$wordToCompletewas theCommandAst), which worked only because interpolating the AST yields the command line. The words now come fromCommandAst.CommandElements, and which of them is being completed comes from the cursor rather than from a comparison with$wordToComplete, which PowerShell hands over normalized: an unfinished"helloarrives as"hello", matches no element as written, and would be sent twice. Reading the cursor also leaves out what follows it, so completing in the middle of a line no longer asks about the words after the cursor.Scripts generated before this change keep working, with the ambiguity they cannot escape: a command line holding
--is answered as an ordinary run. Regenerating the script and sourcing it again is what closes #1993 for a given app.Answering a completion no longer evaluates any part of the command line:
evalandInvoke-Expressionre-parsed it, so a$(...)typed on the line ran on the tab key. That is the same shape of problem as #1993, and it goes away with them.No public API changes;
go run scripts/build.go v3diffis clean.Which issue(s) this PR fixes:
Fixes #1993
Special notes for your reviewer:
COMP_WORDSvalues, what each shell hands its completion function and what the command ends up receiving were all measured, and the review the branch went through is what turned up the differences the tests now cover.--, which means it still runs the command there. That is not an oversight: it is the Shell completion works wrongly even if double dash--are included in arguments #1932 reading, and it is the only reading available without a signal the shell alone can send. The one change is that the flag is no longer swallowed, which also makes an accidental run more likely to fail loudly than to quietly do the real thing.ShellCompletefunctions see an unchangedcmd.Args(). The word being completed is kept in the arguments when it starts with-and left out otherwise, which is the shape the old scripts produced. Functions that branch on whether the last argument starts with-therefore behave identically under both forms. The word itself is recorded separately, unexported for now; exposing it would let aShellCompletefunction tell-c <TAB>from-c<TAB>, which seems worth a follow-up rather than this PR.--handling and has been conflicting since 2025-11; I am happy to close this in favor of a rebase of that one if you prefer, though it does not address the request being imitable in the first place.--XXX<TAB>returns no completions #2248 (--pa<TAB>returns nothing), and theargs[argsLen-2]indexing inDefaultCompleteWithFlags, which is only correct for the root command and so misses flag suggestions after a positional argument on a subcommand. A request naming the word being completed does not go through that indexing at all, so both are now questions about the deprecated form alone.--for any command now, where before it was what the default completion func happened to do. That makes it a guarantee of the library rather than a default, and it closes the door on a wrapper handing its completions to the command it runs (myapp exec -- git pu<TAB>offering whatgitwould offer). No released version has done that — before fix: show flag completions when completing a double-dash prefix #2316 the wrapper answered with its own completions, and since then it has run with the flag stripped off — but the deprecated form does now that it passes the flag on, so a script regenerated for this PR loses something a script for this PR would have had. That is deliberate: delegation only works by running the wrapper, which is the tab key running a command, which is what Autocomplete after double dash (--) executing command action. #1993 is about. The docs now say so.CLI_SHELL_TESTS_REQUIREDis set only on ubuntu.$PSNativeCommandArgumentPassingisStandard, which the script now asks for; 5.1 has no such setting and is documented instead.curthat bash filters the candidates against still holds the quoting, so a quoted word is asked about correctly and then filtered against"hello wo, which matches nothing. Taking the quotes offcurmeans quoting the candidates back before they are inserted, which is a change of its own.Testing
go test ./...,golangci-lint run,go run scripts/build.go v3diffandgfmrunall pass.completion_shell_test.goruns the generated scripts in the shells they are written for and checks the request each one builds, with the command it asks recording the arguments it receives. Asserting on the text of a script only says that a line was written, not what the shell does with it, and every difference found while reviewing this branch lived in that gap: a word bash splits onCOMP_WORDBREAKS, a word arriving with its quotes, a word PowerShell hands over normalized and so sent twice, a command substitution the old scripts executed.The words bash puts in
COMP_WORDSare written out per case as measured in bash 5.3 with a completion function that dumps them, rather than worked out in the test: a driver that splits the line itself checks its own idea of what bash does, and one that letsevaldo it takes the quotes off, joins--opt=vaback together and runs the command substitution, which is three of the cases gone.TestCompletionScriptsSyntaxrenders each script for an app namedmy-appandmy.appand asks the shell to parse it. A shell function name may hold a-; a variable name may not, so a script putting the app name in a variable breaks for those apps and breaks the whole file, since sourcing stops at the syntax error before anything is registered.The zsh run is the one assumption left: driving its completion system needs a pseudo terminal, so
wordsandCURRENTare filled with zsh's own tokenizer instead. bash gets the words as measured, and fish and PowerShell go through the entry point the shell itself uses.A shell that is not installed skips, and bash additionally skips without bash-completion, whose word splitting the script depends on. Because a skip is silent, the ubuntu CI job installs bash-completion, zsh and fish and sets
CLI_SHELL_TESTS_REQUIRED=bash,zsh,fish, which turns "not installed" into a failure for exactly those. pwsh comes with the runner image and is left optional, so a change of image cannot fail the job over a shell nobody asked it to cover.Other new Go tests:
Test_parseShellCompleteRequest: both request forms, including the word being completed, the terminator, an app owning a__completecommand, and the Shell completion works wrongly even if double dash--are included in arguments #1932 pass-through.TestCompletionRequestNeverRunsAction: an action that records having run, asserted not to, for a request with and without a--. This is the Autocomplete after double dash (--) executing command action. #1993 regression test.TestCompletionRequestAfterDoubleDash: no suggestion past a--, while a--being completed still gets flags.TestCompletionDeprecatedRequestPassedOnAfterDoubleDash: the wrapper receives the flag instead of the wrapper answering with its own completions.TestCompletionRequestKeepsArgsShape: aShellCompletefunction sees the samecmd.Args()under both forms.TestCompletionRequestStateIsPerRun: aCommandanswering several requests answers each on its own terms.TestCompletionCustomShellCompleteNotRunPastDoubleDash: a command carrying its own completion func suggests nothing past a--.TestCompletionRequestIgnoredWhenDisabled: an app that has not enabled shell completion receives__completeas the positional argument it is.TestCompletionRequestNestedSubcommand: a request is answered by the command it names, however deep, including a flag being completed after a positional argument.TestCompletionBeforeNotRunPastDoubleDash: aBeforeis not run for a request past a--, and still is for one the command answers.The scripts were also driven in the real shells (bash 5.3, zsh 5.9, fish 4.8.1, pwsh 7.6.4) against a test app whose
execaction writes a sentinel file. In every shell,app <TAB>,app g<TAB>,app -<TAB>andapp get --<TAB>produce the expected candidates, andapp exec -- echo hi<TAB>produces none and leaves the sentinel unwritten. With a script generated before this change, the same app still completes exactly as it does today.Finally, an app with a custom
ShellComplete(ghtkn) was built against this branch: its completions are unchanged, andghtkn exec -- git push origin main<TAB>no longer runsgit push origin.Release Notes