Expand your command line skills

As a programmer you have a wide array of potential tools at your disposal for creating, manipulating, and executing code. You may decide to utilize an IDE (integrated development environment) such as Eclipse or Visual Studio. And no doubt, such programs can be very useful. You may be able to integrate a large portion of your taskflow with your IDE of choice, but there will undoubtedly be times when you still need to use the command line. Leveling up your command line skills is always a fruitful investment, even if you primarily use an IDE for development.

You may also decide to permanently dwell in the terminal and really see dividends for investing in these skills. I tend to work nearly exclusively in a terminal window, using vim as my text editor of choice, and leveraging tmux for its terminal multiplexing and persistent sessions which I can attach to from any device. The familiarity of working within the terminal makes it pretty easy to hop onto other machines and find my way around, given the ubiquity of the core Linux utilities which I tend to most rely upon in my workflow.

To this end, I’d like to go through some of the practical use cases for history expansion that I’ve found particularly useful on a regular basis. As mentioned, this is for bash, which happens to be the default shell on Linux and MacOS terminals. Another shell that I see pop up with regularity from time to time is zsh. I have not verified all of the examples, but I think most of what we’re covering here is also supported in zsh, though YMMV.

Damage control

While playing around with history expansion, you may want to exercise caution so that you don’t do anything unexpected… Let’s set the option in the current shell so that pressing <Enter> with a history expansion will expand the shortcut first, and make you press <Enter> again, so that you don’t accidentally execute an incorrect command which might cause damage.

$ shopt -s histverify

We’ll come back around at the end of the article to discuss how to have this option set for you automatically going forward.

My Favorite Four

!! (previous command)

Entering “!!” in the command prompt will expand to the last command you executed. This is particularly useful whenever you want to execute the same command you entered, but with super user privileges.

# Cannot install as non-root
$ apt-get install foo
E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?

# Let's try it again with super user privileges
$ sudo !!
$ sudo apt-get install foo
Reading package lists... Done
Building dependency tree       
Reading state information... Done
E: Unable to locate package foo

!$ (last argument of previous command)

Entering “!$” in the command prompt will expand to the last argument of the last command you executed. This is probably the history expansion I find myself using the most. It is a pretty common use case to create or modify a file, and then pass it as an argument to another utility, e.g., staging a new or modified file for commit or executing a modified script. I also find myself using it quite often when copying a file to another file and then modifying that new file.

# Create a file and stage it for commit
$ echo "placeholder text for new file" > newfile.txt
$ git add !$
$ git add newfile.txt

# Create or modify a Python script and then execute it
$ vi myscript.py
$ python !$
$ python myscript.py
To Camelot!

^old^new OR !!:s/old/new (substitute old pattern for new pattern in previous command- first/one match only)

Maybe you typed out a big command with a bunch of arguments, and screwed up on one of the filenames or command options. These history expansions are useful for correcting course. Note that they only substitute the first match, so if there are multiple instances of the same pattern that you want to modify, you’ll need to use the next history expansion instead.

$ echo "cat"
cat
$ ^cat^dog
$ echo "dog"
dog
$ !!:s/dog/cat
$ echo "cat"
cat

$ echo "cat dog cat"
cat dog cat
$ ^cat^dog
$ echo "dog dog cat"
dog dog cat
$ !!:s/dog/cat
$ echo "cat dog cat"
cat dog cat

!!:gs/old/new (substitute old pattern for new pattern in previous command- all matches)

Like the previous !!:s/old/new substitution, but with an extra option “g” for “global” substitution.

$ echo "cat dog cat"
cat dog cat
$ !!:gs/cat/dog
$ echo "dog dog dog"
dog dog dog

Handy modifiers extending the fabulous four

!<number> (the <number>th command)

All of the previous “!!” examples can also be used within the general !<number> pattern, in which the number can be a positive or negative number. Via the command “history” you can see your command history and the commands’ associated numbers. Let’s invoke the history command and execute a particular previous history command.

$ history
  ...
  630  echo "cat foo cat"
  631  history
$ !630
$ echo "cat foo cat"
cat foo cat

You can also specify a negative number, e.g., !-2 is the command two commands ago.

$ python myscript.py
  File "myscript.py", line 2

               ^
SyntaxError: unexpected EOF while parsing

# Correct the syntax error and run the script again
$ vi myscript.py
$ !-2
$ python myscript.py
foo

!!:<number> (the <number>th argument of the previous command)

Besides the “!$” expansion to get the last argument of the previous command, you can use the !!:<number> expansion to use the <number>th argument(s) from the previous command, the command itself being the 0th argument.

$ echo making bacon pancakes
making bacon pancakes
$ echo !!:2
$ echo bacon
bacon

You can also specify a range of arguments via !!:<number>-<number>.

$ echo what is the meaning of this life?
what is the meaning of this life?
$ echo !!:1-6
$ echo what is the meaning of this
what is the meaning of this

You can also use * modifier to indicate all arguments, with !* being a shortcut for all arguments from the previous command.

$ touch foo.txt spam.txt eggs.txt
$ git add !*
$ git add foo.txt spam.txt eggs.txt
$ ls
foo.txt    spam.txt    eggs.txt
$ cat !-3:*
$ cat foo.txt spam.txt eggs.txt

!{string}/!?{string} (the previous command starting with/containing the specified {string} value)

I tend to use bash’s reverse search functionality (<control>-r) for this kind of thing, but you can also use this handy history expansion technique.

Starting with match:

$ python myscript.py
foo
$ echo "the dragons are coming" > got.txt
$ touch somefile.txt
$ !python
$ python myscript.py
foo
$ !myscript
-bash: !myscript: event not found

Contains match:

$ echo hello world
hello world
$ !?world
$ echo hello world
hello world

Configuring the histverify option

You’ll want to add the “shopt -s histverify” we executed above to your shell initialization file so that this is enabled by default.

Which file is this? It can be a bit complicated (always many variables and exceptions between distributions), but the intention is that ~/.bash_profile is used for login shells, i.e., when you login sitting at the machine or remotely via ssh, while ~/.bashrc is used for initializing a new shell instance while you are already logged in. It is a common practice to put most things in your ~/.bashrc, and to source your .bashrc file from your .bash_profile file with something like the following:

if [ -f ~/.bashrc ]; then
    source ~/.bashrc
fi

Hack away~