A shell exclamation mark is not for yelling. Be lazy

Lobsters Hottest Tools

Summary

An educational article explaining shell event designators (history expansion) like !! and !$ to reuse and modify previous commands lazily in bash, zsh, and other shells.

<p><a href="https://lobste.rs/s/0mjyep/shell_exclamation_mark_is_not_for_yelling">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 08/07/26, 04:17 AM

# A shell exclamation mark is not for yelling. Be lazy. | Filip Roséen - refp.se Source: [https://refp.se/articles/your-shell-and-the-lazy-exclamation-mark](https://refp.se/articles/your-shell-and-the-lazy-exclamation-mark) ## 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`, and`zsh`\. If these shells are not your daily driver, see[POSIX and the power of`fc`](https://refp.se/articles/your-shell-and-the-lazy-exclamation-mark#posix-and-the-power-of-fc)— worth a read no matter which POSIX\-compatible shell you use\. ### Command\-line repetition we socially accept ``` $ ssh [email protected] $ echo "done with [email protected]" >> ~/work/superfun-client/worklog $ cd ~/work/superfun-client && cat TODO $ ssh [email protected] # <- 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? ``` $ ssh [email protected] $ echo "done with !:1" >> ~/work/superfun-client/worklog $ cd !$:h && cat TODO $ !ssh ``` No yelling — just exclamation marks\. > **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\.sh`shell script\. ## Don't Repeat Yourself\. I bet you've heard it before, "do not repeat yourself", 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`\. ``` $ apt install awesome-package # insufficient permissions $ sudo !! # repeat with sudo ``` ``` $ mkdir /var/mnt/cache # the usual create directory dance $ cd !$ # cd to the directory, no finger strain needed ``` 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? ``` $ 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 ``` ## What isthis magican event designator? Even though they might look mighty daunting at first glance,[event designators](https://web.mit.edu/gnu/doc/html/features_6.html)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? ... ] ``` > **Note**: Want to know the bare necessities? Check out[yell responsibly](https://refp.se/articles/your-shell-and-the-lazy-exclamation-mark#yell-responsibly)for the 4 event designators I consider most useful\. > **Note**: Already fluent? Skip to[posix and the power of`fc`](https://refp.se/articles/your-shell-and-the-lazy-exclamation-mark#posix)for one of the most useful albeit niche commands I have ever had the pleasure of running — who haven't wished they could edit their long commands in an editor rather than in the shell? ### Event Designator The first part immediately following the exclamation mark denotes which lines we are interested in — here are a few examples which will effectively rerun the referred to command: ``` $ !! # 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 $ ^ssh^scp^ # replace first ssh with scp in previous command ``` > **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 # 1st to 2nd --> "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\. ``` $ !:$: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' ``` ``` $ !ssh:p # print what would run, without running it ``` ## POSIX and the power of`fc` Whether or not your shell speaks`\!`,`fc`belongs in your imaginary tool belt\. It can be used to repeat the previous command, but the true power lies in the fact that you can edit your command\-line not in your shell — but in your editor of choice\. Every shell adventurer should run`fc`at least once a year, if only to marvel at its usability: ``` $ fc # edit the previous command $ fc -2 # edit the command 2 steps back $ fc grep # edit last command starting with grep ``` ``` $ fc -3 -1 # the last three commands, all in one buffer $ fc ssh -1 # from the last 'ssh' line through the most recent one ``` ``` $ 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`\. You may also specify which editor to invoke using`\-e`: ``` $ scp api.tar.gz prod-01:/tmp $ ssh prod-01 systemctl restart api $ curl -sf https://prod-01/health ``` ``` $ fc -e 'sed -i s/prod-01/test-01/g' -3 -1 ``` The above will rerun all three commands, replacing`prod\-01`with`test\-01`in each; a contrived example perhaps but man is it useful when you find yourself in such trouble\. > **Note** - `fc`lives in the[User Portability Utilities](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap02.html#tag_02_01_06_02)of POSIX\. - Truly minimal shells like`dash`and busybox`ash`ship without it entirely\. ## Yell responsibly Event designators, or`fc`in the case of[POSIX](https://refp.se/articles/your-shell-and-the-lazy-exclamation-mark#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 - `\!:0`when you need to rerun that annoyingly located script again - `\!$:h`for an easy extraction of the directory part - `\!$:t`for when you want the filename and nothing else We might yell at each other when discussing the best editor, but in our shells we may 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](mailto:[email protected])\. - ### Why not use`ctrl\-r`instead 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?`\!ssh`vs`ctrl\-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 found ``` If you would like to turn things off so that you can use`\!`anywhere, you may use`set \+H`in*bash*and`setopt nobanghist`in*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 internet ``` Remember to put your configuration in the relevant dot\-rc file to make the changes persistent\. ## Further Reading Other articles in this series: - [A shell colon does nothing\. Use it anyway \(refp\.se\)](https://refp.se/articles/your-shell-and-the-magic-colon) Documentation relevant to event designators: - [Event Designators \(gnu\.org\)](https://www.gnu.org/software/bash/manual/html_node/Event-Designators.html?pubDate=20260429) - [Word Designators \(gnu\.org\)](https://www.gnu.org/software/bash/manual/html_node/Word-Designators.html) - [Designator Modifiers \(gnu\.org\)](https://www.gnu.org/software/bash/manual/html_node/Modifiers.html)

Similar Articles

POSIX Is Not a Shell

Hacker News Top

This article argues that POSIX is a specification, not a program, and that shell implementations such as bash and dash behave differently even for basic commands like echo. It calls for testing across multiple shells rather than relying on the notion of 'POSIX compliance' for portability.

A Comma and a Question Mark

Hacker News Top

A developer creates a personal tool that integrates a local AI model (Qwen 3.6 via llama.cpp) and a small agent (Pi) into the zsh terminal, using a comma prefix to suggest shell commands and a question mark to answer questions, with an emphasis on safety by only proposing commands rather than executing them.

Odd Comments and Strange Doings in Unix

Lobsters Hottest

Dennis Ritchie recounts quirky error messages and comments from early Unix source code, including the famous 'values of β will give rise to dom!' and the unlikely comment 'You are not expected to understand this.'