Tommy's Blog

Zsh Keybinding Reminders

I've never been able to remember all of the handy line editing keyboardshortcuts that are available in Zsh. Sure, I know that Ctrl+A goes to the beginning of the line and Ctrl+E goes to the end of the line but there are many other keyboard shortcuts (key bindings) besides these that can be quite useful. I'd like to learn them so that I can use Zsh more efficiently. Maybe a program that annoys me with random keybinding at shell startup could help me learn. Let's make that.

Finding Zsh Keybindings

First I need a list of all the key bindings active in the shell. ZLE or the zsh line editor is the part of Zsh that is responsible for binding a particular key combination to a line editing command. Consulting the man page for ZLE we find that bindkey can be used to display a set of key bindings (a keymap).

% bindkey
"^@" set-mark-command
"^A" beginning-of-line
"^B" backward-char
"^D" delete-char-or-list
"^E" end-of-line
"^F" forward-char
"^G" send-break
[TRUNCATED]

There's a little bit of interpretation involved in understanding this keymap:

Selecting a Random Keybinding

It is easy enough to select a random keybinding, just shuffle all the keybindings and select the first one:

% bindkey | shuf -n1
"^[f" forward-word

Printing Help for ZLE Commands

It's all well and good if a key is mapped to a ZLE command with an obvious name like forward-word but what if that's not the case. Selecting another random keybinding:

% bindkey | shuf -n1
"^@" set-mark-command

What in the world is set-mark-command? The man page has the answer!

% man 1 zshzle

After scrolling a bit:

set-mark-command (^@) (unbound) (unbound)
      Set the mark at the cursor position.  If called with a negative
      numeric argument, do not set the mark but deactivate the region
      so that it is no longer highlighted (it is still usable for
      other purposes).  Otherwise the region is marked as active.

So it should be possible to just grep for this documentation right?

% man 1 zshzle | grep -A1 set-mark-command
% 

Curiously, grep doesn't return any matches. What is going on here? Inspecting the output from man with xxd

% man 1 zshzle | xxd | cut -b11-
…
0a20 2020 2020 2020 7308 7365 0865 7408  .       s.se.et.
742d 082d 6d08 6d61 0861 7208 726b 086b  t-.-m.ma.ar.rk.k
2d08 2d63 0863 6f08 6f6d 086d 6d08 6d61  -.-c.co.om.mm.ma
0861 6e08 6e64 0864 2028 5e08 5e40 0840  .an.nd.d (^.^@.@
2920 2875 6e62 6f75 6e64 2920 2875 6e62  ) (unbound) (unb
6f75 6e64 290a 2020 2020 2020 2020 2020  ound).
…

The label for set-mark-command is present but it is somewhat mangled. Every letter in the label is followed by 0x08 and then a repitition of the original letter. Presumably this strange sequence of characters is to format things nicely for display. This output is a bit annoying to have to deal with but it's not hard to do so by filtering through sed:

% man 1 zshzle | sed 's/.\x08//g

This sed filter is essentailly "when there is a character followed by the 0x08 character, substitute both characters with nothing (delete them)." By chaining grep on the end, it is now possible to search the documentation for a ZLE command by name:

% man 1 zshzle | sed -E 's/^ *|\x08.//g' | grep -A1 set-mark-command
set-mark-command in Emacs mode, or by visual-mode in Vi mode) is
enabled by default; consult this reference for more information.
--
set-mark-command (^@) (unbound) (unbound)
Set the mark at the cursor position.  If called with a negative
--
set-mark-command or exchange-point-and-mark.  Note that whether
or not the region is active has no effect on its use within

There are a few unintended matches here. The match in the middle is what I'm interested in. Tightening up the regular expression:

% man 1 zshzle | sed -E 's/^ *|\x08.//g' | grep '^set-mark-command ('
set-mark-command (^@) (unbound) (unbound)

Switching from grep to sed and generalising to create a Zsh function:

function print_zle_command_help() {
  man 1 zshzle | sed -E -n "
  # unindent and delete special formatting characters
  s/^ *|\x08.//g
  # so $1 matches the start of a command's manpage entry
  # up until the next blank line
  /^$1 \(/,/^$/ {
      # delete header line and blank lines
      /^$1 \(|^$/d
      # print entry
      p
  }"
}

Calling print_zle_command_help with set-mark-command

% print_zle_command_help set-mark-command
Set the mark at the cursor position.  If called with a negative
numeric argument, do not set the mark but deactivate the region
so that it is no longer highlighted (it is still usable for
other purposes).  Otherwise the region is marked as active.

Printing Random Keybinding on Shell Startup

To print a random keybinding every time the shell starts, add the print_zle_command_help function and this print_random_keybinding function to ~/.zshrc.

function print_random_keybinding() {
    # Select a random keybinding

    local keybinding=$(bindkey | shuf -n1)
    # Get keyboard shortcut part of the keybinding, deleting the quotes

    local shortcut=${${${(s. .)keybinding}[1]}[2,-2]}
    # Get the command part of the keybinding

    local command_name=${${(s. .)keybinding}[2]}
    echo $shortcut
    print_zle_command_help $command_name
}

Combining selecting a random keybinding and printing it: