Skip to content

Commit 1c95e05

Browse files
scophoshsadiq
authored andcommitted
bash completion improvements
style(bash-v2): out is not an array variable, do not refer to it as such Even though this to my surprise works, it doesn't accomplish anything but some confusion. Remove it. Merge spf13/cobra#1681 --- perf(bash-v2): use backslash escape string expansion for tab Using a command substitution, i.e. a subshell, with `printf` is expensive for this purpose. For example `__*_format_comp_descriptions` is run once for each completion candidate; the expense adds up and shows when there are a lot of them. This shaves off roughly 25% of the times I receive for my test case in #1680 (~2 seconds before, ~1.5 after). Merge spf13/cobra#1682 --- perf(bash-v2): standard completion optimizations Refactor to remove two loops over the entire list of candidates. Format descriptions only for completions that are actually going to be displayed, instead of for all candidates. Format descriptions inline in completions array, removing need for a command substitution/subshell and a printf escape per displayed completion. These changes shave off a second or so from my case at hand in #1680, it was roughly ~1.7s before, is now ~0.7s after. The diff is largish, but the bulk of it is in `__%[1]s_format_comp_descriptions` due to indentation changes, there aren't that many lines actually changed in it either. `git show -w` helps with the review. Caveat: I have not actually tested generating completions with this code. I worked on the improvements by tweaking an existing emitted completion shell file, then manually ported the changes to the template in Go code here. Hopefully I didn't introduce bugs while doing that. (The code compiles, and test suite fails just like it did before this change, no regressions noticed.) Merge spf13/cobra#1683 --- perf(bash-v2): short-circuit descriptionless candidate lists If the list of candidates has no descriptions, short circuit all the description processing logic, basically just do a `compgen -W` for the whole list and be done with it. We could conceivably do some optimizations like this and more when generating the completions with `--no-descriptions` in Go code, by omitting some parts we know won't be needed, or doing some things differently. But doing it this way in bash, the improvements are available also to completions generated with descriptions enabled when they are invoked for completion cases that produce no descriptions. The result after this for descriptionless entries seems fast enough so it seems there's no immediate need to look into doing that. This alone pretty much eliminates all the slowness related to my specific use case in #1680, bringing it down to ~0.07s (yes, there _is_ a zero on the right of the period), so for the time being I'm not inclined to look into further optimizations at least for the code path where there are no descriptions. Related to #1683, but I suggest merging both as they affect different scenarios. Same caveat as in #1683 about testing and the way the change was made applies here. Merge spf13/cobra#1686 --- perf(bash-v2): speed up filtering entries with descriptions Use simple prefix match instead of single word `compgen -W` command substitution for each candidate match. It's a bit late, but I can't think of a case where this replacement wouldn't be ok this late at night. Performancewise this improves the cases with descriptions quite a bit, with current marckhouzam/cobra-completion-testing#15 on my box, the v2 numbers look like the following (I've stripped the v1 numbers from each list below). Before (current master, #1686 not merged yet): ``` Processing 1000 completions took 0.492 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.600 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.455 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.578 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.424 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.570 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.502 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.585 seconds which is less than the 2.0 seconds limit ``` After (also without #1686): ``` Processing 1000 completions took 0.070 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.165 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.072 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.181 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.089 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.254 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.058 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.122 seconds which is less than the 2.0 seconds limit ``` For curiosity, even though this improves the descriptionless case quite a bit too, #1686 is still relevant, with it applied on top of this one: ``` Processing 1000 completions took 0.036 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.165 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.047 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.183 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.051 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.264 seconds which is less than the 2.0 seconds limit Processing 1000 completions took 0.036 seconds which is less than the 1.0 seconds limit Processing 1000 completions took 0.122 seconds which is less than the 2.0 seconds limit ``` Merge spf13/cobra#1689 --- fix(bash-v2): skip empty completions when filtering descriptions `read` gives a last null value following a trailing newline. Regression from fb8031162c2ffab270774f13c6904bb04cbba5a7. We could drop the `\n` from the feeding `printf` to accomplish the same end result, but I'm planning to submit some related cleanups a bit later that wouldn't play well with that approach, so I went with explicit filtering here. Merge spf13/cobra#1691 --- perf(bash-v2): speed up filtering menu-complete descriptions Similarly as fb8031162c2ffab270774f13c6904bb04cbba5a7 (+ the empty entry fix in spf13/cobra#1691) did for the "regular", non-menu completion cases. I have no numbers on this, but I'm assuming the speedups seen elsewhere are here too, and didn't notice anything breaking in casual manual tests. Could be useful to add some perf numbers for the menu-complete code path too to [marckhouzam/cobra-completion-testing](https://github.com/marckhouzam/cobra-completion-testing/) (maybe as well as other tests, haven't checked if there are any). Merge spf13/cobra#1692 --- perf(bash-v2): read directly to COMPREPLY on descriptionless short circuit Not that it'd really matter that much performancewise given the level we are at for this case, but this change makes the short circuit roughly twice as fast on my box as it was for the 1000 rounds done in marckhouzam/cobra-completion-testing. Perhaps more importantly, this makes the code arguably slightly cleaner. Before: ``` ... <= TIMING => no descriptions: 1000 completions took 0.039 seconds < 0.2 seconds limit ... <= TIMING => no descriptions: 1000 completions took 0.046 seconds < 0.2 seconds limit ... ``` After: ``` ... <= TIMING => no descriptions: 1000 completions took 0.022 seconds < 0.2 seconds limit ... <= TIMING => no descriptions: 1000 completions took 0.024 seconds < 0.2 seconds limit ... ``` Merge spf13/cobra#1700
1 parent 7f3b34a commit 1c95e05

File tree

1 file changed

+64
-69
lines changed

1 file changed

+64
-69
lines changed

resources/bash_completion.sh.gotmpl

Lines changed: 64 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ __{{ .CMDVarName }}_get_completion_results() {
5757
directive=0
5858
fi
5959
__{{ .CMDVarName }}_debug "The completion directive is: ${directive}"
60-
__{{ .CMDVarName }}_debug "The completions are: ${out[*]}"
60+
__{{ .CMDVarName }}_debug "The completions are: ${out}"
6161
}
6262

6363
__{{ .CMDVarName }}_process_completion_results() {
@@ -96,7 +96,7 @@ __{{ .CMDVarName }}_process_completion_results() {
9696

9797
# Do not use quotes around the $out variable or else newline
9898
# characters will be kept.
99-
for filter in ${out[*]}; do
99+
for filter in ${out}; do
100100
fullFilter+="$filter|"
101101
done
102102

@@ -108,7 +108,7 @@ __{{ .CMDVarName }}_process_completion_results() {
108108

109109
# Use printf to strip any trailing newline
110110
local subdir
111-
subdir=$(printf "%s" "${out[0]}")
111+
subdir=$(printf "%s" "${out}")
112112
if [ -n "$subdir" ]; then
113113
__{{ .CMDVarName }}_debug "Listing directories in $subdir"
114114
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
@@ -133,17 +133,16 @@ __{{ .CMDVarName }}_handle_completion_types() {
133133
# If the user requested inserting one completion at a time, or all
134134
# completions at once on the command-line we must remove the descriptions.
135135
# https://github.com/spf13/cobra/issues/1508
136-
local tab comp
137-
tab=$(printf '\t')
136+
local tab=$'\t' comp
138137
while IFS='' read -r comp; do
138+
[[ -z $comp ]] && continue
139139
# Strip any description
140140
comp=${comp%%$tab*}
141141
# Only consider the completions that match
142-
comp=$(compgen -W "$comp" -- "$cur")
143-
if [ -n "$comp" ]; then
142+
if [[ $comp == "$cur"* ]]; then
144143
COMPREPLY+=("$comp")
145144
fi
146-
done < <(printf "%s\n" "${out[@]}")
145+
done < <(printf "%s\n" "${out}")
147146
;;
148147

149148
*)
@@ -154,44 +153,37 @@ __{{ .CMDVarName }}_handle_completion_types() {
154153
}
155154

156155
__{{ .CMDVarName }}_handle_standard_completion_case() {
157-
local tab comp
158-
tab=$(printf '\t')
156+
local tab=$'\t' comp
157+
158+
# Short circuit to optimize if we don't have descriptions
159+
if [[ $out != *$tab* ]]; then
160+
IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "$out" -- "$cur")
161+
return 0
162+
fi
159163

160164
local longest=0
165+
local compline
161166
# Look for the longest completion so that we can format things nicely
162-
while IFS='' read -r comp; do
167+
while IFS='' read -r compline; do
168+
[[ -z $compline ]] && continue
163169
# Strip any description before checking the length
164-
comp=${comp%%$tab*}
170+
comp=${compline%%$tab*}
165171
# Only consider the completions that match
166-
comp=$(compgen -W "$comp" -- "$cur")
172+
[[ $comp == "$cur"* ]] || continue
173+
COMPREPLY+=("$compline")
167174
if ((${#comp}>longest)); then
168175
longest=${#comp}
169176
fi
170-
done < <(printf "%s\n" "${out[@]}")
171-
172-
local completions=()
173-
while IFS='' read -r comp; do
174-
if [ -z "$comp" ]; then
175-
continue
176-
fi
177-
178-
__{{ .CMDVarName }}_debug "Original comp: $comp"
179-
comp="$(__{{ .CMDVarName }}_format_comp_descriptions "$comp" "$longest")"
180-
__{{ .CMDVarName }}_debug "Final comp: $comp"
181-
completions+=("$comp")
182-
done < <(printf "%s\n" "${out[@]}")
183-
184-
while IFS='' read -r comp; do
185-
COMPREPLY+=("$comp")
186-
done < <(compgen -W "${completions[*]}" -- "$cur")
177+
done < <(printf "%s\n" "${out}")
187178

188179
# If there is a single completion left, remove the description text
189180
if [ ${#COMPREPLY[*]} -eq 1 ]; then
190181
__{{ .CMDVarName }}_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
191-
comp="${COMPREPLY[0]%% *}"
182+
comp="${COMPREPLY[0]%%$tab*}"
192183
__{{ .CMDVarName }}_debug "Removed description from single completion, which is now: ${comp}"
193-
COMPREPLY=()
194-
COMPREPLY+=("$comp")
184+
COMPREPLY[0]=$comp
185+
else # Format the descriptions
186+
__%[1]s_format_comp_descriptions $longest
195187
fi
196188
}
197189

@@ -210,45 +202,48 @@ __{{ .CMDVarName }}_handle_special_char()
210202

211203
__{{ .CMDVarName }}_format_comp_descriptions()
212204
{
213-
local tab
214-
tab=$(printf '\t')
215-
local comp="$1"
216-
local longest=$2
217-
218-
# Properly format the description string which follows a tab character if there is one
219-
if [[ "$comp" == *$tab* ]]; then
220-
desc=${comp#*$tab}
221-
comp=${comp%%$tab*}
222-
223-
# $COLUMNS stores the current shell width.
224-
# Remove an extra 4 because we add 2 spaces and 2 parentheses.
225-
maxdesclength=$(( COLUMNS - longest - 4 ))
226-
227-
# Make sure we can fit a description of at least 8 characters
228-
# if we are to align the descriptions.
229-
if [[ $maxdesclength -gt 8 ]]; then
230-
# Add the proper number of spaces to align the descriptions
231-
for ((i = ${#comp} ; i < longest ; i++)); do
232-
comp+=" "
233-
done
234-
else
235-
# Don't pad the descriptions so we can fit more text after the completion
236-
maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
237-
fi
205+
local tab=$'\t'
206+
local comp desc maxdesclength
207+
local longest=$1
208+
209+
local i ci
210+
for ci in ${!COMPREPLY[*]}; do
211+
comp=${COMPREPLY[ci]}
212+
# Properly format the description string which follows a tab character if there is one
213+
if [[ "$comp" == *$tab* ]]; then
214+
__%[1]s_debug "Original comp: $comp"
215+
desc=${comp#*$tab}
216+
comp=${comp%%%%$tab*}
217+
218+
# $COLUMNS stores the current shell width.
219+
# Remove an extra 4 because we add 2 spaces and 2 parentheses.
220+
maxdesclength=$(( COLUMNS - longest - 4 ))
221+
222+
# Make sure we can fit a description of at least 8 characters
223+
# if we are to align the descriptions.
224+
if [[ $maxdesclength -gt 8 ]]; then
225+
# Add the proper number of spaces to align the descriptions
226+
for ((i = ${#comp} ; i < longest ; i++)); do
227+
comp+=" "
228+
done
229+
else
230+
# Don't pad the descriptions so we can fit more text after the completion
231+
maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
232+
fi
238233

239-
# If there is enough space for any description text,
240-
# truncate the descriptions that are too long for the shell width
241-
if [ $maxdesclength -gt 0 ]; then
242-
if [ ${#desc} -gt $maxdesclength ]; then
243-
desc=${desc:0:$(( maxdesclength - 1 ))}
244-
desc+=""
234+
# If there is enough space for any description text,
235+
# truncate the descriptions that are too long for the shell width
236+
if [ $maxdesclength -gt 0 ]; then
237+
if [ ${#desc} -gt $maxdesclength ]; then
238+
desc=${desc:0:$(( maxdesclength - 1 ))}
239+
desc+=""
240+
fi
241+
comp+=" ($desc)"
245242
fi
246-
comp+=" ($desc)"
243+
COMPREPLY[ci]=$comp
244+
__%[1]s_debug "Final comp: $comp"
247245
fi
248-
fi
249-
250-
# Must use printf to escape all special characters
251-
printf "%q" "${comp}"
246+
done
252247
}
253248

254249
__start_{{ .CMDVarName }}()

0 commit comments

Comments
 (0)