Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: help with command completion

Shao Zhang wrote:

> Hi,
> 	I am trying to learn programming command completion using
> 	zstyle. After a long read of /usr/share/zsh/functions, I still
> 	have to idea, not even how to start.
> 	I have the following two cases to do, can someone please give me
> 	a hand and tell me how to start??
> 	lpr -P[TAB] complets to a list a printer name, read from
> 	/etc/printcap.

It's not `using zstyle'. zstyle is only the mechanism used for
configuration it can be used by completion functions but it need not.

The first step is always to create a file named like _lp (the
underscore is the important bit). Place that in a directory in your
$fpath (so that compinit will find it). In the first line in that file 
you specify the commands this file will provide completion
functionality for, e.g.:

  #compdef lpr

Since there is already a function offering completion for lpr (namely
_ps, which completes only names of postscript files) you have to make
sure that the file you created will be found before that (i.e. it is
in a directory earlier in $fpath than the User directory from the

In the rest of the file you use the basic mechanisms described in the
compwid manual page and/or the utility functions described in the
compsys manual page to generate the completions. Since this is only a
simple example I'll use only the basic ones.

First we want to see if we have to complete printer names, which is
the case if we are after a -P option, directly after it in the same
word or in the word after it. For this kind of stuff there are the
special parameters like $PREFIX and $words. We could do:

  if [[ "$PREFIX" = -P* || "$words[CURRENT-1]" = -P ]]; then

Since $PREFIX is the beginning of the string the cursor is on (up to
the cursor position), the first test is true if we are on a word
beginning with -P and the second one uses the $words array which
contains all words from the line and the $CURRENT special parameter
which gives the index for the word we are on into $words array. So
$words[CURRENT-1] gives the previous word.

We would now start generating names of printers as possible
completions, the completion code will use the strings in $PREFIX and
$SUFFIX to see if the names we generate match the stuff from the
line. But since in the first case there is the -P at the beginning
this wouldn't match, so we have to `remove' that. More precisely, we
have to ensure that the completion code still knows about it but
doesn't use it for matching. This is done by removing it from $PREFIX
and appending it to another special parameter: $IPREFIX (that's
`ignored prefix'). We could do it by hand:


But since such tests followed by such modifications are needed quite
often, there is support for it: the compset builtin. This does the
test and if it succeeds, immediatly modifies the parameters that need
to be modified. So we can do everything together with:

  if compset -P -P || [[ "$words[CURRENT-1] = -P ]]; then

Then we can just start generating the printer names in this `if'. One
possibility is something like:

  compadd - "${(@)${(@s:|:)${(@)${(@f)$(< /etc/printcap)}:#[    \#]*}%%:*}%%[ 	]*}"

The compadd builtin gets the possible completions as arguments. It
also supports several options but these are indeed needed relatively
seldom. In the functions from the distribution most options used are
generated more or less automatically by some helper functions. These
mainly make the completion function more fun to use or the completion
listing more fun to look at (at least I hope that).

But lets first continue with the example. Until now the function can
complete only printer names after -P which isn't really useful for a
command like lpr, so we have to make it complete file names, too, but
only for the other arguments:

  if ...; then

This uses the utility function _ps from the distribution which
completes Postscript files (or directories or all files if there are
no Postscript files). _ps itself, btw, mainly uses another helper
function: _files. This is used throughout the completion system and
completes filenames allowing partial pathname completion
(i.e. /u/inc<TAB> can be completed to /usr/include).

So, the whole function (file) could look like:

  #compdef lpr

  if compset -P -P || [[ "$words[CURRENT-1] = -P ]]; then
    compadd - "${(@)${(@s:|:)${(@)${(@f)$(< /etc/printcap)}:#[    \#]*}%%:*}%%[ 	]*}"

Since in the completion system we generally want to avoid the extra
processes needed for $(...), we would use caching for the printer
names there (and the function for the lp commands I just sent to
zsh-workers does that). And we would also make this nicer by
supporting the other mechanisms used throughout the completion system.

One of these is that matches are always added with descriptions that
can be made visible in completion lists using the format style for the 
`descriptions' and `messages' tags. Everything a completion function
needs to do for that is call the utility function _description. It
gets a tag name (users are completely free to chose any tag name they
want), the name of an array and a short description as
arguments. E.g.:

  local expl

  _description printers expl printer

(That `printer' is the description.) _description will set up the
array $expl to contain options for compadd that add the description if 
requested by the user and several other things users can request for
all matches generated (this `requesting', btw, is what is done using

Another thing all completion functions (I mean those in the
distribution) should support is tags. More precisely, users should be
able to configure (using the tag-order style) if they want to see
completions of a certain type or not. For this, there are utility
functions like _wanted and _requested which get a tag name as
argument. In the example above we can use the first one:

  _wanted printers && compadd ...

_wanted and _requested only return zero if the user requested the type 
of matches represented by the tag (more precisely: didn't say
explicitly that he doesn't want them).

With that users can say that they don't want printer names to be
completed (this sounds weird when talking about contexts where only
one type of matches can be generated, but if multiple types are
possible, this quickly makes sense).

And since every decent completion function should use both
descriptions and tags, _wanted and _requested can also be made to call 
_description by giving them the same arguments that would be given to
that, so:

  #compdef lpr

  if compset -P -P || [[ "$words[CURRENT-1] = -P ]]; then
    local expl

    _wanted printers expl printer &&
      compadd - "${(@)${(@s:|:)${(@)${(@f)$(< /etc/printcap)}:#[    \#]*}%%:*}%%[ 	]*}"

That's all that's needed to make the function behave like the other
functions in the completion system (_ps already does all the
description and tag stuff, so we don't have to do anything there).

As Peter already said I, too, would suggest that people who want to
write completion functions first start by copying and modifying
existing functions. Then continue with simple functions using the
basic stuff from the compwid manual and then start using the utility
functions. Note that there is also the completion-style-guide in the
Etc directory of the distribution which should help a bit with the
more complicated stuff (mainly the utility functions from the
completion system).

And if you then arrive at some interesting completion function I would 
be glad if you could send it to the list and at least I (and I guess
others, too) will look at it and see if and how it can be improved or
made more standard in its behaviour.

> 	rm -rf completes fine, but sudo rm -rf does not work.

Hm. The current development version (and some older version, too) have 
a completion function for sudo and it works nicely for me. Don't you
have _sudo or do you have aliased sudo to something?


Sven Wischnowsky                         wischnow@xxxxxxxxxxxxxxxxxxxxxxx

Messages sorted by: Reverse Date, Date, Thread, Author