Don't yell at your colleagues; yell in your shell
You and I are probably very different; my choice of editor is likely to make you cringe just thinking about it, and your favorite programming language is statistically — definitely — not mine.
There is however one thing (and maybe that's the only thing) we all have in common; we are a bunch of lazy bastards.
Note: This article is primarily targeting
bash,csh,tcsh, andzsh; if these shells are not your daily driver — check out the POSIX section for what works everywhere™.
Command-line repetition we socially accept
$ ssh some-user@10.240.33.109
$ echo "done with some-user@10.240.33.109" >> ~/work/superfun-client/worklog
$ cd ~/work/superfun-client && cat TODO
$ ssh some-user@10.240.33.109 # <- forgot something
I am sure we have all ended up in arrow-up-mashing hell when repeating commands in our shell history, but what if we instead could save ourselves the trouble and refer to previous commands and their arguments directly?
No yelling — just exclamation marks:
$ ssh some-user@10.240.33.109
$ echo "done with !:1" >> ~/work/superfun-client/worklog
$ cd !$:h && cat TODO
$ !ssh
Note: The magic described in this article applies to interactive shells unless configured otherwise, it is not meant to be used in your new
amazing.shshell script.
Don't Repeat Yourself.
I bet you've heard it before, either from that
annoying colleague with all the obscure magic (who might smell a little funny) —
or perhaps you said it yourself during the last refactor session that somehow lasted
longer than the federation delay on matrix.org.
In either case most of us have heard DRY repeated since we were ki–, I mean junior. But is it not funny that we've religiously been taught to apply DRY everywhere… except on the command‑line?
$ apt install awesome-package # insufficient permissions
$ sudo !! # repeat with sudo
$ touch ~/projects/work/awesome.sh
$ cd !$:h # cd to the directory of awesome.sh
$ ssh 10.240.33.109 -p2222 -i ~/.ssh/prod/ed25519
$ ssh 10.240.33.110 !:2* # same flags, another host
$ ffmpeg -i /recordings/a-sunny-day.mov !#:2:r.mkv
ffmpeg -i /recordings/a-sunny-day.mov /recordings/a-sunny-day.mkv
$ scp !$:r.* example.com:/media
scp /recordings/a-sunny-day.* example.com:/media
An in-depth explanation of Event Designators
Even though they might look mighty daunting at first glance, event designators always follow the same rather simple pattern:
![event][:word][:modifier]
| | |
| | '--> modifier [ :h :t :r :e ... ]
| '-----------> which part [ :0 :$ :* :2-3 ... ]
'------------------> which line [ !! !-2 !ssh !?needle? ... ]
Event Designator
The first part immediately following the exclamation mark denotes which lines we are interested in — a few examples:
$ !! # the previous line
$ !-2 # two lines back
$ !1337 # the 1337th line, as displayed by `history`
$ !ssh # the most recent line starting with "ssh"
$ !?dandelion? # the most recent line containing "dandelion"
$ !# # the current line, written so far
Note: If the
!is immediately followed by:, no explicit event is specified and the expression will refer to the immediately previous line.
Word Designator
After the event designator (if any) you may specify which part of the line you are interested in. All examples below are written as if they follow the command in the first code block.
$ /path/to/script.sh "hello world" --enable 1337
$ !:0 # 1st word => "/path/to/script.sh"
$ !:1 # 2nd word => "hello world"
$ !:$ # last word => "1337"
$ !:1-2 # 2nd to 3rd => "hello world" "--enable"
$ !:* # all args => "hello world" "--enable" "1337"
$ !:1- # all but last => "hello world" "--enable"
$ !:2* # 3rd to last => "--enable" "1337"
Note: There are several short-form expressions that work without even specifying a colon, like
!$being equivalent to!:$,!*being equivalent to!:*, and so forth.
Modifier
Modifiers are perhaps where we enter "oh-daaaaaaaym"-territory — they allow you to extract/modify only certain parts of what would otherwise be an entire argument.
$ !ssh:p # print what would run, without running it
$ !:$:h # strip filename (`dirname`)
$ !:1:t # strip leading path (`basename`)
$ !:1:r # strip only the extension
$ !:1:e # leave only the extension
$ !:s/hello/bye/ # replace first 'hello' with 'bye'
$ !:gs/foo/bar/ # replace all 'foo' with 'bar'
POSIX
If you are working in a shell which does not support event designators,
everything above is beyond reach — but fear not young padawan; you have fc
which can honestly be just as powerful.
$ fc # edit the previous command
$ fc -2 # edit command 2 steps back
$ fc grep # edit last command starting with grep
$ fc -s ssh # repeat last command starting with 'ssh'
$ fc -s hello=world ssh # replace "hello" with "world" on the line starting with 'ssh'
fc will invoke whatever program is specified in $FCEDIT to do the editing,
if no such specification exists POSIX falls back to ed though many shells
will default to $EDITOR.
Note
fclives in the User Portability Utilities of POSIX.- Truly minimal shells like
dashand busyboxashship without it entirely.
Yell responsibly
Event designators, or fc in the case of POSIX, are not going to change your
life and turn you into an über-10x-developer-always-wearing-a-hoodie; they might
however save you anywhere between four and forty-two keystrokes at a time, a few
hundred times a week, over the many years spanning your career.
For what it's worth, it is often enough to simply remember four of them:
!$the last argument of the previous command:hfor an easy extraction of the directory part:tfor when you want the filename!:0when you need to rerun that annoyingly located script a 2nd time
We might yell at each other when discussing the best editor, but in our shells we will forever whisper: "I am lazy, and the exclamation mark is my weapon".
Frequently Asked Questions
If you have a question or feedback of your own, please feel free to shoot me an email at filip.roseen@atch.se.
Why not use
ctrl-rinstead of memorizing a bunch of things?ctrl-r, a.k.a. command-line history search, is extremely powerful, but it will at best give you a template to modify; event designators allow you to extract partial contents.Also, why search if you already know what you want to run?
!sshvsctrl-r+ssh— the former is also kinda cooler™.Why not use
alt-.instead of!$?alt-.is great for what it is designed to do; iterating over the previous "last arguments" of your shell history. It's great at doing that, but it is also limited to doing just that one thing.Are you not disregarding other useful features?
I am not aiming for some sort of "either or" situation when I publish my articles — whatever gets the work done is what you should use.
And for what it's worth… never do what a stranger tells you online; personal preference and workflows are worth more than any article (no matter the amount of obscure sometimes forgotten magic).
!just gets in the way, how do I turn this off?I bet many have been bitten by the "usability" of event designators without realizing what they are for, such as in the example below:
$ echo "!dlrow olleh" bash: !dlrow: event not foundIf you would like to turn things off so that you can use
!anywhere, you may useset +Hin bash andsetopt nobanghistin zsh; for other shells I recommend consulting your manual.You may also go full smelly-colleague-with-magic (please wear deodorant in public), and instead use any character of your liking:
$ histchars='%^#' # event-trigger, substitution, comment $ echo "hello world" hello world $ echo %$:s/world/the internet/ hello the internetRemember to put your configuration in the relevant dot-rc file to make the changes persistent.