How to configure bash autocomplete for file search and custom search? - bash

I want to have a command autocomplete uploaded scripts that would be run remotely, but the user could also pick a local script to upload. Here is a small example to illustrate the problem I have with the bash complete logic.
_test_complete()
{
local cur prev opts uploaded_scripts
uploaded_scripts='proc1.sh proc2.sh'
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ ${prev} == '-s' ]] ; then
COMPREPLY=( $(compgen -W "${uploaded_scripts}" -- ${cur}) )
return 0
fi
}
complete -F _test_complete remote
The example almost works but it does not autocomplete local file searches anymore.
$ remote -s proc<TAB><TAB>
proc1.sh proc2.sh
$ remote -s proc1.sh ./<TAB><TAB>
Nothing happens when you do the usual file search ./ which should list files in current dir. Any ideas on how you can enable both ?
EDIT: The above example had a problem you could only pick one file with file complete. I hacked a solution which works but if anyone has a better one please leave a comment. Also with the -o default from the accepted answer.
_test_complete()
{
local cur prev opts uploaded_scripts
uploaded_scripts='proc1.sh proc2.sh'
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
[[ $COMP_CWORD == '1' ]] && LAST_OPT=''
[[ ${prev:0:1} == '-' ]] && LAST_OPT=${prev}
if [[ ${LAST_OPT} == '-s' ]]; then
COMPREPLY=( $(compgen -o default -W "${uploaded_scripts}" -- ${cur}) )
return 0
fi
}
complete -F _test_complete remote

You can use complete's -o default option (Usually I'd use both -o default and -o bashdefault):
complete -o default -F _test_complete remote
According to man bash:
bashdefault
Perform the rest of the default bash completions if the compspec generates no matches.
default
Use readline's default filename completion if the compspec generates no matches.

You just have to add all files from the local directory to COMPREPLY too. complete -f -- abc generates a list of files starting with abc.
By the way: Instead of "${COMP_WORDS[COMP_CWORD]}" and COMP_CWORD-1 you can also use $2 and $3 which are supplied to any completion function by bash.
But here I completely dropped the if since it seems you want to allow multiple files after -s. Since you don't suggest -s itself, just suggest the files all the time:
_test_complete() {
local cur="$2" prev="$3" uploaded_scripts='proc1.sh proc2.sh'
COMPREPLY=( $(
compgen -W "${uploaded_scripts}" -- "$cur"
compgen -f -- "$cur"
) )
}
complete -F _test_complete remote
Note: COMPREPLY=( $(...) ) is easy to write but has some flaws. Files with spaces in them will be split into multiple suggestions and special symbols like * will expand and generate even more suggestions. To avoid this, either set IFS=$'\n'; set -o noglob or use mapfile -t COMPREPLY < <(...).
After you have done this, you can use complete -o filenames -F ... such that those problematic suggestions are correctly quoted when being inserted too.

Related

How to remove a single command from bash autocomplete

How do I remove a single "command" from Bash's auto complete command suggestions? I'm asking about the very first argument, the command, in auto complete, not asking "How to disable bash autocomplete for the arguments of a specific command"
For example, if I have the command ls and the system path also finds ls_not_the_one_I_want_ever, and I type ls and then press tab, I want a way to have removed ls_not_the_one_I_want_ever from every being a viable option.
I think this might be related to the compgen -c list, as this seems to be the list of commands available.
Background: WSL on Windows is putting all the .dll files on my path, in addition to the .exe files that should be there, and so I have many dlls I would like to remove in my bash environment, but I'm unsure how to proceed.
Bash 5.0's complete command added a new -I option for this.
According to man bash —
complete -pr [-DEI] [name ...]
[...] The -I option indicates that other supplied options and actions should apply to completion on the initial non-assignment word on the line, or after a command delimiter such as ; or |, which is usually command name completion. [...]
Example:
function _comp_commands()
{
local cur=$2
if [[ $cur == ls* ]]; then
COMPREPLY=( $(compgen -c "$cur" | grep -v ls_not_wanted) )
fi
}
complete -o bashdefault -I -F _comp_commands
Using #pynexj's answer, I came up with the following example that seems to work well enough:
if [ "${BASH_VERSINFO[0]}" -ge "5" ]; then
function _custom_initial_word_complete()
{
if [ "${2-}" != "" ]; then
if [ "${2::2}" == "ls" ]; then
COMPREPLY=($(compgen -c "${2}" | \grep -v ls_not_the_one_I_want_ever))
else
COMPREPLY=($(compgen -c "${2}"))
fi
fi
}
complete -I -F _custom_initial_word_complete
fi

How to use bash_autocomplete for the second argument?

Use case: I am trying to get source activate <env> to autocomplete the names of my conda environments (i.e. the list of directories in ~/anaconda3/envs/).
I've managed to get it to work if I didn't need the 'activate' in there using this code:
_source ()
{
local cur
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=($(ls ~/anaconda3/envs | xargs -I dirs bash -c "compgen -W dirs $cur"))
return 0
}
complete -F _source source
I've tried setting the last argument of complete to source\ activate and 'source activate' but that's not working (it just autocompletes with local files).
The issue seems to be that because source activate is not a function, it doesn't pick it up.
The simple solution is, of course, to make a single-word bash script which just contains source activate $1. But I'd rather do it properly!
My solution was adapted from this conda issue.
It checks that the first argument is activate and then does the compgen using whatever the second word you're typing is.
#!/bin/bash
_complete_source_activate_conda(){
if [ ${COMP_WORDS[COMP_CWORD-1]} != "activate" ]
then
return 0
fi
local cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=($(ls ~/anaconda3/envs | xargs -I dirs bash -c "compgen -W dirs $cur"))
return 0
}
complete -F _complete_source_activate_conda source
Put that script in /etc/bash_completion.d/. Unfortunately it kills the first-word autocompletion - with some more fiddling it would probably be able to handle both.
getting autocomplete to work for the second argument can be done with a case statement
like this
_source()
{
local cur prev opts
case $COMP_CWORD in
1) opts="activate";;
2) [ ${COMP_WORDS[COMP_CWORD-1]} = "activate" ] && opts=$(ls ~/anaconda3/envs | xargs -I dirs bash -c "compgen -W dirs $cur");;
*);;
esac
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
COMPREPLY=( $(compgen -W "$opts" -- ${cur}) )
return 0
}
complete -F _source source
compgen is build in function of bash which you then give the options you want to complete for with $opts for the first argument it will authocomplete "activate" but that can be adapted to be a more existive list and for the second argument it first checks if the first argument is "activate" before attempting to complete
disclaimer i adapted this from a completion function i wrote for ubports-qa, i haven't been able to test it but it should just work

Writing a bash completion script similar to scp

I have a script I thought I would allow completions via Bash completion.
The script takes a quoted string argument and then a file or server e.g.
my_cmd "something" stuff
I thought I could leverage the same completion as the scp command by:
have my_cmd &&
_my_cmd()
{
. /usr/share/bash-completion/completions/scp >/dev/null 2>&1 && complete -F _scp $1 && return 124
} && complete -F _my_cmd my_cmd
Which works, however this appends a colon to known servers. Not really knowing much about how the bash-completion works have you any better ideas to accomplish this?
If you look in the scp completion, it's using _known_hosts_real from /usr/share/bash-completion/bash-completion:
# Helper function for completing _known_hosts.
# This function performs host completion based on ssh's config and known_hosts
# files, as well as hostnames reported by avahi-browse if
# COMP_KNOWN_HOSTS_WITH_AVAHI is set to a non-empty value. Also hosts from
# HOSTFILE (compgen -A hostname) are added, unless
# COMP_KNOWN_HOSTS_WITH_HOSTFILE is set to an empty value.
# Usage: _known_hosts_real [OPTIONS] CWORD
# Options: -a Use aliases
# -c Use `:' suffix
# -F configfile Use `configfile' for configuration settings
# -p PREFIX Use PREFIX
# Return: Completions, starting with CWORD, are added to COMPREPLY[]
So, what you probably want is:
_my_cmd() {
local cur prev words cword
_init_completion || return
_known_hosts_real -a "$cur"
} && complete -F _my_cmd my_cmd
This works for me, at least. I admit, I don't generally write these, but it seems consistent with how other completions work. Here is another related example if you want to do some more shell foo with your completions:
https://unix.stackexchange.com/questions/136351/autocomplete-server-names-for-ssh-and-scp
Edited:
Here's for hosts and files, mostly inspired by what _scp_local_files does:
_my_cmd() {
local cur prev words cword
_init_completion || return
_known_hosts_real -a "$cur"
COMPREPLY+=( $( ls -aF1dL $cur* 2>/dev/null | \
sed -e 's/[*#|=]$//g' -e 's/[^\/]$/& /g' -e "s/^/$prefix/") )
} && complete -F _my_cmd my_cmd
Again, this is just taking the stuff you want from the _scp completion and making your own. This works for me, but the directory name completion is a little wonky.

How to enable default file completion in bash

Say I have a command named "foo" that takes one argument (either "encrypt" or "decrypt") and then a list of files. I want to write a bash completion script that helps with the first argument and then enables standard file completion for the others. The closest I've been able to come is this:
complete -F _foo foo
function _foo()
{
local cmds cur
if [ $COMP_CWORD -eq 1 ]
then
cur="${COMP_WORDS[1]}"
cmds=("encrypt decrypt")
COMPREPLY=($(compgen -W "${cmds}" -- ${cur}) )
else
COMPREPLY=($(compgen -f ${COMP_WORDS[${COMP_CWORD}]} ) )
fi
}
This does the first argument correctly and will chose a filename from the current directory for the subsequent ones. But say the current directory contains a subdirectory bar that contains a file baz.txt. After typing ba-TAB-TAB, completion results in "bar " (space after the "r") and is ready to choose the next argument. What I want is the standard bash completion, where the result is "bar/" (no space after the slash), ready to choose a file in the subdirectory. Is there any way to get that?
I know this is a little late, but I have found a solution here: https://stackoverflow.com/a/19062943/108105
Basically, you use complete -o bashdefault -o default, and when you want to revert to the default bash completion you set COMPREPLY=(). Here's an example:
complete -o bashdefault -o default -F _foo foo
_foo() {
local cur=${COMP_WORDS[COMP_CWORD]}
if (( $COMP_CWORD == 1 )); then
COMPREPLY=( $(compgen -W 'encrypt decrypt' -- "$cur") )
else
COMPREPLY=()
fi
}
The bash documentation can be a little enigmatic. The simplest solution is to alter the completion function binding:
complete -o filenames -F _foo foo
This means the function returns filenames (including directories), and special handling of the results is enabled.
(IMHO the documentation doesn't make it clear that this effectively post-processes COMPREPLY[] as set by your completion function; and that some of the -o options, that one included, when applied to compgen appear to have no effect.)
You can get closer to normal bash behaviour by using:
complete -o filenames -o bashdefault -F _foo foo
that gets you "~" completion back.
There are two problems with the above however:
if you have a directory named "encrypt" or "decrypt" then the expansion of your keywords will grow a trailing "/"
$VARIABLE expansion won't work, $ will become \$ to better match a filename with a $. Similarly #host expansion won't work.
The only way that I have found to deal with this is to process the compgen output, and not rely on the "filenames" post-processing:
_foo()
{
local cmds cur ff
if (($COMP_CWORD == 1))
then
cur="${COMP_WORDS[1]}"
cmds="encrypt decrypt"
COMPREPLY=($(compgen -W "$cmds" -- "$cur"))
COMPREPLY=( "${COMPREPLY[#]/%/ }" ) # add trailing space to each
else
# get all matching files and directories
COMPREPLY=($(compgen -f -- "${COMP_WORDS[$COMP_CWORD]}"))
for ((ff=0; ff<${#COMPREPLY[#]}; ff++)); do
[[ -d ${COMPREPLY[$ff]} ]] && COMPREPLY[$ff]+='/'
[[ -f ${COMPREPLY[$ff]} ]] && COMPREPLY[$ff]+=' '
done
fi
}
complete -o bashdefault -o default -o nospace -F _foo foo
(I also removed the superfluous array for cmd in the above, and made compgen more robust and handle spaces or leading dashes in filenames.)
The downsides now are that when you get the intermediate completion list (i.e. when you hit tab twice to show multiple matches) you won't see a trailing / on directories, and since nospace is enabled the ~ $ # expansions won't grow a space to cause them to be accepted.
In short, I do not believe you can trivially mix-and-match your own completion and the full bash completion behaviour.

bash autocompletion with file names

I can't get a simple bash autocompletion function to work. I need to autocomplete file names from a predefined directory so it will look like this:
$ cmd log<TAB><TAB>
file1.log file2.log file3.log
Where files are from /var/log/app.
I don't see the point of using ls when the shell can list files just fine by itself, so here's one using just the shell.
_cmd() {
local files=("/var/log/app/$2"*)
[[ -e ${files[0]} ]] && COMPREPLY=( "${files[#]##*/}" )
}
complete -F _cmd cmd
Put them into ~/.bashrc
_cmd() { COMPREPLY=($(ls /var/log/app)); }
complete -F _cmd cmd
To write a full-featured auto-complete function,
please take a look at /etc/bash_completion.d/python.
I found this to work as needed:
COMPREPLY=( $(compgen -W "$(ls /var/log/app/)" -- $cur) )
Thanks to dogbane in https://unix.stackexchange.com/questions/28283/autocomplete-of-filename-in-directory !

Resources