Ubuntu – How to properly make custom zsh completions “just work”

autocompleteoh-my-zshUbuntuzsh

Please forgive me if I'm doing something stupid here, the documentation is huge and searching didn't turn up anything yet.

I'm trying to create shell completions for my custom script called fab. For bash it was easy, just drop them in /etc/bash_completion.d and they work. But oh boy, is zsh a PITA…

I have the completion function _fab and it works fine when enabled with compdef _fab fab. I put it in /usr/share/zsh/vendor-completions/_fab which already was in my $fpath. The file starts with #compdef fab and ends with compdef _fab fab. Looks good:

$ type _fab
_fab is an autoload shell function

But whenever I started a new shell, fab completions didn't work (other functions from vendor-completions, like _docker, were fine). compinit fixed this for that specific shell. I figured out that rm ~/.zcompdump ~/.zcompdump-$(hostname)-5.1.1; compinit make it work permanently (5.1.1 = my zsh version).

Questions:

  1. What and when reads ~/.zcompdump to set up initial completions?
  2. man zshall says:

    The next invocation of compinit will read the dumped file instead of performing a full initialization.

    If that was the case, compinit wouldn't fix my completions before I deleted ~/.zcompdump, right? Am I missing something?

  3. What's ~/.zcompdump-$(hostname)-5.1.1 and how is it related to .zcompdump? The only difference is one completion which is in ~/.oh-my-zsh/completions (because $ZSH points to ~/.oh-my-zsh). Is it an oh-my-zsh thing?
  4. If I were to package these completions into a redistributable package or create an installer script, where should I place zsh completions and what else should I do during installation to make sure everything just works?

I'm targeting Ubuntu 16.04, 18.04 and 19.04, but non-distro-specific information is welcome. I'm testing this on Ubuntu 16.04 with zsh 5.1.1 and recent oh-my-zsh.

Best Answer

TL,DR: In normal operations, just drop the file into the appropriate directory. While testing, you need to remove the cache file (.zcompdump by default, but users can put it in a different location, and oh-my-zsh does put it in a different location).


The simple answer is to write the completion function in a file where the first line is #compdef fab. The file must be in a directory on $fpath.

The file can either contain the function body, or a definition of the function followed by a call to the function. That is, either the file contains something like

#compdef fab
_arguments …

or

#compdef fab
function _fab {
  _arguments …
}
_fab "$@"

The file must be present on $fpath before compinit runs. That means you need to pay attention to the order of things in .zshrc: first add any custom directories to $fpath, then call compinit. If you use a framework such as oh-my-zsh, make sure to add any custom directories to $fpath before the oh-my-zsh code.

compinit is the function that initializes the completion system. It reads all the files in $fpath and checks their first line for magic directives #autoload and #compdef.

.zcompdump is a cache file used by compinit. ~/.zcompdump is the default location; you can choose a different location when running compinit. Oh-my-zsh calls compinit with the -d option to use a different cache file name given by the variable ZSH_COMPDUMP, which defaults to

ZSH_COMPDUMP="${ZDOTDIR:-${HOME}}/.zcompdump-${SHORT_HOST}-${ZSH_VERSION}"

The host name is included for the sake of people whose home directory is shared between machines and who may have different software installed on different machines. The zsh version is included because the cache file is incompatible between versions (it includes code that changes from version to version).

I think all of your problems are due to a stale cache file (and that's made you overcomplicate the situation). Unfortunately, zsh's algorithm to determine whether the cache file is stale is not perfect, presumably in the interest of speed. It doesn't check the content or the timestamps of the files on $fpath, it just counts them. A .zcompdump file starts with a line like

#files: 858     version: 5.1.1

If the zsh version and the number of files are correct, zsh loads the cache file.

The cache file only contains associations between command names, not the code of completion functions. Here's are some common scenarios where the cache works transparently:

  • If you add a new file to $fpath, this invalidates the cache.
  • More generally, if you add and remove files on $fpath, and the total number of removed files is not the same as the total number of removed files, this invalidates the cache.
  • If you move a file to a different directory in $fpath without changing its name, this does not affect anything that's in the cache, so the cache remains correct.
  • If you modify a file in $fpath without changing its first line, this does not affect anything that's in the cache, so the cache remains correct.

Here are some common scenarios where the cache becomes invalid, but zsh doesn't realize it.

  • You add some files to $fpath and remove exactly the same number of files.
  • You rename a file in $fpath.
  • You add or modify the #compdef (or #autoload) line at the top of the file.

That last point is what tends to bite during testing. If you change the #compdef line, you need to remove the .zcompdump file and restart zsh (or rerun compinit).

If you put completions in a redistributable package, just drop the completion file into a directory that's in the system-wide $fpath. For an Ubuntu package, the appropriate place is /usr/share/zsh/vendor-completions. For something installed under /usr/local, that's /usr/local/share/zsh/site-functions. That's all you need to do.

The one thing that isn't transparent is if you need to change the #compdef line in an upgrade, or if you remove or rename some files. In such cases, users will need to remove their cache file, and that's not something you can do from a package that gets installed on a multiuser machine.