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.
Best Answer
You're mixing up scripts and functions.
Making a script
A script is a standalone program. It may happen to be written in zsh, but you can invoke it from anywhere, not just from a zsh command line. If you happen to run a script written in zsh from a zsh command line or another zsh script, that's a coincidence that doesn't affect the script's behavior. The script runs in its own process, it doesn't influence its parent (e.g. it can't change variables or the current directory).
Your code accomplishes a standalone task which can be invoked from anywhere and doesn't need to access the state of the shell that runs it, so it should be a script, not a function.
A script must be an executable file:
chmod +x /path/to/script
. It must start with a shebang line to let the kernel know what program to use to interpret the script. In your case, add this line to the top of the file:Put the file in a directory that is listed in the
$PATH
variable. Many systems set up either~/bin
or~/.local/bin
in a user's defaultPATH
, so you can use these. If you want to add another directory, see http://unix.stackexchange.com/questions/26047/how-to-correctly-add-a-path-to-pathWhen you type a command name that isn't an alias, a function or a builtin, the shell looks for an executable file of that name in
$PATH
and executes it. Thus you don't need to declare the script to the shell, you just drop it in the right place.Making a function
A function is code that runs inside an existing shell instance. It has full access to all the shell's state: variables, current directory, functions, command history, etc. You can only invoke a function in a compatible shell.
Your code can work as a function, but you don't gain anything by making it a function, and you lose the ability to invoke it from somewhere else (e.g. from a file manager).
In zsh, you can make a function available for interactive sessions by including its definition in
~/.zshrc
. Alternatively, to avoid cluttering.zshrc
with a very large number of functions, you can use the autoloading mechanism. Autoloading works in two steps:autoload -U myfunction
.myfunction
is invoked for the first time, zsh looks for a file calledmyfunction
in the directories listed in$fpath
, and uses the first such file it finds as the definition ofmyfunction
.All functions need to be defined before use. That's why it isn't enough to put the file in
$fpath
. Declaring the function withautoload
actually creates a stub definition that says “load this function from$fpath
and try again”:Zsh does have a mechanism to generate those stubs by exploring
$fpath
. It's embedded in the completion subsystem.#autoload
as the first line of the file..zshrc
, make sure that you fully setfpath
before calling the completion system initialization functioncompinit
.Note that the file containing a function definition must contain the function body, not the definition of the function, because what zsh executes when the function is called is the content of the file. So if you wanted to put your code in a function, you would put it in a file called
extract
that is in one of the directories on$fpath
, containingIf you want to have initialization code that runs when the function is loaded, or to define auxiliary functions, you can use this idiom (used in the zsh distribution). Put the function definition in the file, plus all the auxiliary definitions and any other initialization code. At the end, call the function, passing the arguments. Then
myfunction
would contain:P.S.
7z x
works on most archive types.