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:
-
^[
followed by character means Alt+character.
Alt+U invokesup-case-word
:
"^[u" up-case-word
-
^
followed by character means Ctrl+character.
Ctrl+A invokesbeginning-of-line
:
"^A" beginning-of-line
-
Sometimes a binding consists of multiple keys pressed in sequence.
Ctrl+X then Ctrl+F invokesvi-find-next-char
:
"^X^F" vi-find-next-char
-
character without
^[
or^
is character by itself.
If Ctrl+X then = is pressed thenwhat-cursor-position
:
"^X=" what-cursor-position
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: