legacy reasons. That is why I am binding to ^I
.
The code below will check what ctrl+i is currently bound to and store it. This means we can override the tab key in such a way that if we are not on a word that should be expanded, we can fall back to whatever tab used to do.
We then bind ctrl+i to a new function that will spawn a notification and then run whatever ctrl+i used to be bound to.
currentwordcomplete(){
notify-send "Yay, it works"
zle ${currentword_default_completion:-expand-or-complete}
}
# Record what ctrl+i is currently set to
# That way we can call it if currentword_default_completion doesn't result in anything
[ -z "$currentword_default_completion" ] && {
binding=$(bindkey '^I')
[[ $binding =~ 'undefined-key' ]] || currentword_default_completion=$binding[(s: :w)2]
unset binding
}
zle -N currentwordcomplete
bindkey '^I' currentwordcomplete
You can check this works by sourcing the file and pushing tab. You should get a notification and then normal tab completion should work.
The LBUFFER
variable in a ZSH widget contains a string equal to everything before your cursor in the buffer. We can use expansion built into ZSH to turn that into an array of words.
tokens=(${(z)LBUFFER})
The z
flag here will expand the string using shell parsing to split the string into arguments. This takes into account quotes and escaped spaces. Full details can be found in the documentation.
This means that we have an array of words. The word we are currently on will be the last:
lastWord="${tokens[-1]}"
I also get the first argument which is normally going to be the command currently being run. Although I don’t use this at the moment, I thought it might be useful to be able to exclude certain commands from completion.
cmd="${tokens[1]}"
I have a function called word_replace
that takes the word that should be replaced and the command, and prints what it should be replaced with. It also returns 0 on success (there was a replacement found).
word_replace(){
local ret=1
local word="$1"
local cmd="$2"
case "$word" in
wl) wordlistSelect; return 0 ;;
myip) ip route | grep -oE '(dev|src) [^ ]+' | sed 'N;s/\n/,/;s/src //;s/dev //' | awk -F',' '{print $2 " " $1}' | sort -u | fzf -1 --no-preview | cut -d' ' -f1; return 0 ;;
esac
return "$ret"
}
In this case, it is a simple switch statement. An interesting side note is the use of -1
in the FZF command for myip
. This will prevent FZF from running if there is only 1 option fed to it. So, if I am only connected on one interface, it will simply fill my ip address rather than prompting me to choose one.
Obviously, the logic used here could be as simple or complex as you wish.
Inside the currentwordcomplete
function, I get the output of the word_replace
function which is passed the current word. If that doesn’t result in a completion, I will run the word_replace
function again, using only the part of the word that comes after an =
sign (if there is one).
In either case, a variable called swap will contain what the current word should be replaced with.
There will also be a variable called ret
that will be equal to 0 if the replacement should be made.
currentwordcomplete(){
...
# Check we haven't pushed space
if [ "${LBUFFER[-1]}" != " " ]; then
swap="$(word_replace "$lastWord" "$cmd")"
ret="$?"
# This part checks if the part after an = is completable
if [ "$ret" -ne "0" ]; then
local afterEqual="${lastWord##*=}"
local beforeEqual="${lastWord%=*}"
# If they are different, there is an equals in the word
if [ "$afterEqual" != "$lastWord" ]; then
swap="${beforeEqual}=$(word_replace "$afterEqual" "$cmd")"
ret="$?"
fi
fi
fi
...
}
Finally, I check if the completion should be made. If it shouldn’t, I call whatever function the tab key used to be bound to. If it should, I change the last item of the tokens array that we created earlier. I then set the LBUFFER variable to the changed string.
if [ "$ret" -eq "0" ]; then
if [ -n "$swap" ]; then
tokens[-1]="$swap"
LBUFFER="${tokens[@]}"
fi
zle reset-prompt
return 0
else
zle ${currentword_default_completion:-expand-or-complete}
return
fi
This is a relatively un-intrusive addition to ZSH that I use most days. I don’t use a huge number of these but the two I mentioned here, word lists and my ip, I use a lot.
You can find the full source here. If you are interested in my full ZSH config is here.