For years now, I’ve been using POSIX shells as one of my primary interfaces to my computers, alongside a browser. In fact, for the past 3 years, I’ve probably spent over 80% of my time on computers using one or the other. Spending so much time with these programs, I try to master them to be as efficient as possible. While mastery over browsers is somewhat limited, there’s nearly boundless possibilities with the shell (really, the assortment of programs accessed through the shell). I continually find new things about programs I use everyday — anything from little tips and tricks to eye-opening, paradigm-shifting features. In fact, just yesterday, I learned about sessions in vim. How have I not known this for years?! And less than a month ago, I learned about brace expansion in bash. Now I can just do mkdir /path/{dir1,dir2,dir3} instead of running three separate commands. It really makes me wonder what incredible knowledge could be gleaned from a thorough read of entire documentations from programs I use often. Interesting as that is, what I want to talk about today is bangs (!) in bash.

Let me first introduce to you the utility of bangs. Ever try to run a program that requires elevated privileges and forget to prepend the obligatory sudo in front? Me too, and too many times. I’d have to scroll up the history with ↑ or Ctrl+p and then shift the cursor to the start of the line with, say, Ctrl+a, and then type the sudo. What a waste of time! Instead of doing that, you can just be angry and tell the shell sudo !!. This is the exact same as if you had ran the command again, except prepending the sudo. For example, if I had ran pacman -Sy abc and then sudo !! right after, what I’d really be running is sudo pacman -Sy abc.

If this isn’t already life-changing, I don’t know what I can tell you. Actually, I’ve known about this for years and it’s been cemented into my workflow. For every few times I remember to add the sudo to a package install command, there’s another time that I forget and sudo !!. Before we go any further though, let’s talk about this weird expression. !! is pronounced “bang bang”, with each exclamation mark being referred to as a “bang”. Why? I’m not sure, but this stackexchange answer points to Unix nerds re-popularizing the name originating from the printing industry. I think it’s a pretty good name and much easier to vocalize than “exclamation point”, which is the same reason why the printing industry ostensibly used it. Enough with the interlude on etymology though, let’s get back to business.

Since learning the !! trick a while ago, I wondered why !! expanded into the previous command and if there were a family of expansions that !! was part of. Sure enough, there is. Broadly, ! is a special bash character for history expansion. From reading the GNU bash docs, ! commands are meant to reference your command history. The way I like to think about it is that it’s an interface to select a previous command, or parts of a previous command. The interface needs to know which command to select and optionally, which parts of the command to select. I won’t go into detail on command selection, especially since the official docs are linked right above, but I’ll give some of the more useful ones.

  • !-n refers to the nth most recent command.
  • !! refers to the last command and is what powers the sudo !!. I use this all the time.
  • !string refers to the most recent command starting with string.
  • !?string? refers to the most recent command containing string. Omit the trailing ? if you want to match string at the end of the command, rather than anywhere in the command.

Word selection is where it gets interesting. Normally, you’d have to delimit the word selection part with a :, but there are exceptions, which we’ll talk about. For this part, let’s say we want to select our previous command, using !!. Keep in mind !! can be replaced with whatever command you want to select (e.g. !-2 or !ls).

  • !!:n selects the nth word of the command, with the delimiting character being whitespaces, 0-indexed.
  • !!:n-m selects the nth to mth word, inclusive, 0-indexed.
  • !!:^ selects the first word (same as !!:1).
  • !!:$ selects the last word.
  • !!:* selects all but the zeroth word.

Again, I’ve omitted a few word selection commands, but I think these are the most important ones. For special characters like $ and *, we can remove the : delimiter entirely, abbreviating to !!$ and !!*. In fact, these two word selections are incredibly useful. Ever had to do a mkdir -p /a/very/very/long/path and then cd /a/very/very/long/path right after? Well, you can just do cd !!$ now! Or, have you ran something like ffmpek --there --are --so --many --options only to realize you spelt ffmpeg as ffmpek? No problem, just do ffmpeg !!*.

Lastly, we should talk about modifiers. Add another delimiter : and modify your bang command with a p to just print, instead of execute the command. For example, ls !!*:p will just print the command that would have been run had you not added the :p. The modifier s/old/new is string substitution, where / can be replaced with any other character (obviously, choose one that doesn’t appear in old or new). For example, if I had ran ls /path/to/long/path/with/spelligg/mistake/, I can make a quick fix with !!:s,spelligg,spelling,.

Whew, that was a lot. And there’s still so much more that I haven’t talked about. If you want to know more, I’ll kindly ask you to RTFM at the official docs 🙂

For me, the biggest takeaways are !!*, !!$, and !!:s/old/new/. The other stuff is powerful, sure, but honestly, I probably wouldn’t notice the difference between taking the slightly less efficient path by Ctrl+r’ing into the previous command and moving by word to edit the command most of the time. Not only that, but remembering the more niche tricks and then learning applying them efficiently isn’t free! In fact, I must confess that much of the reason for writing this post is so I don’t forget it myself…

Cheers, and happy hacking.

Photo by Jake Walker on Unsplash.

Leave a Reply

Your email address will not be published. Required fields are marked *