Saturday, August 8, 2009

The case for zsh

A few months back, in January or February I decided to switch to zsh as default shell and it has made my work a lot more effective. So I encourage you to try it, it has a number of features that are quite useful. Towards the bottom of this post is my own setup, feel free to use it.

Disclaimer: some or all of the features below are probably available in other shells. This is not a "$SHELL is so much better than $OTHERSHELL" posting, this is about how a particular setup has made my work more effective.

The main features I found useful, in no particular order:

  • history size of 5000 with duplicate removal means I type most commands now with Ctrl+R. Most of what I do is repetitive enough that if I have typed some weird command a few months back it will still be in the history.

  • merged histories. ever had 15 terminals open and then found out that the history of one is not available in the others, and on closing only the last one is added to the history? not a problem anymore.

  • commandline completion - just beautiful. includes host completion for ssh commands, man page completion, rpm and CVS module completion, git command/tag/branch completion, etc.

  • completion exclusion: if you type rm foo.c it won't suggest foo.c again since it's already in the list.

  • app-specific completion. You can simply add filetypes to complete for your program (e.g. only pdfs for the pdf reader, etc.)

  • vim/emacs key bindings. whatever you fancy. It's nice to use the vim commands for delete word, replace word, etc. Especially for multi-line commands.

  • git branch display - one of the scripts makes my prompt display the git branch if i'm in a git directory. since I frequently work with 5+ branches, that's really handy. So for example, my prompt looks like this:

    :: whot@dingo:~/xorg/xserver (xi2-protocol-tests)>

    indicating that the xserver repo is on branch xi2-protocol-tests. It also displays whether I have commits queued up or local changes, so I don't forget to commit something before pushing. Type disable-git-prompt to disable this again if your repo is _really_ big (e.g. the kernel), otherwise it takes forever to get the prompt to display.

  • "GUI" selection for tab-completion. hit Tab and below the line you get a list of all files and you can go through with them using Tab. Like this:

So anyway, have a look at my zsh files and use them as you will. Save them as $HOME/.zshrc and $HOME/.zsh/ to get started.


Simon Geard said...

Nice enough, though aren't most of those features also standard in bash? The completion stuff, for example - I've always found bash's programmable completion to be powerful enough to do anything I've ever wanted..

Allan "Goldfish" Clark said...

Your immediate/early ack that it's not a $SHELL/OTHER argument benefits your clear advertisement for the benefits this gives you. Even the bullet-point format seems perfect.

It took a lot to ditch ksh for bash, is it time to consider changing?

How's the overhead? Lots of resident seg memory used? a lot of libs dependency? What's the chance to have a stripped-down zsh on an embedded host?

I saw your article by following felipec, who is slowly dragging me to the git-side. Interesting to see a shell with such enhancements.


Dnas The Great said...

Hrm, was completion exclusion added in a newer version? I don't appear to have it (zsh 4.3.6 and zsh 4.3.9) and I don't see mention of it in your dotfiles.

Peter Hutterer said...

Simon: could be, bash doesn't seem to do much beyond path-completion though. everything else seems to require some setup I don't know how to do. zsh comes with by default.

Peter Hutterer said...

I don't really know. I don't have to deal with embedded systems and overhead isn't something I really need to worry about on a quite beefy machine.
FWIW, a friend changed from tcsh and is quite happy now.

zstyle ':completion:*:(rm|kill|diff):*' ignore-line yes

in the .zshrc file, my version is 4.3.9

Simon Geard said...

@Peter - true, most of the advanced bash completion features aren't enabled by default. For anyone willing to spend the time setting them up though, it has a variety of built-in completions (including hostnames from /etc/hosts), and where that fails, programmable completions can be implemented either in shell functions or as external commands.

Still, while I'm happy with bash, sounds like it might be worth at least taking a look at zsh...

Unknown said...

The most important zsh feature for me (and there are lots I've been using it for nearly 15 years) is the history-beginning-search.

Bind the up/down keys:
bindkey "^[[A" history-beginning-search-backward
bindkey "^[[B" history-beginning-search-forward
bindkey "^[OA" history-beginning-search-backward
bindkey "^[OB" history-beginning-search-forward

Then up/down behaves like an isearch anchored to the start of the line. Ctrl-r is useful sometimes as well but an anchored search is much better. Simply type a couple of characters then up/type some more till you get the command. As you say 5000 line history is fully usable in a few key presses.

The unusual thing about this setup is that when you go up the cursor stays at the start of the line not the end (ctrl-e with emacs bindings for that). This freaked me out for the first few days but after then I can't go back.

Quentin Casasnovas said...

Also, if you're always in front of your shell, it's nice to have colors everywhere. When you use bash, colors are such a pain to configure, with zsh just put this in your .zshrc :
autoload -U colors
There we go, we can color everything (let's say our promp) using arrays like $bg[black], $fg_bold[red].
Last tip, and here I'm ready to troll if people want to, a REAL RIGHT PROMPT, no dirty hack to have a nice, right-aligne, prompt : just export RPS1 as you would do with PS1 (The prompt is hidden when your command goes too far, try to do that with bash ;)
Here is my RPS1, which prints a green 0 if last command was successfull, and the error number, in red, if it failed.
export RPS1="%{%0(?,$fg_bold[green],$fg_bold[red])%}%?%{$reset_color%}"


PS: Thank you for your great articles, especially those about Xi2 :)

Peter said...

This actually seems nice.. Hmm. It feels a bit scary to switch shell though.

Could you do a post (or someone else point me to one) where someone shows the way to do this with bash? For instance "Simon" above, which says it's doable, without any links or references.

Anonymous said...

@Quentin: That RPS1 hack to display command exit value is genius, thanks for that!

Great post, Simon, glad to see your productivity has benefited so greatly from switching to zsh.

I admit I don't use a lot of the enhanced interaction functionality; at the time I switched to zsh (at least 8 years ago, perhaps longer) my hardware just wasn't up to the task of handling such compute-intensive features. To this day my .zshrc contains a note above the commented-out "compinit" invocation, recommending I "NEVER EVER ENABLE THIS UNTIL CRAY RELEASES N2-COOLED DESKTOP". (...Date myself much?)

But even without prescient completion and command-line clairvoyance turned on, zsh puts a lot of power at your fingertips if you're a heavy shell user. I'm still discovering new tricks.

Here are some of my favorites:


ls ~/devel/myproject/**/*.c
The ** glob matches directories to any depth.

ls /media/Video/**/*(.u[ferd]mw+6LM+100)
The glob matches anything under /media/Video that's:
(.) - a regular file
(u[ferd]) - owned by user ferd
(mw+6) - last modified more than 6 weeks ago
(LM+100) larger than 100MB.
...Almost scary.

Zsh's globbing makes find(1) practically obsolete. Of the 2801 lines in my .zsh_history, there are only 4 instances of my using the find command.


sudo yum install system-config-{network,httpd,printer,samba}
Self-explanatory -- {} lists are great when you need to provide several mostly-similar args to a command.

rpm -qf =mysterycommand
A simple time-saver with "setopt EQUALS". Much more convenient than `whence blah` to pass the filesystem location for a command in the path.

for file in ~/Pictures/**/*.(jpg|gif); do convert $file "${file%.*}.png"; done
Create png versions of all jpg or gif images in ~/Pictures, using ImageMagick's convert(1). The odd-looking second reference to $file applies a pattern-substitution that removes the portion of its contents matching ".*", anchored to the end of the string.

Anonymous said...

Er, s/Simon/Peter/, in my previous comment. And if that's the only mistake it contains, I'll consider myself extremely lucky. :)