chris blogs: Zsh

27feb2017 · A time-proven zsh prompt

I’ve been using below shell prompt since 2013 and only slightly tweaked it over time. The most significant change was probably displaying the Git branch.

The basic idea of my prompt is to not show redundant or obvious information. This allows the prompt to be short, yet useful.

By default, the prompt displays the hostname, shortened directory, and a % to signify a zsh. The hostname is bold to make it stand out when you are scrolling, and the sigil is colored to mark the beginning of the command. It looks like this:

juno ~% ./mycommand -x

Long directory names are truncated in the middle:

juno /tmp/dirwithare…gname%

In rare cases, only showing two levels of hierarchy may be confusing, so you can set $NDIRS to something higher, e.g. 4:

juno deeply/nested/dir/structure%

When the previous command failed, the prompt also displays the exit status of the previous command:

juno 42? ~%

When there are background jobs running, the prompt shows how many there are:

juno 1& ~%

Note how the status and job display use the associated ASCII symbols.

When we are in a Git repository, the current branch is displayed inline as part of the base directory (when possible), or as a prefix, together with the repo name. By design, in the most common cases this keeps the prompt very short:

juno prj/rack@master%
juno rack@master/doc%
juno rack@master doc/Rack%

When the prompt detects a SSH session, the prompt sigil is doubled, so we are a bit more careful there:

hecate prj/lr%%

When the shell runs as root, the sigil is red (I don’t usually run zsh as root):

juno /etc#

That’s it, essentially. Apart from the Git integration, it’s really straight-forward. Not visible above is trick 4 to simplify pasting of old lines, and how it updates the title of terminal emulators to hostname: dir respectively hostname: current-command (which needs quite complicated quoting).

The whole thing is defined in the PROMPT section of my .zshrc.

NP: Light Bearer—Aggressor & Usurper

02jan2017 · zz: a smart and efficient directory changer

A nice feature I’ve become used to in the last year is a so-called “smart directory changer” that keeps track of the directories you change into, and then lets you jump to popular ones quickly, using fragments of the path to find the right location.

There is quite some prior art in this, such as autojump, fasd or z, but I could not resist building my own implementation of it, optimized for zsh.

As far as I can see, my zz directory changer is the only one with a “pay-as-you-go” performance impact, i.e., not every directory change is slowed down, but only every use of the smart matching functonality.

The idea is pretty easy: we add a chpwd hook to zsh to keep track of directory changes, and log for each change a line looking like “0 $epochtime 1 $path” into a file ~/.zz. This is an operation with effectively constant cost on a Unix system.

chpwd_zz() {
  print -P '0\t%D{%s}\t1\t%~' >>~/.zz
chpwd_functions=( ${(kM)functions:#chpwd?*} )

The actual jumping function is called zz:

zz() {

How does the matching work? It’s an adaption of the z algorithm: The lines of ~/.zz are tallied by directory and last-used time stamp, so for example the lines

0 1483225200 1 ~/src
0 1483225201 1 ~/tmp
0 1483225202 1 ~/src
0 1483225203 1 ~/tmp
0 1483225204 1 ~/src

would turn into

6 1483225204 3 ~/src
4 1483225203 2 ~/tmp

Also, the initial number, the effective score of the directory, is computed: We take the relative age of the directory (that is, seconds since we went there), and boost or dampen the results: the frequency is multiplied by 4 for directories not older than 1 hour, doubled for directories we went into today, halved for directories we went into this week, and divided by 4 else.

  awk -v ${(%):-now=%D{%s}} <~/.zz '
    function r(t,f) {
      age = now - t
      return (age<3600) ? f*4 : (age<86400) ? f*2 : (age<604800) ? f/2 : f/4
    { f[$4]+=$3; if ($2>l[$4]) l[$4]=$2 }
    END { for(i in f) printf("%d\t%d\t%d\t%s\n",r(l[i],f[i]),l[i],f[i],i) }' |

By design, this tallied file can be appended again with new lines originating from chpwd, and recomputed whenever needed.

The output of this tally is then sorted by age, truncated to 9000 lines, then sorted by score. (My ~/.zz is only 350 lines, however.)

      sort -k2 -n -r | sed 9000q | sort -n -r -o ~/.zz

With this precomputed tally (which is generated in linear time), finding the best match is easy. It is the first string that matches all arguments:

  if (( $# )); then
    local p=$(awk 'NR != FNR { exit }  # exit after first file argument
                   { for (i = 3; i < ARGC; i++) if ($4 !~ ARGV[i]) next
                     print $4; exit }' ~/.zz ~/.zz "$@")

If nothing was found, we bail with exit code 1. If zz is used interactively, it changes into the best match, else the best match is just printed. This allows using things like cp foo.mkv $(zz mov).

    [[ $p ]] || return 1
    local op=print
    [[ -t 1 ]] && op=cd
    if [[ -d ${~p} ]]; then
      $op ${~p}

If we found a directory that doesn’t exist anymore, we clean up the ~/.zz file, and try it all over.

      # clean nonexisting paths and retry
      while read -r line; do
        [[ -d ${~${line#*$'\t'*$'\t'*$'\t'}} ]] && print -r $line
      done <~/.zz | sort -n -r -o ~/.zz
      zz "$@"

With no arguments, zz simply prints the top ten directories.

    sed 10q ~/.zz

I actually shortcut zz to z and add a leading space to not store z calls into history:

alias z=' zz'

The full code (possibly updated) can be found as usual in my .zshrc.

I use lots of shell hacks, but zz definitely is among my most successful ones.

NP: Leonard Cohen—Leaving The Table

19feb2015 · Six hacks for less(1)

Recently I got around to configuring less, and I collected these few tricks:

  1. Sometimes I look at lists with less, and then do things step-by-step, keeping the current action at the top of the page. This works nicely until you end up at the last page of the file, and then can’t scroll down. You lose track of where you are at and get confused.

    It would be much nicer scrolling down, and filling up the buffer with ~ after the end of file, just as if you had searched in the pager.

    Actually, with ESC-SPC, you can move a full page down, filling up the buffer with ~. Toying around a bit, you’ll find out that you can override the “page length” with a prefix, i.e. 1 ESC-SPC will move down one line only!

    However, this is still inconvenient to type all the time, thus let’s define a keybinding. For this, create a file ~/.lesskey where we will put the key definitions. This file then will be compiled using lesskey(1) and generate a binary configuration file ~/.less. (I guess you can be lucky that m4 is not involved in this mess…)

    One problem is actually binding the key. You can easily bind the cursor down key (\kd) to forw-screen-force, but how do you pass 1? The canonical hack is to use the noaction action, which will behave just like you’ve typed the keys after it. Thus, we write:

    \kd noaction 1\e\40
    j noaction 1\e\40

    (By the way, that #command comment is important to tell lesskey you are defining key commands.)

    Finally, scrolling bliss!

    Actually, scrap that.

    The badly underdocumented key J (and K) will scroll how I want, but you only read about that in the example inside lesskey(1). Therefore, we can just do:

    \kd forw-line-force
    j forw-line-force

    These keybindings are there since at least 1997 and I’ve never found them before…

  2. While we are redefining keys, I’ve always found it a bit clumsy to read multiple files, having to type :n and :p. Using [ and ] is much more convenient (at least on a US keyboard), and by default these keys do things of questionable utility.

    [ prev-file
    ] next-file
  3. Did you ever wish to give feedback from less? Like have a script output some info, and you decide how to go on? Since less always exits with status 0 usually, this I thought this was tricky to do, but the quit action actually can return an arbitrary exit code, encoded as a character.

    I bound Q and :cq (like in vim) to exit with status 1:

    Q quit \1
    :cq quit \1

    Now you can do stuff like look at all files and have them deleted when you press Q instead of q to exit:

    for f in *; do less $f || rm $f; done
  4. I use less a lot to look at patches, git log output, and ocassionally mailboxes. The D command as defined below will move to the next line starting with diff or commit or From␣.

    D noaction j/\^diff|commit|From \n\eu

    It will also “type” ESC-u to hide the highlighting. Now I can simply press D to jump to the next chunk of interest.

  5. To return to where you started from after a search or going to the end of file, type ''. Typing '' again will go back, so this is also nice to toggle between two search results.

  6. Back in the old days of X11R2(?) there was a tool called xless, which was exactly that: a pager like less that ran in its own X11 window. It’s quite useful. We can recreate this by combining a X11 terminal emulator and plain less with a small zsh snippet:

    xless() {
        exec {stdin}<&0 {stderr}>&2
        exec urxvt -e sh -c "less ${(j: :)${(qq)@}} </dev/fd/$stdin 2>/dev/fd/$stderr"
      } &!

    Watch the trick how we pass the stdin/stderr file descriptors and the file arguments!

    Now you can just run command-spitting-out-loads | xless and the output will be shown in a new terminal and not lock your shell.

    NP: Feine Sahne Fischfilet—Dreieinhalb Meter Lichtgestalt

17feb2015 · 10 fancy zsh tricks you may not know...

Wow, almost two years have passed since the latest installment of our favorite clickbait zsh tricks series.

  1. When editing long lines in the zle line editor, sometimes you want to move “by physical line”, that is, to the character in the terminal line below (like gj and gk in vim).

    We can fake that feature by finding out the terminal width and moving charwise:

    _physical_up_line()   { zle backward-char -n $COLUMNS }
    _physical_down_line() { zle forward-char  -n $COLUMNS }
    zle -N physical-up-line _physical_up_line
    zle -N physical-down-line _physical_down_line
    bindkey "\e\e[A" physical-up-line
    bindkey "\e\e[B" physical-down-line

    Now, ESC-up and ESC-down will move by physical line.

  2. Sometimes it’s nice to do things in random order. Many tools such as image viewers, music or media players have a “shuffle” mode, but when they don’t, you can help yourself with this small trick:


    Just append ($SHUF) to any glob, and get the matches shuffled:

    % touch a b c d
    % echo *($SHUF)
    d c a b
    % echo *($SHUF)
    c a d b

    Note that this shuffle is slightly biased, but it should not matter in practice. In doubt, use shuf or sort -R or something else…

  3. Are you getting sick of typing cd ../../.. all the time? Why not type up 3?

    up() {
      local op=print
      [[ -t 1 ]] && op=cd
      case "$1" in
        '') up 1;;
        -*|+*) $op ~$1;;
        <->) $op $(printf '../%.0s' {1..$1});;
        *) local -a seg; seg=(${(s:/:)PWD%/*})
           local n=${(j:/:)seg[1,(I)$1*]}
           if [[ -n $n ]]; then
             $op /$n
             print -u2 up: could not find prefix $1 in $PWD
             return 1

    With this helper function, you can do a lot more actually: Say you are in ~/src/zsh/Src/Builtins and want to go to ~/src/zsh. Just say up zsh. Or even just up z.

    And as a bonus, if you capture the output of up, it will print the directory you want, and not change to it. So you can do:

    mv foo.c $(up zsh)
  4. Previous tricks (#6/#7) introduced the dirstack and how to navigate it. But why type cd -<TAB> and figure out the directory you want to go to when you simply can type cd ~[zsh] and go to the first directory in the dirstack matching zsh? For this, we define the zsh dynamic directory function:

    _mydirstack() {
      local -a lines list
      for d in $dirstack; do
        lines+="$(($#lines+1)) -- $d"
      _wanted -V directory-stack expl 'directory stack' \
        compadd "$@" -ld lines -S']/' -Q -a list
    zsh_directory_name() {
      case $1 in
        c) _mydirstack;;
        n) case $2 in
             <0-9>) reply=($dirstack[$2]);;
             *) reply=($dirstack[(r)*$2*]);;
        d) false;;

    The first function is just the completion, so cd ~[<TAB> will work as well.

  5. Did you ever want to move a file with spaces in the name, and mixed up argument order?

    % mv last-will.tex My\ Last\ Will.rtf

    Pressing ESC-t (transpose-words) between the file names will do the wrong thing by default:

    % mv My last-will.tex\ Last\ Will.rtf

    Luckily, we can teach transpose-words to understand shell syntax:

    autoload -Uz transpose-words-match
    zstyle ':zle:transpose-words' word-style shell
    zle -N transpose-words transpose-words-match


    % mv My\ Last\ Will.rtf last-will.tex
  6. If you are an avid Emacs user like me, you’ll find this function useful. It enters the directory the currently active Emacs file resides in:

    cde() {
      cd ${(Q)~$(emacsclient -e '(with-current-buffer
                                   (window-buffer (selected-window))
                                   default-directory) ')}

    You need the emacs-server functionality enabled for this to work.

  7. I’m working on many different systems and try to keep a portable .zshrc between those. One problem used to be setting $PATH portably, because there is quite some difference among systems. I now let zsh figure out what belongs to $PATH:

    export PATH
    path=( ${(u)^path:A}(N-/) )

    The last line will normalize all paths, and remove duplicates and nonexisting directories. Also, notice how I pick up the latest Ruby version to find the Gem bin dir by sorting them numerically.

  8. One of the hardest things is to set the xterm title “correctly”, because most people do it wrong in some way, and then it will break when you have literal tabs or percent signs or tildes in your command line. Here is what I currently use:

    case "$TERM" in
        precmd() {  print -Pn "\e]0;%m: %~\a" }
        preexec() { print -n "\e]0;$HOST: ${(q)1//(#m)[$'\000-\037\177-']/${(q)MATCH}}\a" }
  9. For a cheap, but secure password generator, you can use this:

    zpass() {
      LC_ALL=C tr -dc '0-9A-Za-z_@#%*,.:?!~' < /dev/urandom | head -c${1:-10}
  10. Sometimes it’s interesting to find a file residing in some directory “above” (e.g. Makefile, .git and similar). We can glob these by repeating ../ using the #-operator (You have EXTENDED_GLOB enabled, right?). This will result in all matches, so let’s first sort them by directory depth:

    % pwd
    % print -l (../)

    Now we can pick the first one, and also make the file name absolute:

    % print (../)[1]:A) 

    I knew the #-operator, but it never occurred to me to use it this way before.

    Until next time!

    NP: Pierced Arrows—On Our Way

12mar2013 · 10 fresh zsh tricks you may not know...

Time for a new instance of your favorite installment of this blog!

  1. You probably know M-. to insert the last argument of the previous line. Sometimes, you want to insert a different argument. There are a few options: Use history expansion, e.g. !:-2 for the third word on the line before (use TAB to expand it if you are not sure), or use M-. with a prefix argument: M-2 M-.

    Much nicer however is:

    autoload -Uz copy-earlier-word
    zle -N copy-earlier-word
    bindkey "^[m" copy-earlier-word

    Then, M-m will copy the last word of the current line, then the second last word, etc. But with M-. you can go back in lines too! Thus:

    % echo a b c
    % echo 1 2 3
    % echo <M-.><M-.><M-m>
    % echo b

    Man, I wish I knew that earlier!

  2. Sometimes, you want to combine a few previously typed lines into one:

    % vi foo.c
    % gcc -o foo foo.c
    % gdb --args foo -v

    Repeating these three commands all the time gets annoying (even if you know about C-o, it’s messy if you run other commands in between).

    So, lets combine them into one line. Are you moving your hand to your mouse for copy&pasting? Bzzzt!

    We can use history expansion as well:

    % !-3<TAB>; !-2<TAB>; !-1<TAB>
    % vi foo.c; gcc -o foo foo.c; gdb --args foo -v

    If you don’t see the numbers easily, you could do !vi<TAB> and so on. I always wanted to use C-r for this, however, thus I defined:

    autoload -Uz narrow-to-region
    function _history-incremental-preserving-pattern-search-backward
      local state
      MARK=CURSOR  # magick, else multiple ^R don't work
      narrow-to-region -p "$LBUFFER${BUFFER:+>>}" -P "${BUFFER:+<<}$RBUFFER" -S state
      zle end-of-history
      zle history-incremental-pattern-search-backward
      narrow-to-region -R state
    zle -N _history-incremental-preserving-pattern-search-backward
    bindkey "^R" _history-incremental-preserving-pattern-search-backward
    bindkey -M isearch "^R" history-incremental-pattern-search-backward
    bindkey "^S" history-incremental-pattern-search-forward

    Now C-r will work in a recursive way. It looks like this:

    % <C-r>vi<RET>;
    % vi foo.c; <C-r>gcc
    % vi foo.c; >>gcc -o foo foo.c<< <RET>;<C-r>gdb
    % vi foo.c; gcc -o foo foo.c; >>gdb --args foo -v<< <RET>
    % vi foo.c; gcc -o foo foo.c; gdb --args foo -v

    Since C-r is not very useful if you already typed something, I feel this redefinition is quite neat.

  3. I have defined $WORDCHARS to exclude / since I usually don’t want C-w or M-DEL to remove whole paths. But sometimes I do, thus:

    function _backward_kill_default_word() {
      WORDCHARS='*?_-.[]~=/&;!#$%^(){}<>' zle backward-kill-word
    zle -N backward-kill-default-word _backward_kill_default_word
    bindkey '\e=' backward-kill-default-word   # = is next to backspace
  4. You probably heard of prompts like : juno ~/src/zsh ; (or just ; if you are hardcore-minimalist) which have the benefit that you can copy the whole line in your terminal emulator and just paste it to run it again:

    : juno ~/src/zsh ; grep -r bindkey .
    : juno ~/src/zsh ; : juno ~/src/zsh ; grep -r bindkey .

    The : will nicely gobble up (almost) everything until the ;. But the paste keeps repeating the prompt, which is ugly, and I don’t like to have the : and ; in my prompt really. That’s why I had the IMHO ingenious idea to set my prompt to:

    juno src/zsh% 

    … which actually is:

    juno src/zsh%␣

    That is, the last character is a Unicode non-breaking space (U+00A0). Which will look like a plain space and behave like one. Except that we can bindkey it to clear the input buffer:

    bindkey -s $nbsp '^u'

    Now, pasting the prompt will make it remove itself. Great!

  5. Growing up with bash, I never was fond of zsh’s menu-select widget. I want TAB to complete as much as possible, and if I ever press TAB again, it should display the completions and just let me type on.

    However, sometimes, I’d like to use the menu-select widget, e.g. if the files have prefixes that make selection hard (Maildirs, anyone?) or consist of weird special chars only.

    It took me quite long to figure out how to enable menu-select for certain widgets only. The main problem was that the option NO_ALWAYS_LAST_PROMPT disables the menu widget. Thus, we have to unset it locally:

    zle -C complete-menu menu-select _generic
    _complete_menu() {
      setopt localoptions alwayslastprompt
      zle complete-menu
    zle -N _complete_menu
    bindkey '^F' _complete_menu
    bindkey -M menuselect '^F' accept-and-infer-next-history
    bindkey -M menuselect '/'  accept-and-infer-next-history
    bindkey -M menuselect '^?' undo
    bindkey -M menuselect ' ' accept-and-hold
    bindkey -M menuselect '*' history-incremental-search-forward

    The latter keybindings make it a convenient file selector and browser (using / and DEL).

  6. One thing that zsh lacks is the ability to start an interactive shell session from inside a shell script (e.g. rc(1) can do that). Sometimes you want to spawn a shell that runs a command, but is interactive after the program finished (e.g. when launched from urxvt or tmux).

    Luckily, I found zshi. I have defined it as a script to allow execution from everywhere. It needs some support from the .zshrc:

    if [[ $1 == eval ]]; then
      set --
      zle-line-init() {
        zle accept-line
        zle -D zle-line-init
      zle -N zle-line-init

    That solution is a bit more complex, but it allows you to press Up (or run r) to execute the command again.

  7. If you are watching series, you want to get to the next episode:

    % mplayer foobar-S01-E23.mkv
    % <Up><C-x a>
    % mplayer foobar-S01-E24.mkv

    zsh includes incarg, but it only works if you put the cursor on the number. This solution increments the last number, anywhere, and knows about zero padding:

    _increase_number() {
      local -a match mbegin mend
      [[ $LBUFFER =~ '([0-9]+)[^0-9]*$' ]] &&
        LBUFFER[mbegin,mend]=$(printf %0${#match[1]}d $((10#$match+${NUMERIC:-1})))
    zle -N increase-number _increase_number
    bindkey '^Xa' increase-number
    bindkey -s '^Xx' '^[-^Xa'
  8. I use Emacs keybindings, but sometimes I wish I had vi’s command mode. Luckily, it’s just a C-x C-v away in the default configuration! Heck, you may even go ahead and do:

    bindkey '^[' vi-cmd-mode

    … and i will put you back into Emacs mode again.

  9. A great anti-feature of history expansion is when it fails:

    % a carefully constructed command line !?gcc !?vim !?quux
    zsh: no such event: gcc !<Up>
    % a carefully constructed command line im !?quux

    And your history expanders are gone. Not so with this snippet:

    function _recover_line_or_else() {
      if [[ -z $BUFFER && $CONTEXT = start && $zsh_eval_context = shfunc
            && -n $ZLE_LINE_ABORTED
            && $ZLE_LINE_ABORTED != $history[$((HISTCMD-1))] ]]; then
        unset ZLE_LINE_ABORTED
        zle .$WIDGET
    zle -N up-line-or-history _recover_line_or_else
    function _zle_line_finish() {
    zle -N zle-line-finish _zle_line_finish

    This will keep the last line in all cases, allowing you to fix it:

    % a carefully constructed command line !?gcc !?vim !?quux
    zsh: no such event: gcc !<Up>
    % a carefully constructed command line !?gcc !?vim !?quux
  10. Renaming long file names sucks. Many use graphical file managers for it. I use imv (interactive mv):

    imv() {
      local src dst
      for src; do
        [[ -e $src ]] || { print -u2 "$src does not exist"; continue }
        vared dst
        [[ $src != $dst ]] && mkdir -p $dst:h && mv -n $src $dst

    It will even create the target directory if it doesn’t exist.

  11. Bonus item: This is more for fun than serious use. An updating clock in your prompt:

    _prompt_and_resched() { sched +1 _prompt_and_resched; zle && zle reset-prompt }
    PS1="%D{%H:%M:%S} $PS1"

    As usual, these things and many others are integrated in my .zshrc. Enjoy your Z shell!

    NP: Silly—EKG

06jan2013 · A grab bag of Git tricks

Since its release I’ve been a fan of Git. (I still can remember downloading the initial version.) The thing I like most is that it can be extended and customized in an unixy way. Over time, I have collected some scripts and tricks that I would like to present to a wider audience. Git information online abounds (I especially recommend Mark J. Dominus in-depth posts on Git), thus I will only show stuff I haven’t seen elsewhere.

git news

Let’s start with a simple alias which you can simply add to your .gitconfig:

        news = log -p HEAD@{1}..HEAD@{0}

I am tracking quite a lot of open source projects by cloning them into ~/src and running git pull on them occasionally. Next, I run git news and see only the commits (with diff) that have arrived since the last pull.

Of course it is a very simplistic alias and it probably won’t do what you want if you actually change the HEAD yourself—e.g. by committing. (A more robust version could, for example, parse the output of git reflog and search for the last pull.) On the other hand, as it is, it also can be useful for showing what came in with a merge. I also use it for repositories where I git cvsimport into, with the same benefits.

git comma

Admittedly, I’m a fan of dirty working trees, which is why—when I don’t use magit or finely-grained git add -p/git commit -p already—I commit whole files at once like git commit foo.c bar.c.

One thing that has always annoyed me is that I cannot git commit files unknown to Git, enforcing an explicit git add step only for these new files! One day I took the plunge and wrote git-comma (a portmanteau of commit and add) which gives its best to behave exactly like git commit except for adding the yet-unknown files beforehand. This was a bit more tricky than I expected because I wanted it to work correctly even in the face of partially staged files, thus a stupid git add on all arguments would not work (also, you only want to add explicitly named files, not whole directories and so on). Finally, git comma tries to clean up properly if you decide to abort the commit, unstaging the files again.

(IMO, this should be a flag or configuration option for git commit.)

git attic

A newer script, but a very useful one, is git attic, whose namesake perhaps gives you a shiver down the spine, being reminded of this CVS quirk.

Yet, CVS’ manner with deleted files—moving them into a folder called Attic—had one benefit which cannot be denied: it was easy to see what had been removed and to access the contents again.

Of course, Git has no problem with file removal, but having a look at the old contents can be laborious.

Thus I wrote git-attic, which presents you a nice list of files together with their deletion date:

% git attic
2012-08-14 441e782^:Etc/ChangeLog-5.0
2012-05-31 0793393^:Completion/Unix/Command/_systemctl
2012-01-31 6a364de^:Test/Y04compgen.ztst
2012-01-31 6a364de^:Test/compgentest
2011-08-18 f0eaa57^:Completion/Zsh/Command/_schedtool

The output is designed to be copy’n’pasted: Pass the second field to git show to display the file contents, or just select the hash without ^ to see the commit where removal happened.

(By default, I don’t detect renames, since I want to see which paths don’t exist anymore. If you are looking for “lost” content, feel free to pass -M to the script to detect renames and only show truly deleted files.)

A minimalist, yet powerful zsh prompt

As an avid zsh user for years, I have been using a simple but powerful shell prompt which looks like hecate src/zsh% for years (since 2010-02-11 actually, thanks to homegit, see below.) and ridiculed experiments to make the zsh prompt a kitchen sink. However, my Git usage grew and I started occasionally mixing up branches.

Thus I decided to grin and bear it and wondered how to make a minimalist nevertheless useful Git-enhanced prompt. One feature of my prompt was that it only shows the last few segments of the current working directory (usually 2, which is enough for me unless I need to work in some javaesque file labyrinth). One day I decided to integrate the current Git branch into these path segments. Now, my prompt looks like this:

hecate src/zsh@master% cd Doc

… and it actually sticks to the repository root:

hecate zsh@master/Doc% cd Zsh

When the level gets too deep, the branch and repository moves to the front:

hecate zsh@master Doc/Zsh%

The depth is still configurable:

hecate zsh@master Doc/Zsh% NDIRS=4
hecate src/zsh@master/Doc/Zsh%

I’ve quite come to like this presentation. Additionally, it also works with detached heads (useful when rebasing):

hecate src/zsh@master/Doc/Zsh% git checkout HEAD~42
hecate src/zsh@master~42/Doc/Zsh%

For free, you get some feedback when bisecting:

hecate ~/src/zsh@master% git bisect bad
hecate ~/src/zsh@bisect/bad% git bisect good HEAD~42
hecate ~/src/zsh@bisect/bad~21% git bisect good
hecate ~/src/zsh@bisect/bad~5% git bisect reset
hecate ~/src/zsh@master%

This is the code in all its glory:

# gitpwd - print %~, limited to $NDIR segments, with inline git branch
gitpwd() {
  local -a segs splitprefix; local prefix gitbranch

  if gitprefix=$(git rev-parse --show-prefix 2>/dev/null); then
    branch=$(git name-rev --name-only HEAD 2>/dev/null)
    if (( $#splitprefix > NDIRS )); then
      print -n "${segs[$#splitprefix]}@$branch "

  print "${(j:/:)${(@Oa)segs[1,NDIRS]}}"

Perhaps it turned out to be a bit more challenging than expected. ;) Integration into the prompt is trivial, however:

function cnprompt6 {
  case "$TERM" in
      precmd() {  print -Pn "\e]0;%m: %~\a" }
      preexec() { printf "\e]0;$HOST: %s\a" $1 };;
  PS1='%B%m%(?.. %??)%(1j. %j&.)%b $(gitpwd)%B%(!.%F{red}.%F{yellow})%#${SSH_CLIENT:+%#} %b'



For the last five years I have used Git to manage my dotfiles and I use the repository on a plethora of machines.

I found the following zsh alias to be the simplest and best method to use Git for this purpose:

alias homegit="GIT_DIR=~/prj/dotfiles/.git GIT_WORK_TREE=~ git"

Why not a function? Because an alias will make zsh autocomplete homegit just like it completes git already, without any additional work.

Why not a ~/.git? I decided against it because I didn’t want to accidentally commit stuff from any subdirectory and feared a git clean could wipe my sweet home directory.

The homegit approach works very well for me and I have not felt a need for more complex solutions which symlink dotfiles or copy them around.

Note that the git-* scripts presented here can be called transparently from homegit as well, e.g. with homegit attic. And since $GIT_DIR is set in the environment, the scripts can just call git and will just work correctly!

411 commits as of now tell me I perhaps should scale back customizing stuff all the time, but it can be very helpful indeed to see how things changed over time. Also, tracking changes other programs make to your files (and being able to revert them) is totally worth it.

git trail

One of the newest additions to my Git zoo is git trail, a tool I wanted for years, really. With many branches, it’s easy to get confused about what branched off where and what actually is part of this topic branch and whether this topic branch has been merged but then forgotten or…

Perhaps you feel my pain. Perhaps you tried git show-branch once to get an overview of such a mess, but I feel it’s easier to see stereographic projections of a T-Rex in its output than the state of your branches.

Thus I wrote git-trail, which shows how to reach commits in the current branch from other branches. Since we don’t have enough local branches to make it interesting, lets show remote branches too (-r):

hecate tmp/rack@master% git trail -r
2013-01-04 7e1f081 master
2013-01-04 7e1f081 remotes/origin/HEAD
2013-01-04 1e75faa remotes/origin/hijack~2
2013-01-04 1e75faa remotes/origin/master~1
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1
2012-03-18 7d7977f remotes/origin/rack-1.4~77
2011-05-22 a50dda5 remotes/origin/rack-1.3~99
2010-06-15 dc6b54e remotes/origin/rack-1.2~38
2010-01-03 e6ebd83 remotes/origin/rack-1.1~23
2009-04-25 d221938 remotes/origin/rack-1.0~24
2009-01-05 7fed4c7 remotes/origin/rack-0.9~15
2008-08-09 e9f9f27 remotes/origin/rack-0.4~6

What you see is the first common commit between every branch and the current branch, together with the commit date. If the branch is listed without suffixes, it is completely included. Else, you effectively see how the branch diverges. For example, in rack-1.4, there have been 77 patches since branching from master. The feature branch hijack consists of two commits. Lets look at the view from that feature branch:

hecate tmp/rack@master% git trail -r origin/hijack
2013-01-04 8a311fb remotes/origin/hijack
2013-01-04 1e75faa master~1
2013-01-04 1e75faa remotes/origin/HEAD~1
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1
2012-03-18 7d7977f remotes/origin/rack-1.4~77
2011-05-22 a50dda5 remotes/origin/rack-1.3~99
2010-06-15 dc6b54e remotes/origin/rack-1.2~38
2010-01-03 e6ebd83 remotes/origin/rack-1.1~23
2009-04-25 d221938 remotes/origin/rack-1.0~24
2009-01-05 7fed4c7 remotes/origin/rack-0.9~15
2008-08-09 e9f9f27 remotes/origin/rack-0.4~6

We see that there have been commits to master since hijack was branched, and we should perhaps rebase hijack if we wanted to submit it.

Let’s say we simply merged it into master:

hecate tmp/rack@master% git merge origin/hijack
hecate tmp/rack@master% git trail -r
2013-01-06 68de794 master
2013-01-04 8a311fb remotes/origin/hijack
2013-01-04 7e1f081 remotes/origin/HEAD
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1
2012-03-18 7d7977f remotes/origin/rack-1.4~77

Now hijack appears undecorated: it is completely contained in the current branch history.

Let’s say we work on the other feature branch next, unstandard_uri_escape:

hecate tmp/rack@master% git checkout unstandard_uri_escape
hecate tmp/rack@unstandard_uri_escape% git trail
2012-11-03 decaa23 unstandard_uri_escape
2012-11-03 1824547 master~10^2~1

We can now rebase it to make it a proper child of master:

hecate tmp/rack@unstandard_uri_escape% git rebase master
hecate tmp/rack@unstandard_uri_escape% git trail
2013-01-06 92b40fa unstandard_uri_escape
2013-01-06 c30da33 master

And then master can be fast-forwarded:

hecate tmp/rack@unstandard_uri_escape% git checkout master
hecate tmp/rack@master% git trail
2013-01-06 c30da33 master
2013-01-06 c30da33 unstandard_uri_escape~1
hecate tmp/rack@master% git merge unstandard_uri_escape 
Updating c30da33..92b40fa
hecate tmp/rack@master% git trail
2013-01-06 92b40fa master
2013-01-06 92b40fa unstandard_uri_escape

I hope this exposed how git trail helps me to keep track of dealing with branches.

git neck

The perfect match for git-trail is git-neck, which show commits from the HEAD until the first branching point… that should explain the name.

So, what is the “neck” of our master branch as above?

hecate tmp/rack@master% git neck -r
92b40fa Add a decoder that supports ECMA unicode uris
c30da33 Merge remote-tracking branch 'origin/hijack'
7e1f081 Merge pull request #480 from udzura/master
3edd1e8 Add a rackup option for one-liner rack app server
6d41179 Extract Builder.new_from_string from Builder.parse_file

Likewise, let’s have a look at that remote feature branch sticking around:

% git neck -r origin/unstandard_uri_escape
decaa23 Add a decoder that supports ECMA unicode uris

It was just a single commit. We can also look at the neck of an old release branch:

hecate tmp/rack@master% git neck -r origin/rack-0.4
92f79ea Make Rack::Lint::InputWrapper delegate size method to underlying IO object.
e33cc65 Update to version 0.4
ab9a95e Fix packaging script
1ccdf73 Update README
1b56583 Document REQUEST_METHOD future changes
f0977a8 Disarm and document Content-Length checking in Rack::Lint for 0.4

And we see the 6 commits that are only in rack-0.4.

If you remember the situation before merging the feature branches:

hecate tmp/r2@master% git trail -r
2013-01-04 7e1f081 master
2013-01-04 7e1f081 remotes/origin/HEAD
2013-01-04 1e75faa remotes/origin/hijack~2
2013-01-04 1e75faa remotes/origin/master~1
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1

Here, the neck is the part until master forked off:

hecate tmp/r2@master% git neck -r
7e1f081 Merge pull request #480 from udzura/master
3edd1e8 Add a rackup option for one-liner rack app server
6d41179 Extract Builder.new_from_string from Builder.parse_file

git neck is most useful if you are working in a feature branch which no other branch forks off, because then the neck goes until where you forked it.

Using git diff without Git

At last, another small trick: git diff works between any two files (or directories), even if you don’t use Git at all to track them. But you gain some advantages over regular diff, like --word-diff, --color or --stat without having additional tools beyond Git installed.

Also, you can use git diff --binary to generate efficient binary deltas which you can apply again provided you have the unpatched file. (Possibly you need to edit the patch to make both filenames the same, so git apply finds everything.)

NP: Sophie Hunger—What it is

17feb2012 · 10 new zsh tricks you may not know...

It’s been over a year since the last installment of the series.

  1. Sifting through others’ .zshrc, one occasionally finds aliases like:

    alias ls=' ls'
    alias cd=' cd'

    … and so on. The reason for this is simple, but not obvious: with setopt HIST_IGNORE_SPACE, these commands will be ignored, even if you don’t start these commands with a space.

  2. We already talked about brace expansion like {1..5} which expands to 1 2 3 4 5. But did you know you also can do {1..10..2}, which expands to 1 3 5 7 9 and {005..14..2} which expands to 005 007 009 011 013? Oh, and {10..1} works as well as {10..1..2}. Now you can throw seq(1) away!

  3. Using C-r and C-s to search history is well known, but the default search is a bit limited. Use these lines to enable search by globs, e.g. gcc*foo.c:

    bindkey "^R" history-incremental-pattern-search-backward
    bindkey "^S" history-incremental-pattern-search-forward
  4. One nice trick if you often suspend vi by C-z:

    foreground-vi() {
      fg %vi
    zle -N foreground-vi
    bindkey '^Z' foreground-vi

    This will make C-z on the command line resume vi again, so you can toggle between them easily. Even if you typed something already!

  5. zsh has lots of documentation, but finding what you want to know can be difficult. The manpage zshall(1) contains everything, and this function will make it easy to search in:

    zman() {
      PAGER="less -g -s '+/^       "$1"'" man zshall

    Try zman fc or zman HIST_IGNORE_SPACE! (Use n if the first match is not what you were looking for.)

  6. Recently, I’ve become an avid user of the directory stack, but not really for its intended usage; instead, I use it together with the next trick. Here’s how you can persist the dirstack across sessions:

    if [[ -f $DIRSTACKFILE ]] && [[ $#dirstack -eq 0 ]]; then
      dirstack=( ${(f)"$(< $DIRSTACKFILE)"} )
      [[ -d $dirstack[1] ]] && cd $dirstack[1] && cd $OLDPWD
    chpwd() {
      print -l $PWD ${(u)dirstack} >$DIRSTACKFILE

    First, we limit the dirstack to nine entries, load them from .zdirs if possible, and then we save them again on every directory change.

    For a long time, I used to have something similar that only saved $OLDPWD, so I could open a new shell and cd - and be back where I last changed to. But now I use this, and AUTO_PUSHD, together with the next trick.

  7. Every zsh user knows that you can use dirs to display the dirstack, and cd -N to go to the N-th element.

    But did you know zsh will show the dirstack on cd -TAB? It’s awesome, and does all the directory jumping I need.

    % cd -TAB
    1 -- /home/chris/mess/current
    2 -- /home/chris/mess/current/mdnsd
    3 -- /home/chris/mess/current/mdnsd/libutil
    4 -- /home/chris
    5 -- /home/chris/src/aewm-1.2.7/clients
    6 -- /home/chris/mess/2011/47/fspanel-0.7
    7 -- /home/chris/mess/2011/47
    8 -- /home/chris/src/mcwm
  8. Which words end with ‘tent’? Of course you can do grep tent$ /usr/share/dict/words, but did you know you can do look _tent and press TAB (_ is where the cursor is)?

  9. This fantastic zsh trick is from Julius Plenz: complete words from tmux pane.

  10. Perhaps you know zmv already, but it can be a bit nasty. E.g. to rename all *.lis files to *.txt, the manual recommends:

    zmv '(*).lis' '$1.txt'

    However, with the awesome -W mode, you can write this instead:

    zmv -W '*.lis' '*.txt'

    If you are not sure what happens, use the dry-run mode first (-n).

    That concludes this, now hopefully yearly, installment. Perhaps you’ll find even more new more things in my recently cleaned up .zshrc. Enjoy your Z shell!

    NP: EMA—Milkman

14feb2011 · 10 more zsh tricks you may not know...

It’s been almost three years since the last installment, so here is the next dollop of tips:

  1. =(command) expands to a tempfile with the output of command that is deleted after the line has finished. In effect, the same as <(command) but allows applications to seek. E.g.:

    xpdf =(zcat foo.pdf.gz)
  2. !-history-expansion is nice, but can be confusing if you have a command line with many ! that should be left alone. Either quote the ! with single quotes or write !" at the beginning of the line (yes, that " is left unclosed):

    % !" echo Hey there! Wow!!
    Hey there! Wow!!
  3. An application of modifiers is !:t, which results into the basename of the last argument. Very useful when working with URLs, for example. You’ll never have to strip the path manually again:

    % wget
    % tar xzvf !:t
  4. When playing with parameter expansion flags, it often is annoying having to use variables for immediate values:

    % foo=bar.c; echo ${foo:a:u}

    Instead of the ugly solution

    % echo ${$(echo bar.c):a:u}

    better use this:

    % echo ${${:-bar.c}:a:u}

    Here, ${:-bar.c} is an instance of the well-known ${FOO:-BAR} default substition operator.

  5. To run a command several times, use repeat. Useful for benchmarks, e.g.:

    % repeat 3 time sleep 1
    sleep 1  0.00s user 0.00s system 0% cpu 1.002 total
    sleep 1  0.00s user 0.00s system 0% cpu 1.005 total
    sleep 1  0.00s user 0.00s system 0% cpu 1.002 total
  6. Use glob modifiers to sort glob expansions. Helpful are: (om) (sort by modification time) or (n) (sort numerically):

    % pdfjoin chapter*.pdf(n) -o all.pdf
  7. Another useful glob modifier is P, for example to prefix a flag:

    % tar czvf foo.tar.gz * *.tmp(P:--exclude:)

    (yes, tar can exclude patterns, but some other tools can’t, and zsh does patterns better anyway.)

  8. Some ZLE hacks I use. To override default completion in various ways:

    # Force file name completion on C-x TAB, Shift-TAB.
    zle -C complete-files complete-word _generic
    zstyle ':completion:complete-files:*' completer _files
    bindkey "^X^I" complete-files
    bindkey "^[[Z" complete-files
    # Force menu on C-x RET.
    zle -C complete-first complete-word _generic
    zstyle ':completion:complete-first:*' menu yes
    bindkey "^X^M" complete-first
  9. A function to make adding flags or prefixing arguments easier:

    # Move to where the arguments belong.
    after-first-word() {
      zle beginning-of-line
      zle forward-word
    zle -N after-first-word
    bindkey "^X1" after-first-word
  10. Complete with words in the history (like Emacs dabbrev):

    # Complete in history with M-/, M-,
    zstyle ':completion:history-words:*' list no 
    zstyle ':completion:history-words:*' menu yes
    zstyle ':completion:history-words:*' remove-all-dups yes
    bindkey "\e/" _history-complete-older
    bindkey "\e," _history-complete-newer

    Of course, all things are mentioned in the comprehensive manual, or the great User’s Guide to the Z-Shell which I wholeheartedly recommend. But one needs to find them. :)

    NP: Aimee Mann—Freeway

08aug2008 · Taming $RUBYLIB with the Z shell

Ok, I’m fed up. Writing a good package manager for Ruby is a fight against windmills.

So let’s do the easiest thing that could possibly work. Redefining Kernel#require is a no-go, for it will lead to the gates of hell. Installing multiple projects to the same location is error-prone, requires non-trivial amounts of code and introduces packaging effort.

Luckily, most packages these days run directly from a checkout or their released archives (and if you provide neither, you’re doing it wrong). Essentially, all you need to make it convenient setting and manipulating $RUBYLIB, “A colon-separated list of directories that are added to Ruby’s library load path ($:).” The Z shell (1, 2) to the rescue!

Add this to your .zshrc (or .zshenv, if you want it in non-interactive shells as well):

# unique, exported, tied array of $rubylib to colon-seperated $RUBYLIB
# 08aug2008  +chris+
typeset -T -U -gx -a RUBYLIB rubylib ':'
rubylib-add()   { rubylib+=("$@") }
rubylib-del()   { for i ("$@") { rubylib[(r)$i]=() } }
rubylib-reset() { rubylib=(); [[ -f ~/.rubylib ]] && source ~/.rubylib }

This creates a zsh array rubylib which value reflects $RUBYLIB and vice versa (zsh does the same for $PATH and $MANPATH, for example), and defines three functions to add and remove paths as well as reset the variable.

Also, create a file ~/.rubylib where you set the defaults. I simply use:

rubylib-add ~/projects/{testspec,bacon,rack}/lib
rubylib-add ~/src/{camping,markaby}/lib

Remember, you can use the full power of zsh to set this:

rubylib-add ~/src/rubystuff/*/(lib|ext)(/)

You need to use (x|y) instead of {x,y} here to only expand to existing files. The final (/) ensures these files really are directories.

Reload your .zshrc, and voila, your packages are accessible from every Ruby script. Now, if a project has different requirements, just create a script there to adjust $RUBYLIB. Or use vared to interactively change the load path.

[Thanks to et for improving rubylib-del.]

NP: Curve—Dirty High

29feb2008 · 10 zsh tricks you may not know...

…yet, that is. Or if you do, you read the man page pretty well. :-)

zsh has gazillions of features, but I think these are pretty useful for daily use:

  1. ESC-. inserts the last argument of the previous history line, repeat to go back in history. (Same in Bash.)
  2. ESC-' quotes the whole line. (Useful for su -c or ssh).
  3. ESC-q clears the line and inserts it again on the next prompt, allowing you to issue an interim command.
  4. <(command) returns the filename (in /dev/fd if supported or as a FIFO) of the pipe given by command for reading. (For example, use diff <(ruby foo.rb) <(ruby-1.9 foo.rb) to compare two program outputs).
  5. cd old new substitutes old with new once in the pwd and chdirs there.
  6. !$ expands to the previous history line’s last argument, !^ expands to the first argument, !:n to the n-th argument.
  7. =foo expands to the full path of foo in the PATH (like which foo).
  8. for src in *.c do ... done can be abbreviated to for src (*.c) { ... } (which is actually memorizable). You can even drop the curly braces if you don’t have ; in the command.
  9. <42-69> globs numbers between 42 and 69. Drop the number(s) to make it open-valued. {42..69} expands to the numbers between 42 and 69.
  10. *** expands recursively like **, but follows symbolic links.
  11. Addition! ESC-RETURN inserts a literal newline, so you can edit longer commands easily.

More tricks:

Happy hacking.

NP: Shriekback—Mistah Linn He Dead

25mar2007 · Switching to zsh

With the help of the nice folks in #ruby-de, I finally switched to the Z shell after using GNU Bash for over seven years (with a rather short tcsh intermezzo, admittedly).

I’ve wanted to do the switch for some time, but I never managed to make it familiar enough for daily use (the default setup is unusable). Well, I now got a reasonable .zshrc together, and want like to share some parts of it.

Completion is the most awesome feature of zsh, although the recent Bash versions are steadily coming closer. Let’s enable the completion:

zmodload zsh/complist
autoload compinit && compinit

I don’t want it to think // means anything special, though:

zstyle ':completion:*' squeeze-slashes true

Add colors like in ls(1) for the file name completion:

eval `gdircolors`
zstyle ':completion:*' list-colors ''
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}

And then, make the completion feel as Bash-like as possible (show all completions after two TABS, don’t cycle, put the prompt always below the completions):


History management, this is essentially how I configured it for Bash too:


The prompt, I used a rather bloated prompt with Bash; this is a stripped-down version with everything removed I don’t need. (And the monochrome version.)

PROMPT="%(?..[%?] )%(1L.%L.)#%h<%l>%m:%(4~,.../,)%3~%(!.#.$) "

How it looks, for example:

[1] 2#276<p5>lilith:.../rack/lib/rack$

This means, the last command exited with status 1, it is a second-level subshell, line 276 of history (for !), running on ttyp5 on my trusty iBook lilith, and I’m a normal user ($) in the rack/lib/rack directory on my disk (I know where that is).

I don’t use a right-hand-side prompt at the moment because I think it’s confusing, especially if your terminal isn’t very wide.

Terminal title, using a precmd. Host and path are enough for my purposes.

precmd () {print -Pn "\e]0;%n@%m: %d\a"}

Z line editor, zsh’s version of readline has a very good Emacs keybinding support which we’ll enable and configure it to regard / as a word seperator too (highly useful for editing paths):

bindkey -e

Mess, finally, mess dir support for zsh:

function mess {
  DIR=`~/bin/mess.rb "$@"`
  [[ $? -eq 0 ]] && cd "$DIR"

That’s essentially it; as you see, nothing really special but it’s a non-trivial configuration I’m very comfortable with so far.

Now, what benefits does zsh give me?

  • Mighty globs (** alone is worth the switch).
  • A good multiline editor.
  • Ready-made completion of just about everything.
  • Better redirection (shortcuts like |& for 2>&1, which I always get wrong).
  • Control structures I can remember (for x (1 2 3); echo $).
  • Better scripting (I don’t really do shell scripts anymore, tough).
  • Tetris. Ehem.

The only thing so far I couldn’t find out was how to disable file name escaping in the completion list, but that’s a minor aesthetic feature. Feel free to tell me if you know how that can be fixed.

Addition from January 19, 2007: zsh supports UTF8 line editing if the locale is right. For English messages, therefore set LANG=en_US.utf8 as LANG=C will not work.

NP: Guns N’ Roses—There Was A Time

Copyright © 2004–2016