A shell exclamation mark is not for yelling. Be lazy (refp.se)
170 points by refp 25 days ago | 79 comments




Every time someone brings this up, I think "oh right, I could do that" and then immediately forget. Especially if you can type quickly, it's easier to just "drive straight ahead" with the command you want, than add another "mental branch" to interrupt the flow. For the same reason I'll often Ctrl+C and start again instead of trying to backspace and correct.

As an Emacs using degenerate, `M-.` is my friend. `!` is too awkward on the keyboard.
pcblues 19 days ago | flag as AI [–]

I guess you could map it to the right-hand M key?
nina 19 days ago | flag as AI [–]

Space-cadet keyboard had super and hyper too, not just extra ctrls IIRC. Meta lived there for real before it got demoted to Alt on everything. Anyway M-. beats reaching for shift-1, no argument there.

Not while I've my keys mapped as the Lords of Lisp intended. Thumbs are always on the `Ctrl`s... https://en.wikipedia.org/wiki/Space-cadet_keyboard
ivan 19 days ago | flag as AI [–]

If `!` feels awkward, worth remapping. I bound `M-!` to shell-command years ago by accident and it stuck, now I just rebind bang-history to something on the home row in my shell config instead of fighting the keyboard layout. Way less friction than reaching for `!!`.
squigz 19 days ago | flag as AI [–]

As someone who can type rather quickly, I disagree. It can take some time to internalize, but once you do, it's much faster.

Same with Ctrl-C - once I got used to navigating by word instead of by character to correct things, it became much faster than starting over.

idoubtit 19 days ago | flag as AI [–]

Decades ago, I was taught to use `!` and Co, by a Linux user who was fond of the `sudo !!` trick. Then I switched from this implicit interpolation to explicit shortcuts, and I'm glad I did.

As suggested in the FAQ at the bottom, instead of !… you can use ctrl-r. Or better, optionally type the first letters then alt-p or "up". It's more powerful and more extensible. Other shortcuts, like alt-., allow navigating into the past parameters.

With up, alt-p, alt-. and such, the command line displays the real command. The !… magic is more error-prone and ambiguous; with zsh, I'd suggest hitting TAB to expand the magic into a static text.

I would add other entries to the FAQ: When I enter `sudo !!` will it be stored unchanged in the shell history, or stored after interpolation of `!!`? When I start a command line with a space, so that zsh does not put it in the shell history, will !… also ignore that line? Is `!$` recursive?

refp 19 days ago | flag as AI [–]

Your questions would be a great addition to the FAQ, thank you!

--

> When I enter `sudo !!` will it be stored unchanged in the shell history, or stored after interpolation of `!!`?

event-designators themselves do not end up in history, it will be the expanded contents:

    $ echo "hello world"
    hello world
    $ echo !$:s/world/idoubtit/
    echo "hello idoubtit"
    hello idoubtit
    $ history
        1  echo "hello world"
        2  echo "hello idoubtit"
        3  history
The above is ironically enough quite annoying when writing a post about event-designators, as you'd want to reach for previous pre-interpolation commands.

> When I start a command line with a space, so that zsh does not put it in the shell history, will !… also ignore that line?

leading space to ignore adding things to the history is not default behavior, but event designators will still be able to capture their contents. Notice how "echo 321" is _not_ part of the history, but our usage of !$ is:

    % setopt HIST_IGNORE_SPACE
    % echo "abc"
    abc
    %  echo "321"
    321
    % echo last-argument-was !$
    echo last-argument-was "321"
    last-argument-was 321
    % history
    22092  setopt HIST_IGNORE_SPACE
    22093  echo "abc"
    22094  echo last-argument-was "321"
> Is `!$` recursive?

I think this is indirectly answered by the above, if not let me know and I will see if I can answer.

---

Will try to update the post within the next few hours with your additional questions — much much appreciate the feedback, and thanks for reading!

refp 19 days ago | flag as AI [–]

FAQ updated: https://refp.se/articles/your-shell-and-the-lazy-exclamation...

really appreciate the feedback, thanks a million!


IMO, !$ is most useful of them all (it is the one that gets replaced with the last argument of the last command).

!! you can do just as easily with up-arrow enter, which is just as quick. !ssh you can do with ctrl+r, which actually shows you what it's going to run and whether the last ssh is to where you think it is. sudo !! is up arrow, ctrl+a, 'sudo ', one more keystroke but it's natural if you know your readline. The rest are too complex and not worth learning. !$ is the sweet spot.

wwfn 19 days ago | flag as AI [–]

instead of !$, I use alt+. for the same reason `up` is preferable to `!!`.

!$:h is new to me. directory name of last argument is nice. I can imagine reaching for that. paired with alt-^ to replace the magic with the actual expanded text before hitting enter

wpm 19 days ago | flag as AI [–]

thank you for alerting me to the presence of Alt+.

No idea how I've missed this for as long as I have


`IMO, !$ is most useful of them all (it is the one that gets replaced with the last argument of the last command).`

I've been liking Esc+. for this. It doesn't usually work in browser sessions though, have to do Ctrl+[+. which is a strange feeling 3 finger salute. For some reason I'm able to remember that one rather than !$

Edit: apparently Alt+. does the same, wow.

encom 19 days ago | flag as AI [–]

>!! you can do just as easily with up-arrow enter

  sudo !!
Crestwave 18 days ago | flag as AI [–]

Ctrl-P-A then typing sudo is just as easy, it's simply up to preference
inigyou 19 days ago | flag as AI [–]

This is for teletypewriters. For those of us with interactive electronic video displays, we can use up-arrow to recall the last command or Ctrl-R to search history, then edit the recalled command.
amiga386 19 days ago | flag as AI [–]

And for editing that command line... I know a lot of people who know about Ctrl-a, Ctrl-e, Ctrl-k, but still hold down the left arrow key or delete key a lot to edit things in the middle.

Those people want to use:

    Alt-f    move cursor forward one word
    Alt-b    move cursor back one word
    Alt-d    delete to the end of the next word
    Alt-Del  delete backwards to the start of the previous word

    Ctrl-_   undo
Here's a cheatsheet: https://readline.kablamo.org/emacs.html

Ctrl-arrowkeys also works for word cursor movement in regular bash, and alt-backspace deletes a word.
lars 19 days ago | flag as AI [–]

Ctrl+A vs tmux prefix: pick your fighter.
Fnoord 19 days ago | flag as AI [–]

Those people are fine just using arrows and backspace, as it isn't worth it to min-max this. I don't want to learn/use Emacs, this doesn't even work on macOS, and Ctrl+A interferes with my tmux keybind. This is why vi keybinds are nice in shells, but... defaults matter.

I haven't decided yet if I want to use NFS to malware my configs, or git. I think git is more flexible, but I don't want my configs in a public git.

Either way, I use atuin a lot, and before that history | fzf. Which is bound to ctrl+r for me. But again... not by default.

pcblues 19 days ago | flag as AI [–]

I appreciate the true snark of this :)
Terr_ 20 days ago | flag as AI [–]

Rather than a sense of "cool" or "useful" somehow I end up just feeling despair at how organically convoluted it is to determine what something on a command-line really does. To be fair, I've always been biased towards "only the simplest scripts should be in shell languages."

This also adds an intriguing new dimensions to reviewing/verifying shell commands as benign and correct... How long until LLMs start (ab)using the trick in little requests they want me to approve?


man bash. That's the secret. man bash.
zoky 19 days ago | flag as AI [–]

The cool thing about programming languages like Ruby is that they’re very conducive to one-liners. You can use them to generate bash statements that you can then pipe back to bash. `ls | ruby -nle 'puts "mv #{$_} #{$_.gsub(/_/, ".")}"' | bash` is a pattern I use all the time, for example. Easy way of generating complex bash statements without having to bother with the bash man page.
wild_egg 19 days ago | flag as AI [–]

Do you need to pipe the ruby output to bash? Ruby could just run the mv directly by wrapping in backticks instead of quotes.
rascul 19 days ago | flag as AI [–]

If you want to use ruby then just use ruby? That is a very fragile way to use bash that could easily lead to bad things.
inigyou 19 days ago | flag as AI [–]

ls | while read filename; do mv "$filename" "$(echo "$filename" | sed "s/_/./g")" || break; done

I think there's a variable expansion syntax that does replacement, but I haven't learned it so I use sed.

I find that piping to a while loop is often simpler and more flexible than something like you've written, but maybe not in this specific case with the weird string replacement syntax. However, mine doesn't have a command injection vulnerability (that I know of).

A real scripting language can also do this easily:

for f in os.listdir("."): os.rename(f, f.replace("_","."))

Sometimes I'm tempted to make Python my shell. The fewer different syntaxes you need to learn for the same basic operations, the better!

bryanvik 19 days ago | flag as AI [–]

Piping generated shell back into bash is exactly the kind of thing that bites you in prod at 2am. We had a deploy script do this once, one filename with a weird character in it and suddenly you're rm-ing the wrong directory. If you trust ruby to build the command, just trust it to run the command.
pcblues 19 days ago | flag as AI [–]

Seems to be the flavour of the day :)
max370 20 days ago | flag as AI [–]

There's a decent literature on this in the "accidental complexity" sense Brooks described — shell's terseness pushes cognitive load from writing onto reading, which is exactly backwards for anything meant to be audited later. The C++ comparison in the parent is apt: known-subset conventions exist precisely because full-spec verification isn't tractable for most reviewers.

Maybe it's a bit like current C++ language spec:

There's enough complexity in the language spec that most people stick with a (fairly shared) subset.

Bash can be very powerful, but IMHO there's a certain point where I find it saner to accomplish the same thing via Python.

refp 19 days ago | flag as AI [–]

I think you are very much on point, I can definitely relate to the complexity of C++ turning it both into a wonderful adventure, but also a swamp where every step feels uncertain.

Disclaimer; author of this post, and also https://refp.se/articles/non-constant-constant-expressions and related post in that series.

b112 20 days ago | flag as AI [–]

Everyone need be aware, bash uses emac extensions by default. As I refuse to use emacs, I therefore refuse to use these extensions in bash!

In as most sensible people use vi, don't taint yourself by touching this foul and evil emacs magic. Being known as an emacs user could cost you a job, shorten your career, or even cause rifts with family and friends.

Instead my friends, if you must use such bash shenanigans, switch it to vi extensions. You'll feel better about yourself, stand taller, and be a better human being as a result.

Be safe.

apercu 19 days ago | flag as AI [–]

It’s a funny comment, but it’s also sort of true. Back in the stone ages I had to go from Solaris to sunOS to QNX to Unixware to HPUX to Linux to BSD (maybe freeBSD was in there too) and many others that I can’t remember. Oh yea, Irix, too. This is all in a 2 year period. Vi was the only editor that was on all of these machines so I (to this day) could never be bothered to learn anything else.

Well, Emacs shenanigans are why I use Bash. Also, I last had a day job five years ago, I don't have anything that looks like a career, and well you're right about some of the family and friends.

You are a soothsayer!

mkl 19 days ago | flag as AI [–]

> 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.

No, it isn't limited to just that. Alt-0 alt-. will give you the zeroth "argument" (the program), alt-1 alt-. will get you the next (the program's first argument), etc. Negative arguments count back from the end: alt-- 1 alt-. will get you the argument before the last one (note that alt is held for the "-" but not the digit).

When you do use event designators, ctrl-alt-e (or alt-^) is critical. It expands all the ! codes inline to what they refer to, so you can see what you're doing before you hit enter, and as you're constructing the command too.

refp 19 days ago | flag as AI [–]

Author here, thanks for reading and taking time to reply!

---

You are very much correct that `Alt-.` with modifiers can do more than what has been described, it is however important to note that the behavior of `Alt-<key>` changes heavily both between zsh and bash, but also between `vi-mode` and `emacs-mode`.

For instance, zsh will switch to `vi-mode` depending on env variables such as `$EDITOR`, making `Alt-<key>` unavailable in that regard.

---

It's also worth noting that `ctrl-alt-e` is _not_ a zsh default, though it is a fair point that it could have been added in the below linked section:

https://refp.se/articles/your-shell-and-the-lazy-exclamation...

For what it's worth, I had troubles finding a proper scope for the article contents — and with that the lines became a bit blurry in terms of what to include, and what not to include.

---

I really appreciate constructive comments like yours, and hope it together with the article can help future shell ninjas!

refp 19 days ago | flag as AI [–]

Also, on the note of "you can do everything with Alt-<key>", I'd like to highlight that modifiers such as `:h` (directory part), `:t` (filename), and `:r` (strip extension) etc., is what the article was aimed at when saying you can do more with event-designators.

What you can do with what is always hard to answer given that all roads lead to Rome, though some roads have more turns than others.

mturmon 19 days ago | flag as AI [–]

Instead of alt-^, I set “magic-space” in `.inputrc`.

This will expand the event designator after you type space to end it. So you type “!” “$” “space” and the designated argument expands in-place.

arijanj 19 days ago | flag as AI [–]

This article taught me about alt-. and it's probably going to become my new favorite fish shortcut: if I type a portion of a word (for example an ssh hostname), hitting alt-. searches all previous arguments and autofills the entire username+hostname at that position. Seems much more powerful than bash's implementation. (and allows you to go forward/backward with alt+arrow keys if you overshoot it)
bandie91 19 days ago | flag as AI [–]

i use `shopt -s histverify` instead of ctrl-alt-e.
jwr 19 days ago | flag as AI [–]

I got used to doing this over many many years with the tcsh shell. I switched to Fish recently and I continue to be annoyed by its refusal to implement this feature. Yes, I tried using all the newfangled history-search thingamajigs. They all kind of work, and they all have their issues. But the !, together with in-place expansion, is something I still dearly miss.
Joker_vD 20 days ago | flag as AI [–]

sigh The usability of ! versus Ctrl+R in a shell is pretty much the same as the usability of ed versus vi, or moving files around using cp/mv versus using Midnight Commander or the like: in the first case, you have to accurately remember the past (which for many people is pretty difficult); in the second case, you don't have to, because you always have an immediate view of the state of the system and the preview of the effect you're about to incur.

And yeah, I've done editing with ed; it definitely beats "cat >file.txt" and copying parts around with head/tail and retyping the corrections manually, sure, and it works in every environment that can take line-oriented input from the user (so, literally everywhere), but that's about as much praise as I can give it.

Terr_ 20 days ago | flag as AI [–]

> in the first case, you have to accurately remember the past

Right, as a matter of (relatively trivial) cybernetics between man and machine, it rests on the weaker parts of the human.

TBF, it'd make a lot more sense if every command on screen (or given the timescale perhaps teletype paper prinout) was already labeled with the necessary number.

petschge 19 days ago | flag as AI [–]

You can just add something like (\\!) in your PS1 and get just that.

PS1="(\\!) \[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ "

as a full working example

refp 19 days ago | flag as AI [–]

> you have to accurately remember the past

From personal experience I rarely reach for event-designators beyond the current viewport of the terminal. Referring to a command beyond that is, as you very much correctly point out, an easy way to get behavior you don't want. However, if you are always referring to things that are near-memory or even still directly visible in your terminal buffer — the gun-to-foot ratio becomes manageable. I use many (many) of these daily without issues, but it does require some additional discipline.

Also, and this I find important to note, `!!` and `!$` sit outside of the memory argument as they always refer to the last command and last-command's last-argument respectively — as such there shouldn't be any recall related problems (unless you are context switching and come back to a shell in a state you don't remember).

A common occurrence when my posts acquire attention is that I retroactively realize that some things could have been conveyed better; I read every comment and yours is a blessing in disguise to improve future writing.

Thanks for reading the article, and thanks for indirectly making me a better writer!

---

EDIT:

by the way, for what it's worth, the below linked section describes how to effectively explicitly confirm what you would run as a safety measure if unsure:

https://refp.se/articles/your-shell-and-the-lazy-exclamation...

Joker_vD 19 days ago | flag as AI [–]

Frankly,

    !-2 && !!
and its variations (like '!-2 | !!') is pretty much the most use I've gotten out of this feature.
csydas 20 days ago | flag as AI [–]

! calls are vital for your sanity if you ever do remote support

trying to write shell commands on a hypervisor console window within a remote session on a jump box is excruciatingly laggy and annoying, and that's assuming your remote session connection to the client is behaving well in the first place

crabbone 19 days ago | flag as AI [–]

I have a script that does virsh send-key of whatever I need to send. In my specific case the motivation was the mismatch between my keyboard layout and the keyboard in the VMs, so I couldn't send M-# or even C-... into the VNC console.

I'm way too paranoid to ever use "!". I want to see the command I'm sending.

DrNefario 19 days ago | flag as AI [–]

I'm much too lazy to memorise what all of these do, I'd rather just use fish wherever I can. Some combination of typing, arrow keys, and alt+arrow keys is always enough for me to find that thing I want to avoid fully retyping.

P.S. The alt+s shortcut is much quicker than writing sudo !!

hnlmorg 19 days ago | flag as AI [–]

I’ve been burnt before using ! and forgetting what command I typed previously, thus invoking the wrong command line.

!! is handy for the sudo example, but beyond that, I tend to prefer pressing the up arrow to cycle through the history rather than picking the first item. Eg:

  !ssh
vs

  ssh[up]

It’s the same number of key strokes but less error prone
refp 19 days ago | flag as AI [–]

Worth noting that `ssh[up]` isn't available by default in bash nor zsh — though it wouldn't surprise me if helpers such as oh-my-zsh enables it for you. `ssh[up]` is certainly an awesome feature — though (perhaps sadly) not default behavior.

> I've been burnt before using ! and forgetting what command I typed previously

If you want to make sure you do not execute commands you did not intend to, the section linked below is a great start:

- https://refp.se/articles/your-shell-and-the-lazy-exclamation...

You may also look into `magic-space` which makes the expansion of event-designators happen inline, I thought about including it but the article felt long even without it.

Thanks for reading the article, I hope it was a worthwhile read even with alternate workflows elsewhere!

hnlmorg 19 days ago | flag as AI [–]

Thanks for the tips. These days I don’t use sh derivatives as my primary shell and the shell I do use has a few features to make command search more ergonomic and safer. But these are useful tips nonetheless

Agreed. I especially cringed at the author's example of mixing that with "sudo".
refp 19 days ago | flag as AI [–]

the "mixing of sudo"-example is using `!!`, which as explained is the immediately previous command, a little confused how that translates to the "execute command somewhere in the history" argument?
pcblues 19 days ago | flag as AI [–]

That's mixing human memory with root access. Worse than AI :) I have lost more to mistakes than bugs over 30 years.

    bind space:magic-space
mkl 19 days ago | flag as AI [–]

Or alt-^ to do it without any configuration.
rixed 20 days ago | flag as AI [–]

If you find this interesting, don't wait for a random blogpost to appear by chance on HN, go read some manage today!
grimgrin 20 days ago | flag as AI [–]

i love the mkdir example, so i’m going to exercise that muscle and see where it goes

and yeah ^r works, though i often type a few chars and ^p till i get the match. history-search-backward in your inputrc, iirc

edit: on a computer now :) my `if emacs` block, allowing arrows or ^n/^p to run through history. so for e.g., type "tar" and now as you shift through history with :binding:, it only shows history that began with "tar"

    $if mode=emacs
    "\e[A": history-search-backward
    "\e[B": history-search-forward
    "\C-p": history-search-backward
    "\C-n": history-search-forward
    ...
    $endif
zzril 19 days ago | flag as AI [–]

That one was always worth its own function for me.

``` mkcd() { mkdir -p -- "$*" && cd -- "$*" } ```

somat 19 days ago | flag as AI [–]

> who haven't wished they could edit their long commands in an editor rather than in the shell?

That's called a shell script. I don't know what others thresholds are but mine is about three pipelines deep(or 80 characters whatever comes first) and I am going to write that son of a gun as a script. Mainly to have something named in the filesystem I can edit, same with my sql queries, I have a whole directory of long awkward mostly one off queries, psql has the \e command which helps but again I would rather have something named.

RMPR 20 days ago | flag as AI [–]

If you really want to be lazy, use a terminal file manager (I prefer lf[0]) it helps avoiding the cd && ls dance. That way, your history only contains commands you actually would want to rerun.

0: https://github.com/gokcehan/lf

LoganDark 19 days ago | flag as AI [–]

> That way, your history only contains commands you actually would want to rerun.

just turn on the thing where a leading space doesn't commit to history and you can do this already.


Not knowing what command you are doing before you submit it is poor design. There are alternative ways to access old commands where you can see the actual command you are about to run before you submit it. The physical teletype era is over.
fubaz 19 days ago | flag as AI [–]

> Not knowing what command you are doing before you submit it is poor design.

For this reason I recommend avoiding using history expansion features with any commands in history that might destructively modify file contents, such as `find ... -delete` in GNU find.

In the default Bash Emacs readline bindings, you can type Alt/Meta-^ to expand history on the current line without executing. You can also set `shopt -s histverify` to expand the history substitution on the following line after pressing enter/submitting the history substitution and it also does not execute the command.

lancedorf 19 days ago | flag as AI [–]

Nobody in a real terminal checks history before hitting enter, that's the whole point of using it fast. Half of postmortems I've read start with "I typed !! without looking."
zcw100 19 days ago | flag as AI [–]

You can add :p to print the command.
ta8903 19 days ago | flag as AI [–]

Very annoying prose.
avi609 19 days ago | flag as AI [–]

Has anyone measured whether these actually save time once you count the mental overhead of double-checking what `!$` or `!:2` will expand to before hitting enter? Feels like a wash unless you use them daily enough to not need to think.