Repository navigation
perf: faster bash completion with many matches - #2514
Conversation
Juneezee
left a comment
There was a problem hiding this comment.
LGTM, thanks!
I reproduced the slowdown with 1278 candidates, the count in the PR description. Listing them (the second TAB, COMP_TYPE=63) took 12.43s on main and 0.12s with this PR. The first TAB was already fast on both, so only the listing was slow.
To reproduce, save this as repro.sh and run bash repro.sh from the root of a urfave/cli checkout, once on main and once on this PR (gh pr checkout 2514):
# A fake app that prints 1278 completion candidates with descriptions.
app() { for ((i = 1; i <= 1278; i++)); do echo "cmd$i:description $i"; done; }
# Stub for bash-completion's _comp_initialize, so this runs as a plain script.
_comp_initialize() { cur=${COMP_WORDS[COMP_CWORD]}; words=("${COMP_WORDS[@]}"); cword=$COMP_CWORD; }
# Render the template the way `app completion bash` does.
eval "$(sed 's/%\[1\]s/app/g; s/%%/%/g' autocomplete/bash_autocomplete)"
# Complete `app <TAB>`: COMP_TYPE 9 is the first TAB, 63 the second (the listing).
COMP_WORDS=(app '') COMP_CWORD=1
for COMP_TYPE in 9 63; do
echo "COMP_TYPE=$COMP_TYPE"
time __app_bash_autocomplete
echo "${#COMPREPLY[@]} replies, first: ${COMPREPLY[0]}"
doneThe real times on my machine (bash 5.3):
COMP_TYPE=9 COMP_TYPE=63
main 0.10s 12.43s
PR 0.09s 0.12s
For ordinary candidates the replies are the same as before: both TABs, with and without descriptions, with and without a typed prefix. A double TAB in an interactive bash 5.3 lists the same thing.
The single pass also brings three small improvements:
- Descriptions are padded to the longest match, not the longest candidate overall.
- A repeated token shows its own description, not the first one's.
- The token and description arrays are now local, not globals left in the user's shell.
cli/autocomplete/bash_autocomplete
Lines 44 to 61 in db1ab19
The existing implementation used a quadratic search for completions, which becomes noticeably slow when an app emits 1000+ completion candidates. Update to a single-pass approach. Regression tested on bash-3.2. On a real app that emits 1278 candidates, speed is improved from roughly 20 seconds to basically instantaneous. Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
The bash completion script used to pass the candidates to compgen -W,
which expands each word. A candidate such as $(touch${IFS}x) then ran
as a command when the user pressed TAB. The previous commit dropped
compgen to make completion faster, which fixed this too.
Run the generated script in bash with such a candidate, for the first
and the second TAB, and check that it comes back as is and runs
nothing.
Reference: 7544c56 ("Added some documentation and bash integration code")
Assisted-by: claude:claude-opus-5-5
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
db1ab19 to
d090115
Compare
The existing implementation used a quadratic search for completions, which becomes noticeably slow when an app emits 1000+ completion candidates. Update to a single-pass approach and remove an extra subshell per reply. Regression tested on bash-3.2.
On a real app that emits 1278 candidates, speed is improved from roughly 20 seconds to basically instantaneous.
What type of PR is this?
What this PR does / why we need it:
Bash completion enters the tens of seconds when an app emits 1000+ replies.
The single pass also stops the script from expanding candidates. It used to pass them to
compgen -W, which expands each word, so a candidate such as$(touch${IFS}x)ran as a command when the user pressed TAB. The second commit adds a test for this.Which issue(s) this PR fixes:
I didn't make one :(
Testing
Tested on bash 5 and bash 3.2
Release Notes