Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add similar whitespace escape logic to bash v2 completions than in other completions #1743

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

kangasta
Copy link

This PR would escape bash v2 completions when there are only one completion left and use line-break to separate completions when passing them to compgen. This should fix issues described in #1740 and only affect completions with special characters, such as whitespace.

@CLAassistant
Copy link

CLAassistant commented Jun 23, 2022

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions bot added the size/S Denotes a PR that chanes 10-24 lines label Jun 23, 2022
@marckhouzam
Copy link
Collaborator

Thanks @kangasta for the contribution 🎉
I will need to take the time to give this proper attention but I will not have time for the next couple of weeks. I'll get back to this as soon as I can. Thanks for your patience.

@marckhouzam
Copy link
Collaborator

Ok, I've started looking into this and I started by running some tests and the PR seems to work as expected.

Next step was to add a test to the bash v2 testing of cobra-completion-testing: marckhouzam/cobra-completion-testing#22. So now we can see that if a single completion has a space, the space character is not escaped. Running the same test with the PR shows that the space character is escaped. I think I have to add a couple more tests to cover each possible scenario, so I will keep working on it.

One issue is that the changes cause a slow down in the completion logic. I'm a little surprised because the changes are isolated to when there is a single completion left, so I'll need to dig into it.

More to come...

Comment on lines 223 to 237
compgen_words=$(printf "%%s\n" "${completions[@]}")
IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "${compgen_words}" -- "$cur")
Copy link
Author

Choose a reason for hiding this comment

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

Could this cause the performance loss?

This also feels bit clumsy to me, but I was not able to get completions with whitespace to be separate indices of COMPREPLY without printf here.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's almost certainly here. One starting point for debugging would be to just with strategically placed times, e.g. time compgen_words=....

printf -v "arr[index]" ... in a loop would avoid a subshell, but the net result could be even slower as this doesn't create subshells in a loop.

Choose a reason for hiding this comment

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

I don't think we need printf at all. Wouldn't something like this work?

IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${completions[*]}" -- "$cur")

IFS=$'\n' compgen ... sets IFS for just the compgen call.

IFS=$'\n'; compgen ... sets IFS for both the compgen call and the "${completions[*]}" string

And since that change is in a subshell, it won't affect IFS for the rest of the script


You can see what this does with this simpler example:

$ arr=( 1 2 3 )
$ IFS=$'\n' echo "${arr[*]}"
$ IFS=$'\n'; echo "${arr[*]}"

Copy link
Collaborator

Choose a reason for hiding this comment

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

It was important to escape all completions, not only if there was only one and it seems compgen does not persist escaped sequences, so we would have to use printf "%q" for each completions. This makes this "short circuit" if statement just as slow as when we process descriptions.

Therefore, to avoid having to maintain two code paths, I removed the "short circuit" if block.

bash_completionsV2.go Outdated Show resolved Hide resolved
bash_completionsV2.go Outdated Show resolved Hide resolved
bash_completionsV2.go Outdated Show resolved Hide resolved
bash_completionsV2.go Outdated Show resolved Hide resolved
@kangasta kangasta force-pushed the fix/bash-v2-completion-with-whitespace branch from ac82162 to 5f27234 Compare August 25, 2022 14:23
@github-actions
Copy link

The Cobra project currently lacks enough contributors to adequately respond to all PRs. This bot triages issues and PRs according to the following rules:

  • After 60d of inactivity, lifecycle/stale is applied. - After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the PR is closed.
    You can:
  • Make a comment to remove the stale label and show your support. The 60 days reset. - If a PR has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interseted in reopening.

@kangasta kangasta force-pushed the fix/bash-v2-completion-with-whitespace branch from 5f27234 to 3f2dad7 Compare October 27, 2022 10:54
@github-actions github-actions bot removed the area/shell-completion All shell completions label Oct 27, 2022
@kangasta
Copy link
Author

Rebased this on top of main

@marckhouzam how big of a performance loss this caused? One option to get this forward could be to split the fix so that optimization for completions without \t, from where the performance loss likely comes, would be addresses in separate PR.

Or is there something I could help with on the testing side?

@marckhouzam
Copy link
Collaborator

I'll try to get back to this PR next week. Thanks for your patience.

@github-actions
Copy link

The Cobra project currently lacks enough contributors to adequately respond to all PRs. This bot triages issues and PRs according to the following rules:

  • After 60d of inactivity, lifecycle/stale is applied. - After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the PR is closed.
    You can:
  • Make a comment to remove the stale label and show your support. The 60 days reset. - If a PR has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interested in reopening.

@kangasta
Copy link
Author

Adding a comment to keep this from being closed.

@marckhouzam
Copy link
Collaborator

Wow, I completely lost track of this. Sorry @kangasta.
The slow down is about 3 to 4 times slower.

I just ran make bash in https://github.com/marckhouzam/cobra-completion-testing

@marckhouzam marckhouzam modified the milestones: 1.8.0, 1.9.0 Nov 15, 2023
@kangasta
Copy link
Author

kangasta commented Feb 6, 2024

Yeah, I have not much ideas on how to optimize this further. There were comments from @scop about unnecessary subshells, but those would require dropping support for older bash versions.

We have a hack in upctl to bypass this issue that replaces spaces with non-breaking spaces (UpCloudLtd/upcloud-cli#204), but it would be nice to get rid of that.

@@ -233,7 +233,14 @@ __%[1]s_handle_standard_completion_case() {

# Short circuit to optimize if we don't have descriptions
if [[ "${completions[*]}" != *$tab* ]]; then
IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur")
local compgen_words=$(printf "%%s\n" "${completions[@]}")
IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "${compgen_words}" -- "$cur")

Choose a reason for hiding this comment

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

If we want to support other bash special characters, we need more quoting:

local compgen_words="$(printf "%%q\n" "${completions[@]}")"
cur="$(printf "%%q" "${cur}")"
IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "${compgen_words}" -- "${cur}")

compgen's -W honors shell quoting, so a completion like foo\>bar would become foo>bar to compgen. We also have to quote ${cur} because compgen appears to honor shell quoting after checking to make sure each word begins with ${cur}


Similarly, we need quoting up above: https://github.com/spf13/cobra/blob/main/bash_completionsV2.go#L62-L84

$ args=( echo "foo\>bar" )
$ eval "${args[*]}"
foo>bar

eval also honors shell quoting, so if we want to pass the actual quoted value through to the __complete call, we need to quote the args in requestComp

$ args=( echo "foo\>bar" )
$ eval "$(printf "%q " "${args[@]}")"
foo\>bar

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with the first part of this comment, but in the end I have removed that if block altogether.

As for the second part of the comment @JeffFaer , I think you wrote it before we discussed that the user will be typing the escape characters as well. So I don't think it applies.

JeffFaer added a commit to JeffFaer/cobra that referenced this pull request Mar 21, 2024
…haracters.

Special characters include whitepace, so this is more general than
 spf13#1743.

I added some test cases to cobra-completion-testing. This PR makes them
pass. It also doesn't trip any of the performance regression tests.
@@ -216,7 +216,7 @@ __%[1]s_handle_completion_types() {
comp=${comp%%%%$tab*}
# Only consider the completions that match
if [[ $comp == "$cur"* ]]; then
COMPREPLY+=("$comp")
COMPREPLY+=( "$(printf %%q "$comp")" )

Choose a reason for hiding this comment

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

If I'm reading cobra-completion-testing right, this line is the performance regression. We're creating a subshell to quote each individual completion

We could instead batch all of the completions up into another array, and then quote them all at once kinda like how we're doing for the input to this loop in the first place

Copy link
Collaborator

Choose a reason for hiding this comment

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

For the moment, printf -v seems to work and is fast.

JeffFaer added a commit to JeffFaer/cobra that referenced this pull request Mar 21, 2024
…haracters.

Special characters include whitepace, so this is more general than
 spf13#1743.

I added some test cases to cobra-completion-testing. This PR makes them
pass. It also doesn't trip any of the performance regression tests. I'm
happy to submit those tests as a PR as well.
  - https://github.com/JeffFaer/cobra-completion-testing/tree/special_characters
  - JeffFaer/cobra-completion-testing@52254c1
JeffFaer added a commit to JeffFaer/cobra that referenced this pull request Mar 21, 2024
…haracters.

Special characters include whitepace, so this is more general than
 spf13#1743. This should also fix spf13#1740.

I added some test cases to cobra-completion-testing. This PR makes them
pass. It also doesn't trip any of the performance regression tests. I'm
happy to submit those tests as a PR as well.
  - https://github.com/JeffFaer/cobra-completion-testing/tree/special_characters
  - JeffFaer/cobra-completion-testing@52254c1
@marckhouzam marckhouzam removed this from the 1.8.1 milestone Nov 4, 2024
JeffFaer added a commit to JeffFaer/cobra that referenced this pull request Dec 5, 2024
…haracters.

Special characters include whitepace, so this is more general than
 spf13#1743. This should also fix spf13#1740.

I added some test cases to cobra-completion-testing. This PR makes them
pass. It also doesn't trip any of the performance regression tests. I'm
happy to submit those tests as a PR as well.
  - https://github.com/JeffFaer/cobra-completion-testing/tree/special_characters
  - JeffFaer/cobra-completion-testing@52254c1

Signed-off-by: Jeffrey Faer <jeffrey.faer@gmail.com>
kangasta and others added 3 commits January 17, 2025 23:22
This ensures completions with whitespace are completed as a single argument.

Signed-off-by: Toni Kangas <toni.kangas@upcloud.com>
This ensures that completions with whitespace are treated as single completion.

Signed-off-by: Toni Kangas <toni.kangas@upcloud.com>
Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com>
@marckhouzam marckhouzam force-pushed the fix/bash-v2-completion-with-whitespace branch from 3f2dad7 to 3fb6896 Compare January 18, 2025 04:22
Copy link
Collaborator

@marckhouzam marckhouzam left a comment

Choose a reason for hiding this comment

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

I worked very hard trying to make sure #2126 this was safe to merge, and in the end I felt there were too many changes I didn't understand well enough so I tried to simplify the change as much as I could. By the time I was done and had something working, it looked a lot like this PR here! So, I felt like the fair thing to do was to come back to this PR and fix it to make it work.

Working with the new tests of marckhouzam/cobra-completion-testing#25, I was able to get this PR working as I feel we needed.

And with the use of printf -v mentioned by @scop , the performance is much better. We just need to discuss if we need to protect ourselves against printf -v not working in all shell versions.

I have pushed a new commit on top of yours; and all together I feel we got a solution.

Comment on lines 223 to 237
compgen_words=$(printf "%%s\n" "${completions[@]}")
IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "${compgen_words}" -- "$cur")
Copy link
Collaborator

Choose a reason for hiding this comment

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

It was important to escape all completions, not only if there was only one and it seems compgen does not persist escaped sequences, so we would have to use printf "%q" for each completions. This makes this "short circuit" if statement just as slow as when we process descriptions.

Therefore, to avoid having to maintain two code paths, I removed the "short circuit" if block.

bash_completionsV2.go Outdated Show resolved Hide resolved
@@ -216,7 +216,7 @@ __%[1]s_handle_completion_types() {
comp=${comp%%%%$tab*}
# Only consider the completions that match
if [[ $comp == "$cur"* ]]; then
COMPREPLY+=("$comp")
COMPREPLY+=( "$(printf %%q "$comp")" )
Copy link
Collaborator

Choose a reason for hiding this comment

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

For the moment, printf -v seems to work and is fast.

if ((${#COMPREPLY[*]} == 1)); then
__%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
comp="${COMPREPLY[0]%%%%$tab*}"
__%[1]s_debug "Removed description from single completion, which is now: ${comp}"
COMPREPLY[0]=$comp
COMPREPLY[0]=$(printf %%q "${comp}")
Copy link
Collaborator

Choose a reason for hiding this comment

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

It turns out that doing the escaping only when there is a single completion is not sufficient.

# Say I have a completion that is "bash1 space"
# The below works
$ testprog prefix special-chars bash1[tab]
$ testprog prefix special-chars bash1\ space

# But this does not
$ testprog prefix special-chars bash1\ s[tab]

This is because further above we do [[ $comp == "$cur"* ]] || continue but since $cur == "bash1\ s (that is what the user has typed), it won't match $comp because we haven't escaped $comp.
The solution is that we need to escape every completion before we start filtering on $cur

@@ -233,7 +233,14 @@ __%[1]s_handle_standard_completion_case() {

# Short circuit to optimize if we don't have descriptions
if [[ "${completions[*]}" != *$tab* ]]; then
IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur")
local compgen_words=$(printf "%%s\n" "${completions[@]}")
IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "${compgen_words}" -- "$cur")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with the first part of this comment, but in the end I have removed that if block altogether.

As for the second part of the comment @JeffFaer , I think you wrote it before we discussed that the user will be typing the escape characters as well. So I don't think it applies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size/S Denotes a PR that chanes 10-24 lines
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants