Writing a bash completion script similar to scp - bash

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.

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 configure bash autocomplete for file search and custom search?

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.

Can rsync use shorthand/alias for an address?

I often rsync files to the same directory on the remote machine. Is it possible to give it an alias so that I don't have to type it in full every time?
Yes. Generally I will set up short scripts that define the rsync options and src and dest directories or perhaps arrays containing the directories of interest, for transferring files between hosts I admin. I usually simply pass the file to transfer src and dest files as arguments to an alias for that host (so I avoid having to type the hostname, etc..). Just make sure the script is executable, and define the alias in your .bashrc. It works fine. (I generally use rs_host for sending to a remote host, and aliases of rsf_host (the f indicating 'from').
For example, say I have a host valkyrie.3111skyline.com that is reachable only within my LAN. I so a lot of mirroring, and storage of software there for updating the kids computers as well as backups of my stuff. While I have backup scripts for bulk directories, sometimes I just was to shoot a file to the server or get a file from it. I don't like to type, so I make that process as short as possible. To do that I set of aliases (as described above) with rsvl to rsync to valkyrie and rsfvl to rsync from valkyrie. If I have updated my .bashrc and I want to send it to valkyrie, I only want to type:
rsvl ~/.bashrc
Similarly, if I want to get a file from valkyrie (say some C file in ~/dev/src-c/tmp/getline_ex.c and transfer it to my current directory, I only want to type:
rsfvl ~/dev/src-c/tmp/getline_ex.c
I want the scripts to take it from there. Since I know there can be corner cases and I often want to confirm a transfer involving a large number of files, I have the scripts accept the -n (--dry-run) option so that the scripts will show what will be done before they actually do it. (handy when your not quite sure)
I keep my rsync scripts in ~/scr/net (not too creative, I know...). In my .bashrc, the aliases are simply:
alias rsvl='/home/david/scr/net/rsyncvl.sh'
alias rsfvl='/home/david/scr/net/rsyncfvl.sh'
The scripts themselves are just quick and dirty helper scripts to get rsync to do as I want, e.g. the rsyncvl.sh scritpt (to valkyrie) is:
#!/bin/bash --norc
desthost=valkyrie.3111skyline.com
excl='--exclude=.~* --exclude=*.class'
usage() { ## simple usage function to display usage
cat >&2 << USG
usage: ${0##*/} [[-n|--dry-run|'options'] src [dest (~/Documents)]
for
rsync [-uai[n]]'options' \$src ${desthost}:\$dest [${desthost}:~/Documents]
USG
exit 0
}
## test for '-h' or '--help'
[ -z "$1" ] || [[ "${1:0:3}" =~ '-h' ]] && usage
## test for additional options passed to rsync
if test "${1:0:1}" == '-'; then
# options present so src is $2, dest is $3
src="$2"
destfn=${3:-~/Documents}
dest="${desthost}:${destfn}"
# preserve '-n' capability for '-uain' test
if test "$1" == '-n' || test "$1" == '--dry-run'; then
echo "[DRY RUN]"
opts='-uain'
else
# use options supplied for rsync
opts="$1"
fi
else
# default use
src="$1"
destfn=${2:-~/Documents}
dest="${desthost}:${destfn}"
opts='-uai'
fi
## output the text of the comman executed
echo "rsync $opts $excl ${src} ${dest}"
if [[ "$src" =~ '*' ]]; then
rsync $opts $excl ${src} "${dest}" # allow file globbing expansion
else
rsync $opts $excl "${src}" "${dest}"
fi
And the script to get files from valkyrie:
#!/bin/bash --norc
src="$1" ## source file on srchost
dest="${2:-.}" ## destination on local machine
srchost=valkyrie.3111skyline.com
usage() { ## simple usage function to display usage
cat >&2 << USG
Usage: ${0##*/} src dest ($HOSTNAME) --> rsync -uav [${srchost}:]\$src \$dest
USG
exit 0
}
[[ -z "$1" ]] && usage
## allow '.' shorthand to set pwd as source/dest dir
[[ $src == '.' ]] && src="$(pwd)" && {
dest="$(pwd)"
dest="${dest%/*}"
}
[[ $src == './' ]] && src="$(pwd)/"
[[ $src =~ '/' ]] || src="$(pwd)/$src"
srcstr="${srchost}:${src}" ## form source string
## echo command executed and execute
echo -e "\n rsync -uav ${srcstr} ${dest}\n"
rsync -uai "${srcstr}" "${dest}"
They can always be improved upon, and I wasn't all that particular when I wrote the first one a decade or so ago (which somehow has been duplicated for all the other hosts I have now...) So feel free to improve and tailor them to your needs. For basic use, all you need to do is change the hostname to the hostname of your remote machine and they will work fine. Both will display a short Usage if run without arguments and the to valkyrie script will respond to -h or --help as well. Good luck with your transfers.
You can add alias like that alias myrsync='rsync -av -e "ssh" user#server:/path/to/sync /path/to/local/destination' and You can add that to your .bashrc file.
After that you type "myrsync" and command "rsync -av -e "ssh" user#server:/path/to/sync /path/to/local/destination" will be execute.

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

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.

Resources