Current Word Completion
Tab completion is one of the main reasons I decided to make the switch to ZSH. Being able to see descriptions and cycle between options makes finding options a breeze:
However, there are times that I would prefer to have completion
options based on the current word I have typed, regardless of the
command I am running. I suppose this is more like an extension of ZSH’s
global aliases, but with the ability to choose from multiple options. An
example of this is for word lists. There are a lot of commands I use
that require word lists and it is a pain to write out the full path
before I am able to tab-complete. My solution is what I have been
calling Current Word Completion. My current iteration of it relies on FZF for the completions,
although in the long term, I would like to make it work with ZSH’s built
in completion system. Here is a demonstration using wl
for
wordlists and myip
for my local machines ip addresses.
You can see that it doesn’t stop ZSH’s normal completion from working and it can work multiple times on any command.
As with tab-completion, I have it bound to my tab key.
If all you want to do is implement it yourself, you can find the
source code here.
Simply source the file in your .zshrc
and you should be
good to go. You can add new options to the word_replace
function.
If you are interested in how it works, keep reading.
Overriding tab key
The tab key is interpreted by the terminal as ctrl+i for 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.
Splitting up the current line
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]}"
Word Replace
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.
Getting the completion
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
...
}
Making the change
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
Conclusion
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.