scop / bash-completion

Programmable completion functions for bash
GNU General Public License v2.0
2.92k stars 380 forks source link

bash-completion pollutes $_ #901

Closed devkev closed 1 year ago

devkev commented 1 year ago

Describe the bug

As per the bash manpage, the $_ bash variable:

expands to the last argument to the previous simple command executed in the foreground, after expansion

This can be useful for referring to the final arg of the previous command, without having to re-type it, and without having to muck around editing history lines. See for example the repro below.

Unfortunately, invoking bash-completion (ie. by pressing Tab) causes simple commands to be run, which causes the value of $_ to be overwritten. This manifests as commands which fail - or more usually, succeed but with highly unexpected (and potentially dangerous) results - if tab completion has been used on the input line, but work just fine otherwise. See the repro below.

To reproduce

See https://asciinema.org/a/cHIfI9Fv6k4TLRt5JtywxShlN . This shows first the expected behaviour (with plain bash completion), and then the behaviour with bash-completion loaded.

Repro:

mkdir test
cd test
touch foobarbazfredbarney
mkdir -p some/place/far/far/away
cp -a foobarb<Tab> $_    # succeeds, but the file has not been copied into the directory, rather it has been copied to a file called _filedir
mkdir -p some/where/else/entirely
cp -a foobarbazfredbarney $_    # succeeds, and correctly copies the file into the directory - but only because tab completion has been avoided

Expected behavior

As above - the value of $_ should be unaffected by bash-completion invocations.

Versions (please complete the following information)

Additional context

I've encountered a similar problem before when using a DEBUG trap to show the current command in my terminal title bar. In that case the simple and easy solution was to deliberately pass an ignored final arg of "$_" to the function, ie.

function show_command_in_title_bar {
    # the arg passed to this is deliberately ignored; it's there purely to preserve "$_"

    # ... code to update terminal title bar ...
}

case "$-" in
    *i*)
        # interactive only
        trap 'show_command_in_title_bar "$_"' DEBUG
        ;;
esac

I expect something similar can be done in this case. Changing every _filedir call to _filedir "$_" (and similarly for every other function call / command) seems exceptionally painful. A more general solution might be to adjust every "entry point function" (eg. _longopt, _service, etc) so that the first thing is does is local last="$_", and the last thing it does is : "$last" (though early returns make this "last thing" a pain, and _longopt alone has 7 of them). However, I'm not sure if there's a strategy which can make this work easily for any/every possible entry function. It would be nice if complete -F could somehow pass extra args to the function (or even set vars), but this seems impossible. So the least bad possibility is probably doing something like:

diff --git a/bash_completion b/bash_completion
index 46f0cf1c..631e7240 100644
--- a/bash_completion
+++ b/bash_completion
@@ -2293,7 +2293,7 @@ _complete_as_root()
     [[ $EUID -eq 0 || ${root_command-} ]]
 }

-_longopt()
+__longopt()
 {
     local cur prev words cword split comp_args
     _comp_initialize -s -- "$@" || return
@@ -2342,6 +2342,16 @@ _longopt()
         _filedir
     fi
 }
+__entry_pt()
+{
+    local last="$_"
+    "$@"
+       : "$last"
+}
+_longopt()
+{
+    __entry_pt _"$@"
+}
 # makeinfo and texi2dvi are defined elsewhere.
 complete -F _longopt a2ps awk base64 bash bc bison cat chroot colordiff cp \
     csplit cut date df diff dir du enscript env expand fmt fold gperf \

And perhaps a wrapper function around the complete builtin could automatically define and inject such a shim whenever complete -F is used (ugh).

akinomyoga commented 1 year ago

This is not an issue specific to bash-completion. I think the general solution is just to save the value of $_ to another variable in PROMPT_COMMAND and restore it just before running the command. E.g.,

# bashrc

PROMPT_COMMAND=$'_devkev_save_lastarg=$_\n'$PROMPT_COMMAND
bind -x '"\xC0\1":: "$_devkev_save_lastarg"'
bind '"\xC0\2": accept-line'
bind '"\r": "\xC0\1\xC0\2"'

In this way, you do not have to care about anything about $_ in the programmable completions, bind -x functions, etc.

devkev commented 1 year ago

Thanks! That's an excellent workaround for this problem, that I would never have thought of (and only barely understand). Closing this out.