Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
zsh - autocomplete with braces in the middle of a directory
Suppose I have the following directory structure:
folder/
aaa/
f.txt
bbb/
f.txt
I want to compare the file f.txt
as it is common to both directories. So in zsh I type this:
% diff folder/{aaa,bbb}/
Pleasingly, zsh autocompletes both the aaa
and bbb
directories even inside the brace.
Next, I press the Tab key hoping that zsh will autocomplete any files common to both the aaa
and bbb
directories, so I'm hoping that pressing Tab will result in this:
% diff folder/{aaa,bbb}/f.txt
However, what zsh actually does is expand the braces resulting in this:
% diff folder/aaa/ folder/bbb/
Is there a way to get zsh to keep the braces in the middle of the directory and autocomplete the common file name as I described?
1 answer
I figured out a solution myself. I'm not a zsh expert so this may be improved, but it works.
1) make a custom completion file for the diff command
The completion file that zsh uses for the diff
command is called _diff
.
On my computer, this file is located at /usr/share/zsh/functions/Completion/Unix/_diff
and is shown below:
#compdef diff gdiff
_diff_options "$words[1]" ':original file:_files' ':new file:_files'
We will create a new _diff
file to override the default file above in order to perform the custom completion.
We save this file as /home/trevor/zsh_completions/_diff
whose contents are shown below:
#compdef diff
function _files_common() {
last_buffer_item=$words[-1]
matches=$(sed -n 's/^\(.*\){\(.*\),\(.*\)}\(.*\)$/\1 \2 \3 \4/p' <<< $last_buffer_item)
if [[ -n $matches ]]; then
# parse sed output
before_braces_dir=$(cut -d ' ' -f 1 <<< $matches)
folder_1=$(cut -d ' ' -f 2 <<< $matches)
folder_2=$(cut -d ' ' -f 3 <<< $matches)
after_braces=$(cut -d ' ' -f 4 <<< $matches)
# get any directories after the braces
after_braces_dir=$(sed -n 's/^\/\(.*\/\).*$/\1/p' <<< $after_braces)
# full path of each of the two directories
path_1=$before_braces_dir$folder_1/$after_braces_dir
path_2=$before_braces_dir$folder_2/$after_braces_dir
# arrays for compadd
local -a _descriptions_files _values_files _descriptions_dirs _values_dirs
# iterate through names in path 1, and see if they're in path 2
# note: ls -F adds a slash at the end of folder names
names=( $( ls -F ${path_1//\~/$HOME} ) )
for name in "${names[@]}"; do
test_path=$path_2$name
commandline_arg=$before_braces_dir{$folder_1,$folder_2}/$after_braces_dir$name
# if path is a file
if [[ -f ${test_path//\~/$HOME} ]]; then
_values_files+=( $commandline_arg )
_descriptions_files+=( $name )
# if path is a directory
elif [[ -d ${test_path//\~/$HOME} ]]; then
_values_dirs+=( $commandline_arg )
_descriptions_dirs+=( $name )
fi
done
# add completions
# for directory completions, use an empty string as the suffix to prevent a
# space being added at the end of the completion
compadd -Q -d _descriptions_files -a _values_files
compadd -S '' -Q -d _descriptions_dirs -a _values_dirs
fi
}
# if the argument contains left and right braces, use the _files_common function
# otherwise, use the _files function
# note: the format of these commands is based on
# /usr/share/zsh/functions/Completion/Unix/_diff
last_buffer_item=$words[-1]
if [[ $last_buffer_item == *{*,*}* ]]; then
_diff_options "$words[1]" ':both files:_files_common'
else
_diff_options "$words[1]" ':original file:_files' ':new file:_files'
fi
2) add the custom completion file to zsh's path
To make zsh use the custom completion file from the previous step, we add the directory of the custom completion file to the fpath
variable by adding this to your .zshrc
:
fpath=(/home/trevor/zsh_completions/ $fpath)
Note that we must prepend to fpath
rather than append in order to give the custom file precedence.
Also note that this line should be added before the compinit
command.
3) configure zshrc to run complete-word instead of expand-or-complete for the diff command
Next, find the function that is mapped to key in zsh.
To do so, run $ bindkey
and then find the "^I"
entry, which corresponds to .
Doing this on my system shows that is set to expand-or-complete
.
If we were to use expand-or-complete
with the diff
command and my custom completion function, then zsh will expand the commandline argument with braces into two separate arguments.
Instead, we need to use complete-word
instead of expand-or-complete
when the diff
function is being used.
To do so, add this to your .zshrc
:
custom_tab () {
words=(${(z)BUFFER})
if [[ ${words[1]} == diff ]]; then
zle complete-word
else
zle expand-or-complete
fi
}
zle -N custom_tab
bindkey '^I' custom_tab
notes
Here is a reiteration of a couple of key concepts that have been used to make this work:
-
The
-Q
flag must be used with thecompadd
function to not quote metacharacters. Without this flag, zsh will use\{
in place of{
, and\}
in place of}
in the commandline argument. -
The
-S ''
flag is used with thecompadd
function to prevent a space from being added after completion of directories. -
If the
~
character is present in the commandline argument, it will not be expanded and the solution will not work. So, we replace the~
character with$HOME
in the appropriate places, which is done via${path_1//\~/$HOME}
and${test_path//\~/$HOME}
.
There are some edge cases in which this solution will not work, but can be fixed with some modifications:
- The completion options will not include hidden files or folders, but they can be added by adding the
-a
option to thels
command.
For further modification of this code, the following notes may be helpful:
-
It may be helpful to replace any
\{
characters with{
, and any\}
characters with}
in thePREFIX
parameter (i.e.,PREFIX=${PREFIX//\\\{/\{}
andPREFIX=${PREFIX//\\\}/\}}
). -
It may also be helpful to use the
ignore_braces
option.
0 comment threads