Symkat

Bloggity Blog

Understanding Bash History

Articles for Systems Administration

When first exposed to the .bash_history file most users are appalled that their commands are being stored. Some systems administrators get an evil look in their eyes. Both are highly overrated responses. This file is actually extremely helpful and understanding how to fully utilize bash's history functions are paramount to being able to quickly recall commands and navigate the terminal.

An aside for evil systems administrators: reading .bash_history files is one of the least useful methods of monitoring used on a server for a number of reasons, including that any sufficiently malicious user wouldn't leave their commands behind.

An Introduction To History

History is maintained both in a file .bash_history and in ram (manipulated with the command history and environment variables). It is important to note that the history contained in ram is only written after a user logs out of his or her session.

Let's take a look at some of the important bash history variables and their explanations:

Variable Meaning
HISTFILE This variable contains the location of the history file. When bash is logged out of, the contents of the history command will be written to this file.
HISTFILESIZE The variable contains the maximum number of lines that may be in the history file. When bash writes to the history file, the oldest commands that go over this maximum number will be deleted.
HISTCONTROL This variable contains instructions for what should be ignored when added to the history file. This will be explained further below.
HISTSIZE This variable contains the maximum number of lines that be contained in the in-ram version of history, it acts the same as HISTFILESIZE.

The HISTCONTROL shell variable has four settings that define a set of standard rules that can used to determine which lines typed into the shell will be recorded into the history file.

Setting Name Setting Effect
Ignorespace When this setting is used, any line that begins with one or more spaces will not be added to the history.
Ignoredups When this setting is used lines that match the previous entry in the history are not saved.
Ignoreboth When this setting is used it implies the conditions of both ignoredups and ignorespace.
Not Defined If HISTCONTROL is not defined no conditions will be applied before the line is entered into the history [1].

These values can be changed using the export command, followed by the variable to set and its value. We're going to set up a small environment for testing, beforehand lets take a look at the default values.

symkat@symkat:~$ export | grep HIST
declare -x HISTCONTROL="ignoreboth"
declare -x HISTFILE="/home/symkat/.bash_history"
symkat@symkat:~$ echo $HISTSIZE; echo $HISTFILESIZE
500
500
symkat@symkat:~$

Now let's limit the history to ten lines.

symkat@symkat:~$ export HISTSIZE=10 HISTFILESIZE=10
symkat@symkat:~$ echo $HISTSIZE; echo $HISTFILESIZE
10
10
symkat@symkat:~$

Now that we understand the various settings used by bash's history, let's fill it with some lines and then take a look at how we can get to that quick-recall of commands.

symkat@symkat:~$ vim script.pl
symkat@symkat:~$ perl script.pl
Hello World
symkat@symkat:~$ history
1  vim script.pl
2  perl script.pl
3  history
symkat@symkat:~$ vim index.html
symkat@symkat:~$ history
1  vim script.pl
2  perl script.pl
3  history
4  vim index.html
5  history
symkat@symkat:~$

It is important to note the numbers shown before the command line. The first command was given the number 1, while commands after each got incremented by one. Your last command will always have the highest number, while the older the command the lower the number.

Bang Bang !!

The numbering comes into play when we look at the bang bang operator. You can rerun previous commands by addressing them by number, or be “N commands ago”. Let's take a look:

symkat@symkat:~$ !-2
vim index.html
symkat@symkat:~$ !!
vim index.html
symkat@symkat:~$ !1
vim script.pl
symkat@symkat:~$

The first command we used asked bash to run the second to last (-2) command again. By looking at the code example above this we see that the last command was history and the one previous was vim index.html. vim index.html was run again. The !-2 expanded to the command, and was added back to the history file as the last command.

After this we ran Bang-Bang, or !!. This expands to the very last command that was run (shorthand for !-1). Because the “vim script.pl” was expanded and added to the history file, this is the command that was run.

Finally, we addressed the very first command by its actual index number, 1. This ran vim script.

symkat@symkat:~$ history
1  vim script.pl
2  perl script.pl
3  history
4  vim index.html
5  history
6  vim index.html
7  vim script.pl
8  history
symkat@symkat:~$

Taking a look at the history command now we notice something. Although vim index.html was run twice in a row, the second time it's not shown. This goes back to the HISTCONTROL setting, which works against expansion with Bang-Bang operators.

It is worth noting that the Bang-Bang operators here do not have to be run on a line exclusively. You can combine them with literal text to have the expansion fill in, so that both the expansion and the literal strings are combined into one line that is executed.

symkat@symkat:~$ echo !!
echo history
history
symkat@symkat:~$ !! manipulation can be fun.
echo history  manipulation can be fun.
history manipulation can be fun.
symkat@symkat:~$

The double lines here are the result of the Bang-Bang operator printing the line that was executed before actually executing the line.

Bang Cash !$

Suppose that you would like to take the last argument to the last command and use it in your current line. You can use the Bang-Cash operator.

symkat@symkat:~$ ping 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=63 time=0.243 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=63 time=0.228 ms
64 bytes from 192.168.1.1: icmp_seq=3 ttl=63 time=0.250 ms
^C
--- 192.168.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 0.228/0.240/0.250/0.015 ms
symkat@symkat:~$ traceroute !$
traceroute 192.168.1.1
traceroute to 192.168.1.1 (192.168.1.1), 30 hops max, 40 byte packets
1  207.99.1.13 (207.99.1.13)  0.568 ms  0.590 ms  0.611 ms
2  192.168.1.1 (192.168.1.1)  0.155 ms  0.149 ms  0.149 ms
symkat@symkat:~$ nmap -p22,80,443 -P0 !$
nmap -p22,80,443 -P0 192.168.1.1

Starting Nmap 4.62 ( http://nmap.org ) at 2010-09-15 01:30 PDT
Interesting ports on 192.168.1.1:
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 0.070 seconds
symkat@symkat:~$

Bang Command !string

You can complete previous commands by the first few characters of the command as well. The Bang Command operator takes a string and matches it against previous commands executed. The matching is done such that the string is anchored to the beginning of the command, and an implicit * is attached to the end. It searches ordered based on the most recent commands executed.

symkat@symkat:~$ !nm
nmap -p22,80,443 -P0 192.168.1.1

Starting Nmap 4.62 ( http://nmap.org ) at 2010-09-15 01:34 PDT
Interesting ports on 192.168.1.1:
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 0.021 seconds
symkat@symkat:~$

Bang Question Mark !?string

In the situation where you know a part of the string which is not anchored to the beginning, or wish to search another part of the sting (for instance, looking at an IP address you recently nmap'ed or sshed to) you can use the Bang-Question-Mark-String operator.

symkat@symkat:~$ !?192
nmap -p22,80,443 -P0 192.168.1.1

Starting Nmap 4.62 ( http://nmap.org ) at 2010-09-15 01:36 PDT
Interesting ports on 192.168.1.1:
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 0.022 seconds
symkat@symkat:~$

Bang with Tongue !x:p or !?x?:p

If you're not too sure about the command you're about to run with a Bang operator, you can always use colon p (:p) to print the line that would be run without executing it. Because the expansion will be added to the history file, you can simply up-arrow-enter if it matches. It is worth noting that :p is a shortcut for ?:p. If you're using substring searching, you will need to use ?:p to have the same effect. If you're using string searches, or by number you can simply use :p.

symkat@symkat:~$ !!:p
nmap -p22,80,443 -P0 192.168.1.1
symkat@symkat:~$ !nm:p
nmap -p22,80,443 -P0 192.168.1.1
symkat@symkat:~$ !-9:p
nmap -p22,80,443 -P0 192.168.1.1
symkat@symkat:~$ !-4:p
history
symkat@symkat:~$ !?192?:p
nmap -p22,80,443 -P0 192.168.1.1
symkat@symkat:~$ !?192:p
-bash: !?192:p: event not found
symkat@symkat:~$

Although we've been using substring matching against the first part of an argument, it can be used against any portion of the strings in an argument. Searching for 168 would have yielded the exact same results above.

You may run into errors if your commands don't actually match anything in the history.

symkat@symkat:~$ !1
-bash: !1: event not found
symkat@symkat:~$ !
-bash: syntax error near unexpected token `newline'
symkat@symkat:~$ !!!
-bash: !: event not found
symkat@symkat:~$

The History Command

You can delete an entry from the history list by passing the -d switch and the number of the command in the history index to delete it.

symkat@symkat:~$ history
28  echo "History Entries"
29  random
30  some history here
31  history
symkat@symkat:~$ history -d 28
symkat@symkat:~$ history
28  random
29  some history here
30  history
31  history -d 28
32  history
symkat@symkat:~$  history -d 30
symkat@symkat:~$  history
28  random
29  some history here
30  history -d 28
31  history
symkat@symkat:~$

After deleting an entry in history, the number will be replaced with the next command that was run.

You can also clear the history completely with the -c switch.

symkat@symkat:~$ history -c
symkat@symkat:~$ history
28  history
symkat@symkat:~$

Bash history is extremely powerful and can save a great deal of time that would otherwise be spent with one's finger down on the up-arrow searching for a previous-entered command.

What's your favorite time saver in a terminal?

blog comments powered by Disqus