Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

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

+5
−0

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?

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

1 answer

+2
−0

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:

  1. The -Q flag must be used with the compadd function to not quote metacharacters. Without this flag, zsh will use \{ in place of {, and \} in place of } in the commandline argument.

  2. The -S '' flag is used with the compadd function to prevent a space from being added after completion of directories.

  3. 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:

  1. The completion options will not include hidden files or folders, but they can be added by adding the -a option to the ls command.

For further modification of this code, the following notes may be helpful:

  1. It may be helpful to replace any \{ characters with {, and any \} characters with } in the PREFIX parameter (i.e., PREFIX=${PREFIX//\\\{/\{} and PREFIX=${PREFIX//\\\}/\}}).

  2. It may also be helpful to use the ignore_braces option.

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »