Learning Shell Scripting with Zsh — Save 50%
Your one-stop guide to reading, writing, and debugging simple and complex Z shell scripts with this book and ebook
This article by Gastón Festari, the author of Learning Shell Scripting with Zsh, will teach to apply specific constructs such as the double bang (!!) to the shell's history and avoid repeating yourself to boredom.
(for more resources related to this topic, see here.)
Working with history
Like an elephant, many modern Unix shells tend to remember in great detail the copious amount of commands entered while working with them. Being able to glimpse at what you have been up to is not only practical from a work-log perspective, but also as a way to speed things up. Think about it; you could use history to see (and eventually edit) a previously typed command, get a bit of context as to what's going on with your system, or avoid retyping the same thing over and over.
We'll now take a look at how to use zsh's history expansion to work with previous entries in the command line.
One of the ways zsh provides for you to access your history is via the so-called history expansion. This works whenever your input begins with the bang ! special character.
Accessing your history entries is done via what we call event designators. Like escape sequences, designators are fancy names for constructs that the shell expands in order to know exactly what needs to be retrieved from history. One of the most popular and helpful event designators is the double bang (!!), which by itself refers to the most recent command entered:
% sh myscript.sh > myscript.sh: Error: you need to be root to execute this. % sudo !! > myscript.sh: executing myscript.sh
As you can see, the !! character can be really useful for those occasions when you forget to run something on elevated privileges. What happens then is that zsh immediately expands the reference to the last command in history and replaces it in the line that contains the sudo call, saving you from entering the whole line again.
That's really neat for the command we just typed, but what if the previous command is further behind in the history timeline? Well, then we need to use the vanilla event bang:
% !cat % cat /etc/hosts | grep 127.0.1.1
Here my last executed command that had cat in it was a printout of my hosts file (cat /etc/hosts), followed by a call to grep as I was looking for lines that have 127.0.1.1 on them.
If you connect to a remote host using SSH, you could use something like the following to retrieve the last run connection:
% !ssh % ssh firstname.lastname@example.org
As you can see, the syntax for history expansion is fairly easy to remember. Just put a ! character together with the command you're looking for and let zsh work its magic.
Having the shell making substitutions and automatically executing commands demands a bit more "blind faith" than most of us would like to deposit on their shell. Luckily, we can set the HIST_VERIFY option in .zshrc to force zsh into asking for confirmation every time you bang a command:
% setopt HIST_VERIFY % echo 'Hello!' > Hello! % !! % echo 'Hello!'
The shell completes the input in your prompt using the previous command, but does not execute it. This is really useful for things like elevated privileges or sudo commands. Feel free to go ahead and add setopt HIST_VERIFY to your .zshrc file, as we'll assume it is being used from now on.
Let's kick it up a notch then; you can combine the special characters ^ and $ in order to access the first and last arguments of a history entry respectively:
% mkdir new_folder % cd !^ % cd new_folder
The ^ character gets expanded into the first argument of the mkdir command, which in this particular case is new folder.
% touch log1.txt log2.txt % nano !$ % nano log2.txt
Here the same happens with $, only this time the last argument of the touch command is expanded so we can eventually edit it using nano.
If you are familiar with regular expressions, both of these designators' behavior shouldn't be too surprising. However, if what you need to do is access some string that is not located either at the beginning (^) or end ($) of the history, then you need the ? designator:
% !?etc > cat /etc/hosts | grep 127.0.1.1
The preceding expression matches the most recent command containing etc. Generally speaking, the syntax for using the ? event designator can then be summed up as follows:
The optional ? you see there at the end is only necessary if the command is followed by any text that is not to be considered part of str; for example:
% !?etc?^ > /etc/hosts
Did you notice how both the ? characters serve as delimiters for the etc keyword? Think of them as parentheses that wrap the expression you're trying to match. The caret operator (^) is there as we are interested in the first argument of that particular command line, which coincidentally is the /etc/hosts string.
There's lots more we can do with the history bang operator. Another neat trick is that it can refer to a particular line in your history. As before, the syntax is merely a tweak of what we already know:
!<hist_number> % !103 # this retrieves the 103rd entry in your $HISTFILE. % !4 # this retrieves the 4th entry.
But what about knowing which line I want to use? Well, that's a bit more complex, but not as much as using grep, ack, or whatever it is that kids are using these days to search within your history file:
% history | grep nano > 2045 nano /etc/hosts
Using grep and searching for entries that feature nano, I can see that I edited /etc/hosts with it, and that the record resides on line 2045 of my $HISTFILE. If we wanted to open the hosts file again, it'll be a simple matter of calling:
% !2045 % nano /etc/hosts
Interestingly, you can also use a negative integer to refer to the nth-to-last entry:
% !-2 # this will retrieve the 2nd to last entry in history. % !-97 # this does the same to the 97th to last entry.
Negative indexes should be pretty familiar territory for some programmers (I'm looking at you, Python and Ruby developers).
You can prevent a command from being added to your history by setting HIST_IGNORE_SPACE in your startup files. This will make the shell ignore the lines that start with a space.
% echo "this line will be recorded in history" % echo "this will not"
More useful options
Here are a couple of history-related options worth considering when populating your startup files, in addition to what we have already discussed. Just put any (or all) of these on your .zshrc and remember to append setopt before each entry.
- EXTENDED_HISTORY: Saves a timestamp and duration for each history entry run. An excellent addition for the data analysis aficionado.
- HIST_IGNORE_ALL_DUPS: Ignores duplicate entries when showing results.
- HIST_FIND_NO_DUPS: Does not display eventual duplicates of a line that has already been found.
- HIST_REDUCE_BLANKS: Removes extra spaces and tabs from history entries.
- INC_APPEND_HISTORY: Adds entries to the history as they are typed, that is, doesn't wait until the shell exits. Probably one of the most awesome features of zsh. You know you want this.
- SHARE_HISTORY: Shares history between different zsh processes. Another great option to compliment the previous entry.
So we had a look at some of the most prominent time-saving features of zsh by learning about history expansion and to avoid repeating ourselves into oblivion. We went beyond using the Up and Down arrow keys and applied some more specific constructs such as the double bang (!!) to recall the very last entry of our log.
Not bad at all. Pat yourself in the back or go grab a beer. Although, come to think about it, there seems to be much more left in store for us to discover yet…
Resources for article:
- Linux Shell Scripting – various recipes to help you [article]
- Installing VirtualBox on Linux [article]
- Linux Shell Script: Monitoring Activities [article]
|Your one-stop guide to reading, writing, and debugging simple and complex Z shell scripts with this book and ebook|
eBook Price: $17.99
Book Price: $29.99
About the Author :
Gastón Festari is a scripting language enthusiast with over five years of experience and a firm believer in free, open source software. Currently working as a developer for Globant, he likes to spread the word about zsh at different meetups and events when away from the keyboard.