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.

Post History

66%
+2 −0
Q&A zsh - autocomplete with braces in the middle of a directory

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

posted 6d ago by Trevor‭  ·  edited 5d ago by Trevor‭

Answer
#4: Post edited by user avatar Trevor‭ · 2025-04-12T01:55:36Z (5 days ago)
  • 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:
  • ```sh
  • #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:
  • ```sh
  • #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`:
  • ```sh
  • 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 <Tab> key in zsh.
  • To do so, run `$ bindkey` and then find the `"^I"` entry, which corresponds to <Tab>.
  • Doing this on my system shows that <Tab> is set to `expand-or-complete`.
  • If we were to use `expand-or-complete` with diff 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`:
  • ```sh
  • 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.
  • 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:
  • ```sh
  • #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:
  • ```sh
  • #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`:
  • ```sh
  • 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 <Tab> key in zsh.
  • To do so, run `$ bindkey` and then find the `"^I"` entry, which corresponds to <Tab>.
  • Doing this on my system shows that <Tab> 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`:
  • ```sh
  • 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.
#3: Post edited by user avatar Trevor‭ · 2025-04-10T18:52:27Z (6 days ago)
  • 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:
  • ```sh
  • #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:
  • ```sh
  • #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`:
  • ```sh
  • 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 <Tab> key in zsh.
  • To do so, run `$ bindkey` and then find the `"^I"` entry, which corresponds to <Tab>.
  • Doing this on my system shows that <Tab> is set to `expand-or-complete`.
  • If we were to use `expand-or-complete` with diff 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`:
  • ```sh
  • 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.
  • 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:
  • ```sh
  • #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:
  • ```sh
  • #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`:
  • ```sh
  • 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 <Tab> key in zsh.
  • To do so, run `$ bindkey` and then find the `"^I"` entry, which corresponds to <Tab>.
  • Doing this on my system shows that <Tab> is set to `expand-or-complete`.
  • If we were to use `expand-or-complete` with diff 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`:
  • ```sh
  • 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.
#2: Post edited by user avatar Trevor‭ · 2025-04-10T18:52:01Z (6 days ago)
  • 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:
  • ```sh
  • #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:
  • ```sh
  • #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`:
  • ```sh
  • 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 <Tab> key in zsh.
  • To do so, run `$ bindkey` and then find the `"^I"` entry, which corresponds to <Tab>.
  • Doing this on my system shows that <Tab> is set to `expand-or-complete`.
  • If we were to use `expand-or-complete` with diff 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`:
  • ```sh
  • 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.
  • 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:
  • ```sh
  • #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:
  • ```sh
  • #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`:
  • ```sh
  • 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 <Tab> key in zsh.
  • To do so, run `$ bindkey` and then find the `"^I"` entry, which corresponds to <Tab>.
  • Doing this on my system shows that <Tab> is set to `expand-or-complete`.
  • If we were to use `expand-or-complete` with diff 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`:
  • ```sh
  • 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.
#1: Initial revision by user avatar Trevor‭ · 2025-04-10T18:18:54Z (6 days ago)
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:
```sh
#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:
```sh
#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`:
```sh
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 <Tab> key in zsh.
To do so, run `$ bindkey` and then find the `"^I"` entry, which corresponds to <Tab>.
Doing this on my system shows that <Tab> is set to `expand-or-complete`.

If we were to use `expand-or-complete` with diff 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`:
```sh
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.