July 12, 2009

Know Your History

The UNIX/Linux shells evolved on a planet where saving every keystroke and millisecond of time was absolutely essential to survival. As a result, they're chock full of shortcuts, many of them with overlapping functionality, letting the user choose the method that works best for them.

The shell command history is a prime example. Even a newborn knows to use the up and down arrow keys to recall commands, and most toddlers are piping history into less to perform manual searches for complicated commands they don't feel like recreating. But if you ever have the chance to stare over the shoulder of a grey-bearded shell guru, you'll see that the true masters use several different techniques to pull up commands in the most efficient way. The following report was compiled from the sage advice given by these mysterious wizards.

Bang!
We'll start with the classic C Shell history syntax. You've no doubt used an exclamation point to re-execute a command from the history list, like !42, or have used the double exclamation to execute the previous command, !!. You can get pretty fancy with the available event designators, for instance, !-3 will execute the command three commands ago. And !echo will execute the most recently run command that began with echo.

This syntax has always scared me, as I could see myself executing a dangerous command without realizing it, especially if I'm in a hurry. But there is a safer way to use it, just include the :p modifier, which displays the command, but doesn't actually execute it. For instance, if the last command executed was echo Hello, typing !!:p would preview the command without running it, so you'd see echo Hello, instead of Hello. This allows you to make sure it's the command you intended to recall, and you can simply press the up arrow to execute it.

The ! syntax can be really useful in a grep-the-history-file workflow. Say I grep for the last time I issued an esxcfg-firewall command:

  history | grep esxcfg

    Output:
    203  esxcfg-firewall --openPort 9090,tcp,in,SimpleHTTP

  !203:p

    Output:
    esxcfg-firewall --openPort 9090,tcp,in,SimpleHTTP

Now if I hit the up arrow, the command is put on the prompt, and I can edit it to add a different port, etc.

If you'd like to delve deeper, google c shell command history

Fix Command
Another method for recalling commands from the history list is the bash built-in Fix Command, invoked with fc. To get the skinny on fc, bring up the man page for the built-in commands, with man builtins. Invoking fc with the -l option will print the last 16 commands. You can also specify a range of history commands to display, like:

  fc -l 208 234

The fc command can come in real handy when you are trying to recall and edit a whole series of commands. For instance, say you remember adding an alias to your .bashrc file, and you want to add an additional alias using the same series of commands to make sure it was configured properly. You recall using a cp command first to backup the .bashrc file, and specifying 'cp' as a search string after fc -l will print the history list beginning with the last occurrence of the command that matches the search string:

  fc -l cp

  110  cp .bashrc backup_bashrc
  111  echo "alias lax='ls -lax'" >> ~/.bashrc
  112  cat .bashrc
  113  . .bashrc
  114  lax

To create an la alias, invoke fc with a range of history events to copy them all into the default editor, which is set to vi in the ESX console:

  fc 110 114

Running the above command will copy the history events 110 through 114 into a vi editor session, where they can be modified to create the alias for la. Typing ZZ in vi will exit the editor and execute all the commands in the buffer: backing up .bashrc, creating the alias, cat'ing the file to see the change, restarting bash, and finally testing the new la alias. I don't use fc very often, but for scenarios like this it is a great tool to be familiar with.

vi Command Editing
The next two methods for using the command history rely on functionality provided by the GNU Readline library, a standard set of C functions available to any application, including bash. There are two editing and key-binding modes, emacs and vi. The default mode is emacs, and you can see what mode your shell is in now by typing:

  set | grep SHELLOPTS

If you're in the default emacs mode, you'll see emacs in the colon-separated list of options. To change this to vi mode, type:

  set -o vi

Now if you check the SHELLOPTS variable, you'll find emacs has been replaced with vi. I'm a vi kind of guy, so I always add the set option to my .bashrc file, with a command like:

  echo "set -o vi" >> ~/.bashrc

And source .bashrc again to get the changes:

  . ~/.bashrc

We'll look at how to use the Readline library in vi editing mode with an easy example. I've just grepped the default word file that comes with the service console for every word that begins with 'a' (this word file is not present in ESX 4.0). I'll do the same for the letters 'b' through 'f', and if I execute history, this is the output:

[admin@esx02 admin]$ history
    1  grep ^a /usr/share/dict/words
    2  grep ^b /usr/share/dict/words
    3  grep ^c /usr/share/dict/words
    4  grep ^d /usr/share/dict/words
    5  grep ^e /usr/share/dict/words
    6  grep ^f /usr/share/dict/words
    7  history

Back at the shell prompt, we can search through the history list for the last instance of the grep command. Press Esc to enter vi command editing mode, and then use a forward slash to search, just like in a normal instance of vi:

/grep

After hitting return, we should find grep ^f /usr/share/dict/words has been placed on the command line. Pressing n will iterate through each grep command until the first instance is found -- the grep command for words starting with 'a'. Continuing to press n will do nothing now, as we've reached the end of the matches for grep. However, pressing N will now iterate through the grep matches in reverse, working its way to the most recent grep command. This is handy and easy to remember, n or N for next, forward or backward through the history list.

Of course, the whole point of accessing the history list with this method is to easily edit the commands before executing them. If we wanted to modify the grep command to find all words starting with 'fox', we can just press Esc at a bash prompt to enter vi command editing mode, type /grep ^f to find the history entry where we searched for words starting with f, press i to enter vi insert mode, edit the command: grep ^fox /usr/share/dict/words, and press return to execute it. If we want to get every word that starts with ox, we just perform another search, /grep ^fox, move the cursor over the 'f', and hit x -- the vi delete character command -- to remove it.

This example is beyond goofy, but if you play around with this method, you'll find it to be very powerful and a huge timesaver.

Ctrl-r
If you press Ctrl-r in your terminal, you'll get a curious looking prompt:

  (reverse-i-search)`':

If you start typing a part of the command you wish to search through the history for, the command will appear after the prompt:

  (reverse-i-search)`grep': grep ^f /usr/share/dict/words

This is the most recent grep found while searching back through the history list. If you press Ctrl-r again, you'll find the one before that:

  (reverse-i-search)`grep': grep ^e /usr/share/dict/words

If you hit return, the command will execute. If you hit the left or right arrow keys, the command will be left on the prompt so you can edit it first.

The reverse-i-search prompt is a bash built-in named reverse-search-history. You can see that by default it is bound to the Ctrl-r key by issuing a bind -P command. If you look through the list, you should also see forward-search-history has been bound to Ctrl-s. The forward-search-history command is a useful companion because if you play with Ctrl-r, you'll notice that as it iterates through the history list, it remembers where it left off. So if you reverse search all the way to the start of the history file, you then have to cycle back around, even though the command you want was just one command more recent.

But there is a big problem with forward-search-history, it's bound to Ctrl-s by default, which is also bound to the stty stop output command, and stty will intercept it before anything has a chance to see it (if you already pressed Ctrl-s and your terminal appears dead, just type Ctrl-q to bring it back to life).

You can view the key bindings used by stty with this command:

  stty -a

But these two commands are too handy to let a little key binding issue get in the way, and we can easily add custom key bindings to our .bashrc file:

  bind '"\C-f": forward-search-history'

  bind '"\C-b": reverse-search-history'

Just edit the ~/.bashrc file with the editor you prefer, add those two lines in, and re-source with: . ~/.bashrc

Now Ctrl-b searches back through the history file, and Ctrl-f searches forward, and you can toggle back and forth between them and the search term will remain. Nice!

Share your secrets
Hopefully you found a new trick to play with, and if you have your own history workflow, please leave a comment with the details.

No comments:

Post a Comment

Post a Comment