Skip to content

perf: faster bash completion with many matches - #2514

Merged
Juneezee merged 2 commits into
urfave:mainfrom
nickajacks1:slow-completion
Nov 7, 2026
Merged

Juneezee merged 2 commits into
urfave:mainfrom
nickajacks1:slow-completion

Conversation

@nickajacks1

@nickajacks1 nickajacks1 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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?

  • cleanup

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

Improve performance of bash completion. Bash completion no longer expands candidates, so a candidate such as `$(cmd)` no longer runs as a command.

@nickajacks1
nickajacks1 requested a review from a team as a code owner October 31, 2026 01:56

@Juneezee Juneezee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]}"
done

The 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.

local -a matches=() descriptions=()
local line token description
local longest=0
while IFS=$'\n' read -r line; do
token="${line%%%%:*}"
if [[ -z "${token}" || "${token}" != "${cur}"* ]]; then
continue
fi
description=""
if [[ "${line}" == *:* ]]; then
description="${line#*:}"
fi
matches+=("${token}")
descriptions+=("${description}")
(( ${#token} > longest )) && longest=${#token}
done <<< "${opts}"

Nicholas Jackson and others added 2 commits November 8, 2026 01:26
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>
@Juneezee
Juneezee merged commit 025d953 into urfave:main Nov 7, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants