Bind extended keys to BASH functions

Bind basics

BASH's bind command permits three activities -- definition of new macros, definition of new key bindings for existing commands, and dumping the installed keybindings.

Macro is a string of characters  binded to a key combination. A typical textbook example would be:

lotzmana@safe$ bind '"\M-k"':"\"ls -f\""

It pastes "ls -f" when you press Alt+K. Later in life one usually finds that engaging the pre-existing keybinginds provides for some more inventive use of bind. More elaborate example follows.

Have you ever found yourself at the end of very long command just to remember that something else had to be executed before that, usually this means ctrl+c and more typing and retyping. The designers of the beloved 4NT thoughtfully provided ctrl+k to save the current line into history instead of executing it. Can't we forge something similar in bash?

lotzmana@safe$ bind '"\M-k"':"\"\C-ahistory -s '\C-e'\C-m\"" 

It binds a macro to Alt+K. The macro is not a plain text to be pasted but invokes some editing commands already binded to other keys. Imagine that you have already typed a long line, to save it you press ctrl+a (this moves the cursor to the beginning of the line), type "history -s ", press ctrl+e to go to the end and Enter (or ctrl+m) to execute. This is precisely the sequence of actions encoded in the macro binded to Alt+K.

PgUp in 4NT invokes a popup menu with the most recent commands for easy selection. While GUI programming is not an option, bind can still bring us closer to civilized life:

lotzmana@safe$ bind '"\e[5~"':"\"history | tail -25\C-m\""

Explore the manpages of readline and bash for more details and options.

Binding to keys that have no name definitions -- extended keys

The binding to PgUp didn't use some key name but employed the extended ESC sequence which PgUp generates. On some terminals using the meta encoding, like M-k, might also not work. It just happens that there is no accepted standard for how functional keys are encoded. This might be an issue for the deployment of one single .bashrc in heterogenous OS/terminal environment.

For example on my xterm I have to define the macro like this:

lotzmana@safe$ bind  '"\xeb"':"\"\C-ahistory -s '\C-e'\C-m\"" 

How did I know that 0xeb is the character that Alt+K generates? Well, first approximation was to use the debug terminal of the WW multi-platform console editor. The keyboard handler has a debug mode where it prints all the character sequences it couldn't match to known key combinations. Because this editor is not yet very popular I exported the knowledge into a handy standalone perl script. Start the script, press Alt+K, press 'x':

lotzmana@safe$ perl keys.pl
press 'x' to exit
\xeb
restore console mode

Then use the hex sequence on the left-hand side of bind.

Moving back in time  to the niceties of 4NT I remember that ctrl+left-arrow/ctrl+right-arrow moved cursor to the beginning of the previous or the next word. This is also available in bash -- Alt+F/Alt+B, but why not have the real thing?! Here are the key sequences I need under xterm:

lotzmana@safe$ perl keys.pl
press 'x' to exit
\x1b\x5b\x31\x3b\x35\x44
\x1b\x5b\x31\x3b\x35\x43
restore console mode
lotzmana@safe$ bind '"\e\x5b\x31\x3b\x35\x44"':backward-word
lotzmana@safe$ bind '"\e\x5b\x31\x3b\x35\x43"':forward-word

List of useful keybindings of bash under xterm

In the course of the last few weeks I assembled a list of a few key definition bindings which I find useful. They either assign functions to new keys or link up function previously inaccessible because of differences in key definitions on xterm. Best is to add this to your .bashrc:

# Now map xterm's alternative keybindings to existing functionality
# Some are simple translations to correspontend M- combinations
# ctrl+left/right arrows:
bind '"\e\x5b\x31\x3b\x35\x44"':backward-word
bind '"\e\x5b\x31\x3b\x35\x43"':forward-word
# alt+b/f:
bind '"\xe2"':'"\M-b"'
bind '"\xe6"':'"\M-f"'
# atl+backspace:
bind '"\xff"':backward-kill-word
# alt+'.':
bind '"\xae"':yank-last-arg
# alt+k:
bind '"\xeb"':"\"\M-k\""
# alt+w:
bind '"\xf7"':'"\M-w"'

Notice the way Alt+k is mapped to M-k, it is handy when you don't immediately know the name of the function you are assigning to, but only know the original keybinding. If you log in under other terminals you must use the keys.pl script to figure the keysequences of the keys you wish to bind there and add them to your .bashrc.

Inside keys.pl

You can simply download keys.pl and begin using it. If you are curious how it works, look at the source below and the explanation notes after that:
lotzmana@safe$ nl -w2 -s' '  keys.pl    
1 #!/usr/bin/perl -w
2 # keys.pl 1.0.0, 13-jul-2005
3 # petar marinov, http:/geocities.com/h2428, this is public domain

4 use strict;
5 $| = 1;
6 my $got = '';
7 while (1) {
8 # wait for a sequence to begin
9 $got = getone() while (ord($got) == 0);
10 # process a sequence ($got already holds the first character)
11 while (ord($got) != 0) {
12 exit(0) if ($got eq 'x');
13 printf("\\x%02x", ord($got));
14 $got = getone();
15 }
16 print "\n";
17 }
18 exit(0);

19 BEGIN {
20 use POSIX qw(:termios_h);
21 use Fcntl;

22 my ($term, $oterm, $echo, $noecho);
23 $term = POSIX::Termios->new();
24 $term->getattr(fileno(STDIN));
25 $oterm = $term->getlflag();

26 # puts console in a raw mode -- permits non-blocking reading
27 sub raw {
28 my $flags = 0;
29 $term->setlflag($oterm & ~(ECHO | ECHOK | ICANON));
30 $term->setcc(VTIME, 1);
31 $term->setattr(fileno(STDIN), TCSANOW);
32 fcntl(STDIN, F_GETFL, $flags)
33 or die "Couldn't get flags for HANDLE : $!\n";
34 $flags |= O_NONBLOCK;
35 fcntl(STDIN, F_SETFL, $flags)
36 or die "Couldn't set flags for HANDLE: $!\n";
37 }

38 # restores console to original mode
39 sub cooked {
40 print "restore console mode\n";
41 $term->setlflag($oterm);
42 $term->setcc(VTIME, 0);
43 $term->setattr(fileno(STDIN), TCSANOW);
44 }

45 # reads character or times out
46 sub getone {
47 my $key = ' ';
48 my ($rv, $rin, $win, $ein);49 my ($nfound, $timeleft, $rout, $wout, $eout);

50 $rin = $win = $ein = '';
51 vec($rin, fileno(STDIN), 1) = 1;
52 $ein = $rin | $win;
53 ($nfound,$timeleft) =
54 select($rout=$rin, $wout=$win, $eout=$ein, 0.10);
55 return chr(0) if $nfound == 0;
56 $rv = sysread(STDIN, $key, 1);
57 return chr(0) if (!defined($rv));
58 return $key;
59 }

60 raw();
61 print "press 'x' to exit\n";
62 }

63 END {
64 cooked()
65 }

7-17: The main loop consiste of two sub loops. The first first loop (line 9) waits for a new sequence  to begin. The second extracts all keys of a sequence and prints them on the screen.

26-37: For a program to read characters in a non-blocking mode it has to first switch the terminal to raw mode -- echo off, non-canonical, time-out set to minimum, and non-blocking mode read for STDIN.

45-59: The non-blocking read function uses select() to wait with timeout for a character from STDIN. It returns 0 on time out or ascii when character is present.

64-65: It politely returns the terminal back to mode which is good for command line editing.


last updated: 13-jul-2005 | home page | the text of this page is public domain 1