leah blogs: March 2013

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

Copyright © 2004–2022