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
-Qflag must be used with thecompaddfunction 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 thecompaddfunction 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$HOMEin 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
-aoption to thelscommand.
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 thePREFIXparameter (i.e.,PREFIX=${PREFIX//\\\{/\{}andPREFIX=${PREFIX//\\\}/\}}). -
It may also be helpful to use the
ignore_bracesoption.

0 comment threads