skip navigation

A Case for the Z Shell

This article was originally written for Daemon News a few years ago. Some parts may be out of date, but the bulk is still useful.

In other news, zsh 4.0 has just been released and it's great!

Abstract

This article hopes to highlight some of the useful features of the Z Shell (referred to as just "zsh" for the rest of this document) to people who use other shells. Who knows, some of them may switch. :-)

The author recognises that choice of shell is a highly subjective matter, but hopes that readers will not balk at being presented with alternative material.

The information presented is based on the authors experience with zsh 3.0.5 under both FreeBSD and (ack! spit!) Solaris.


Introduction

During a recent thread on the freebsd-chat mailing list, the issue of which shell to use came up. The thread was started by somebody who insisted that FreeBSD was ancient and not worth bothering with because it didn't ship with bash in the base system, only sh and csh. Needless, to say, plenty of opinions soon emerged.

One thing emerged that interested me was the large proportion of bash users, who leapt to its defense (as well as the large number of tcsh users). I don't have bad things to say about bash. I think it's a fine shell and well engineered (I believe the author develops it under a *BSD platform!). However, I also feel that zsh offers much more, that users of other shells might well find useful.

Before I start to look at zsh specifics, let's take a trip back in time and see how we arrived where we are today.


History of the Unix shell

In the beginning, the Unix shell was basic. Very basic. Even commands such as "if", "for" (and even "cd" to begin with) were not built in. Things mostly stayed this way until 6th Edition Unix, when the folks at Berkeley produced csh from the 6th Edition shell. Csh was a big improvement over the original sh in many respects; it had more built-in commands, a different (and possibly friendlier syntax) and most importantly of all command history. Naturally, csh took off quickly.

Shortly afterwards, 7th Edition arrived, with a completely rewritten shell, courtesy of Steve Bourne. This supported much better programmability, although it had few interactive luxuries, like a history. The release notes list features as:

Completely new SH program supports string variables, trap handling, structured programming, user profiles, settable search path, multi-level filename generation, etc.

This shell became the base standard for practically every other shell in existence today.

Some time later, David Korn developed the Korn shell as he needed superior programming facilities than were available in the Bourne shell. For some background to that development, see ksh: An Extensible High Level Language (the history section).

The Korn Shell (88 Edition) has become fairly standard on commercial Unixes at this point, due to cheap licensing. Most of the commercial Unixes have not yet updated to the 93 Edition of the Korn shell, as far as I am aware (although it is included as part of CDE, but is merged in with the Motif libraries to create dtksh).

Because ksh source code wasn't freely available, a group of developers went on to create pdksh, a freely available clone of ksh. It's almost exactly compatible with the 88 Edition of ksh, with a few enhancements, such as bindable keys.

As part of the GNU project in the late 1980s, Chet Ramey developed the Bourne-Again SHell, bash. This is designed to be a superset of the POSIX shell specifications. Those specifications were originally created out of the Bourne Shell and the Korn Shell, so there is a large amount of compatibility between bash and ksh. However, bash aims hard to be as POSIX compliant as possible, rather than trying to emulate the Korn Shell.

For a long time, csh had been the standard shell for interactive use. However, it could be better. Someone (not sure who) developed tcsh, which is a much better interactive shell for people who are used to csh. One of the main features that proponents like is its programmable completion.

Finally, there is the Z Shell. Originally designed by Paul Falstad, it was intended to be a superset of ksh, with features to make it palatable to users of csh. To bring some relevance to this journal, zsh is distributed under a BSD-style license.

I am sure that there are errors in the above. If you spot any, please mail me. I know that the text is also incomplete. It doesn't mention rc or es anywhere. Again, let me know if you think I've left something vital off.


Zsh Goodies

This section aims to describe fun features you may not know about in zsh. If you don't already have zsh installed, it's probably available as a port on your system, in which case you can do:

cd /usr/ports/shells/zsh ; make install

If you don't use zsh as your login shell already, then just type in zsh at your prompt, play around and type in exit when you are done.

All examples are shown with a prompt of a lone '%' character (except where we are playing with the prompt!). Note that many examples below refer to shell options; these can be turned on with the setopt command and turned off with the unsetopt command.

Only a basic introduction to features will be given here and only features which are oriented towards interactive use at that. For full details, zsh has extensive manual pages and an info file. If you want to know more about a feature, please look it up in the manual.

Globbing

Globbing is the process the shell expands the wildcards on the command line before it starts the process. You are probably familiar with the canonical example:

% echo *

Which lists all the files in the current directory. Well, actually it lists directories, pipes and many other things. But it doesn't list files (and directories...) which begin with a dot.

Zsh has extended globbing in many directions. Of course, it still supports all the original forms. To activate most of these you have to turn on the shell option EXTENDED_GLOB.

One of the first things that is useful is a way to specify exclusion. For instance, have you ever tried to get a listing of all filenames without dots in with ordinary globbing? Here's how in zsh.

% touch filea file.b filec
% ls *~*.*
filea   filec

You can list any pattern you like after the ~ to exclude it. As well as exclusion, grouping is also possible (and similiar to csh brace expansion) with brackets.

% touch foo.c foo.h bar.c bar.h fred.c fred.h
% ls (foo|bar).*
bar.c   bar.h   foo.c   foo.h
% ls *.h~(fred|foo).h
bar.h

If you have files with numbers in them, then zsh will glob them numerically for you. This is particularly useful with things like MH folders or news spools where every filename is a number.

% grep '^From:' ~/Mail/inbox/<1-5>
/home/dom/Mail/inbox/1:From: Dave Williams <dwilliams@example.com>
/home/dom/Mail/inbox/2:From: rmc@devnull.Aus.Sun.COM (Richard McDougall)
/home/dom/Mail/inbox/3:From: Dave Sloan <dsloan@demon.net>
/home/dom/Mail/inbox/4:From: Dave Sloan <dsloan@demon.net>
/home/dom/Mail/inbox/5:From: Dave Sloan <dsloan@demon.net>

As noted earlier, normal globbing is rather inflexible about selecting which types of files you want to glob. Zsh can supply options to select what type of files you want to glob or to glob files by date or perhaps userid. Here's the alias I use when I've just unpacked a new piece of software. It views all the files that begin with a capital letter, and only files, no directories. This usually amounts to the NEWS, READMEs, and other assorted release notes that I want to read first. It also excludes .c and .h files as I'm not usually interested in them.

% alias readme='less [A-Z]*~*.[ch](.)'

Read from right to left, it reads like "run less on all things that begin with a capital letter, except those that end in .c or .h. The brackets after the glob specify the options. In this case, a single "." just means select ordinary files only. You can select directories with "/" and symlinks with "-".

% alias lsd='ls -ld *(/)
% cd ; lsd
drwxr-xr-x   5 dom  ho   512 Jan  8  1999 GNUstep/
drwxr-xr-x   4 dom  ho  1536 Jul 12 12:20 bin/
drwxr-xr-x   7 dom  ho  2560 Jul 30 10:00 doc/
drwxr-xr-x   4 dom  ho  1536 Jul 30 16:12 etc/
drwxr-xr-x  13 dom  ho   512 Jun 15 16:38 libdata/
drwxr-xr-x   6 dom  ho   512 Jun  8 16:41 libexec/
drwx------   4 dom  ho   512 Jul 30 16:21 mail/
drwxr-xr-x   5 dom  ho  1024 Jul 27 12:42 public_html/
drwxr-xr-x  16 dom  ho  1024 Jul 29 09:47 src/
drwxr-xr-x  10 dom  ho   512 Jul 20 19:45 work/

There are many other options available for selecting different files and directories. You can choose user, group or world readable/writeable/executable, or maybe setuid/setgid files. Options can be negated with a "^" character. One of the ones I find useful is the ability to do date based globbing. Here is an example of how I clear up old stuff that hasn't been looked at (accessed) or changed (modified) in over 6 months from an application's spool directory.

% rm -f /legacy/data01/fds/spool/*(.aM+6mM+6)

Lastly, one of the most powerful aspects of zsh globbing. Zsh can glob recursively, using a "**/". This provides a means similiar to find, but easier to use than find -print | xargs command. For example, to find all the syslog related stuff in the source code for the /sbin directory:

% egrep -l LOG_ /usr/src/sbin/**/*.[ch]

When combined with the previously described options, this can be exceptionally powerful. For example, to search for all setuid root executables on your system.

% ls -l /**/*(su0x)

Throughout zsh, you will see more examples of this extended globbing and how useful it is.

Startup files

Zsh probably has more startup files than any other shell. Please, don't be scared, it's a lot easier to manage than you think. And not only that, it makes it easier to make sure that the right commands get executed at the right time.

In bash, people often wonder why their .bashrc file isn't always executed when they start a session. Likewise in csh (and possibly tcsh; I'm not sure) people often manage to screw themselves by putting interactive commands (those that use a terminal) in their .cshrc file.

Zsh reads its files in the following order.

  1. ~/.zshenv

    This is where you would put settings for environment variables only. You can't assume that you have access to a terminal device here, because it might get run as part of a script.

  2. ~/.zprofile

    This file is only sourced if the shell is a login shell. This is the place where you'd run things that produce output, such as /usr/games/fortune.

  3. ~/.zshrc

    This is the main zsh initialization file. It's where you put all of your aliases, shell functions and so on. It gets executed for every interactive shell. If you're confused by all these files, just put all of your startup commands in here and things will probably work just fine.

  4. ~/.zlogout

    This gets run when an interactive shell gets logged out. So, put your witty closing remarks here.

Aliases & Functions

You are probably familiar with aliases from other shells. Simple aliases in zsh are basically the same as in bash.

% alias ls='ls -F'
% alias l='ls -s'
% alias ll='ls -l'

Zsh has another trick up its sleeve, though. It is possible to define global aliases, which are expanded anywhere on the command line. If, like me, you can never manage to type in more or less correctly these may be helpful for you.

% alias -g M='| more'
% alias -g L='| less'
% ls -l /usr/bin L

You should be careful with global aliases, as they can create a lot of havoc if not used carefully. For example, if you defined an alias called /etc/passwd, which expanded to /etc/group, you would end up very confused! Generally, if you stick to all caps in your global aliases, you won't go far wrong.

Zsh aliases are not like csh aliases that take arguments. If you need to use an alias with arguments, use a shell function instead. For example,

% function fa {
function> 	finger $1@admin
function> }
% fa dom
[admin]
Login       Name               TTY         Idle    When    Where
dom      Dominic Mitchell      pts/3        <Aug 19 11:10> voodoo.pandhm.co    
% fa
[admin]
No one logged on

If you have an extensive csh setup, there is a little script in the zsh distribution, Misc/c2z, which attempts to convert your csh setup to a suitable zsh one.

Fun with tildes

You've probably seen the notation that originated in csh to refer to a user's home directory: ~user. The zsh also supports this notion, but it extends it so that you can refer to arbitrary directories, not just home directories.

If you reference a shell variable using the tilde syntax, then zsh will expand it as though it was a username. However the difference is that it also remembers this fact. For this example, we'll change the prompt to show our current directory.

~% news=/usr/local/lib/news
~% cd ~news
~news% pwd
/usr/local/lib/news
~news% cd ..
/usr/local/lib% cd news
~news% 

You can gain access to this for your own purposes. For example, here is a small function to put the current working directory into the title bar of an xterm, substituting with tilde syntax where appropriate.

% function xtwd {
function> print -n "\033]2;$(print -D $PWD)\007"
function> }

If you call this function chpwd, then it will be executed every time you change directory.

Prompting

Like bash and tcsh, zsh supports a wide variety of substitution values for it's prompt. The default prompt looks like this.

hostname% echo $PROMPT
%m%# 

The percent-m substitutes the hostname, and the percent-hash substitutes a real percent for a non root user or a hash mark for a root user. The prompt I use is similiar, but includes the directory.

voodoo:~> echo $PROMPT
%m:%B%2.%b> 

The percent-big-b and percent-little-b combination stand for start bold and end bold respectively. The percent-dot means "print the last component of the working directory, with tilde substitution". Preceding it with a two, however, means print the last two components of the directory. This way, I do not get a hugely long path when I am inside a deeply nested directory.

Just to be different, zsh also lets you put a prompt on the right hand side of your screen. Here's an example of how to have the time on the right hand side of your screen.

% RPROMPT="[%t]"
%                                                                      [5:21PM]

As before, zsh lets you get at the percent expansion outside of the prompt. Here's the same xterm-directory-title function as before, but this time, using percent expansion to make it simpler.

% function xtwd {
function> print -Pn "\033]2;%~\007"
function> }

The print -P will let you expand percent sequences in what you print. The percent sequences are quite powerful, and allow you to specify things such as field widths and behave differently depending on whether it's a weekday or weekend. It's all in the manual...

Editing

Zsh comes with a very powerful line editor called zle (original, huh? :-). Zle allows complete editing of the command line in one of two modes: emacs or vi (something for everyone). By default it starts up in emacs mode, which is what you'll probably want to use, because the arrow keys on your keyboard will most likely work correctly. The editing is fairly similiar to the editing available in bash and tcsh, so I'll just describe some extensions that I find useful here. I'm afraid I don't use vi-mode so I don't know what the equivalent bindings are for the features I describe.

When I say M-a or "Meta a", I mean that you should press ESC, then a. If you have a Windows key on your keyboard, then it may well be set up as a meta key in which case you can just press windows-a. Some lucky souls will even have a key labelled meta. However most won't, so pressing ESC is the portable way to do it. If you do have a Meta key and it doesn't work, try doing bindkey -m and see if it does.Much more simply, when I say C-a, I mean control-a.

One of the most useful keybindings that a lot of people don't realise is M-q. If you get half way through typing a long command and you suddenly need to look at the man page to remember it's options (it always happens with compctl to me), then press M-q and your command will be squirrelled away whilst you can type in another one. When that second command has completed, you will find yourself back in the middle of the first command where you left off.

% gnuclient /usr/local/lib/xemacs/site-lisp/fred.elM-q
% echo $DISPLAY
bighost:0.0
% gnuclient /usr/local/lib/xemacs/site-lisp/fred.el

Of course, if all you want to do is look at the man page for a command, then try pressing M-h. It calls a zle function run-help, which just brings up the man page for the command you're editing. And when you've finished looking at the man page it puts you back at the prompt.

One common thing that is seen in csh is people tying in !$ to recall the last argument on the previous line. In zsh, M-. will do the same thing. However, if repeated, it will bring back the last argument of the previous command, and so on.

% cat /etc/networks
% ls -l /etc/hosts
% vi M-.
% vi /etc/hosts M-.M-.
% vi /etc/hosts /etc/networks

To find a the last command that started with a certain word, just type in the first couple of letters and press M-p. To do an interactive search across all of the history, use C-r, just as in bash.

Once occasionally handy feature in zle is undo. If you've just pressed M-d by accident instead of M-f to go forwards, you can press C-x then C-u, the same as in emacs, to undo your change.

Lastly, one of the the things that makes life easier. Zle adds a built-in command to zsh called vared. If you type in (for example) vared PATH, you will get put into an editing buffer where you can interactively edit the contents of that variable. I like to do this for when I connect to my FreeBSD box from a sun box with the wrong hostname. Then, I can just do vared DISPLAY and correct the problem.

If you are used to the csh's mechanism for history, you can still use that too, with a couple of very minor changes (you can make it even more compatible with setopt cshjunkiehistory). Although I prefer to turn it off with unsetopt banghist, so I can type in commands with exclamation marks in them.

Completion

One the most useful areas of zsh is completion. Zsh can complete practically anything that isn't a random number. As in bash and tcsh, you have basic completion available on the press of the TAB key. The default is unlike bash, in that it does not automatically display a list of completions when you give an ambiguous choice. You can activate this by doing setopt autolist, although I prefer to cycle through the list of choices on the command line instead (use setopt automenu).

Something I find useful is that zsh will attempt to complete wildcards by expanding them in the command line. This is handy if you want to edit out a couple of the results. eg:

% vi *[0-9]TAB
% vi fred1 fred2 fred3 fred4

As in bash, zsh will complete variables, function names, file names, command names and user names. It doesn't complete host names by default, although it can be made to do so. Zsh can also complete other things such as just aliases, just functions, shell options, just array variables, just scalar variables. However, you do need to tell it what you want completed. This leads on to one of the best features of zsh; programmable completion. This will be familiar to users of tcsh. In fact, there is a utility in the zsh distribution (Misc/lete2ctl) to convert your tcsh complete statements to zsh compctl statements.

As a simple example, let's make setopt and unsetopt complete their options. The command used is compctl and it takes about 3 billion options, but we can start simply.

% compctl -o setopt unsetopt
% setopt rmsTAB
% setopt rmstarsilent

Other basic flags include -f to complete files, -v to complete variables, -j for jobs and -u for user names. You can do more, though. You can complete with a glob by using -g or from a list of values in an array by using -k. Here are a few that I use.

Completions can be "prioritised". Here, I've rearranged the ghostview completion such that it completes any file if there isn't a .ps file in the current directory.

% compctl -g '*.ps' + -f ghostview

Even with all of this, it's not always quite enough. The next stage in programmable completion is extended completion. There's a heck of a lot you can do with this, so please, check the manual for details. Anyway, here's what I use to do completion with dd.

% compctl -x 's[if=] , s[of=]' -f -- dd

This says that if the current argument starts with either "of=" or "if=", then complete filenames immediately.

If extended completion still isn't enough, you can write a shell function to return whatever values you would like to complete.

There are lots of examples of using compctl in the zsh distribution in Misc/compctl-examples. It's well worth reading through this file to see quite what you can achieve with this remarkably flexible mechanism.

Miscellaneous

One of the most common complaints about zsh is that expansion doesn't work the same way as in the other shells. If you really want it to, then you can do setopt shwordsplit. However, you should really be using arrays instead, as shown below.

% srcs='fred.c barney.c'
% ls -l $srcs
ls: fred.c barney.c: No such file or directory
% srcs=(fred.c barney.c)
% ls -l $srcs
-rw-r--r--  1 dom  ho  97645 Aug 20 13:22 barney.c
-rw-r--r--  1 dom  ho   6734 Aug 20 13:22 fred.c

If you're used to the spelling correction in tcsh, you can have that in zsh, too.

% setopt correct
% la
zsh: correct 'la' to 'ls' [nyae]? y
file1 file2
% 

You can turn on spelling correction for all arguments as well as just the command with setopt correctall.

A nice little feature is called process substitution. It is easiest to demonstrate with an example.

% mail -f =(zcat ~/mail/sent.old.gz)
Mail version 8.1 6/6/93.  Type ? for help.
"/tmp/zshv51809": 1677 messages 1677 new
>N  1 dom@phmit.demon.co.u  Mon Jun 21 11:31  86/3370  "Re: Veritas"
 N  2 dom@phmit.demon.co.u  Mon Jun 21 11:31  20/597   "nofingerd"
 N  3 dom@phmit.demon.co.u  Mon Jun 21 11:31  44/2140  "utime.tar"
...
& q
"/tmp/zshv51809" complete

This is similiar to the <(proc) available in bash, but it creates a regular file instead of a fifo. This is necessary for programs that need to seek(2) on their input, such as mail(1). Of course, the fifo notation is also available in zsh.

Zsh can automatically set up tees for you. This example creates two files, both containing the find output.

% find . -name '*.[ch]' -print >list >../list.backup

Conclusion

Firstly, I must apologise for the somewhat rambling and lengthy nature of this discourse, but shells have always interested me.

Anyway, I hope this has piqued your interest in zsh. Please, give it a try!

Credits

Many thanks to Paul Falstad and all the volunteers who have put so much time into making zsh the marvel it is today.

Links