Shell 中的感叹号不是用来大喊大叫的。懒一点吧。
A shell exclamation mark is not for yelling. Be lazy

原始链接: https://refp.se/articles/your-shell-and-the-lazy-exclamation-mark

通过掌握 shell **事件指示符 (event designators)**,停止在终端中浪费时间和按键。与其“狂按向上键”,你可以使用感叹号语法来动态引用之前的命令及特定的参数。 核心概念包括: * **事件指示符:** 引用历史命令(例如 `!!` 重复上一条命令,`!ssh` 运行最后一条以 "ssh" 开头的命令)。 * **单词指示符:** 提取命令的特定部分(例如 `!$` 返回最后一个参数;`!:$:h` 提取目录路径)。 * **修饰符:** 对命令或参数执行文本操作(例如 `:r` 去除扩展名,`:s/old/new/` 进行替换)。 如果你使用的是兼容 POSIX 的系统,`fc` 命令是一个重要的替代方案,它允许你在执行前于首选的文本编辑器中编辑之前的命令。 为了避免误操作,请在 shell 配置中启用 `HIST_VERIFY`;这会在你按下回车前,将快捷方式在命令行中展开以供检查。归根结底,这些快捷方式不仅仅是技巧,更是帮助你在命令行遵循“DRY”(不要重复自己)原则的工具,将重复的终端任务转化为高效的工作流。

这篇 Hacker News 帖子讨论了使用 Shell 感叹号(如 `!!`、`!$`)来重新运行或引用先前命令的实用性。 虽然原帖提倡将这种技巧作为一种“懒人”生产力捷径,但评论区的看法存在分歧: * **认知负荷:** 一些用户认为,依赖 Shell 快捷键需要准确回忆过去的命令,这会增加心理负担。许多人宁愿重新输入命令或使用 `Ctrl+C` 重新开始,也不愿中断工作流程去排查语法问题。 * **替代方案:** 更好的替代建议包括使用 `Ctrl+R` 进行历史记录搜索,或者采用终端文件管理器(如 `lf`)来避免复杂的导航。 * **复杂性与安全性:** 持怀疑态度者指出,Shell 快捷键会使命令历史变得杂乱且难以审计。此外,还有一个关于安全性的担忧,即大语言模型(LLM)最终可能会利用这些隐晦的快捷键,在用户批准的脚本中掩盖恶意命令。 归根结底,尽管这些快捷键在任何地方都适用,但用户们强调,相比于依赖依赖记忆的 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 — 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]              

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 
$ sudo !!                     
$ mkdir /var/mnt/cache 
$ cd !$                

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 
$ ssh 10.240.33.109 -p2222 -i ~/.ssh/prod/ed25519
$ ssh 10.240.33.110 !:2* 
$ 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 is  this magic  an event designator?

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

Note: Want to know the bare necessities? Check out yell responsibly for the essential designators I consider most useful.

Note: Already fluent? Skip to posix and the power of fc 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:

$ !!           
$ !-2          
$ !1337        
$ !ssh         
$ !?dandelion? 
$ !#           
$ ^ssh^scp^    

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   
$ !:1   
$ !:$   
$ !:1-2 
$ !:*   
$ !:1-  
$ !:2*  

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 
$ !:1:t 
$ !:1:r 
$ !:1:e 
$ !:s/hello/bye/ 
$ !:gs/foo/bar/  
$ !ssh:p 

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      
$ fc -2   
$ fc grep 
$ fc -3 -1  
$ fc ssh -1 
$ fc -s ssh             
$ fc -s hello=world 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

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.

See below for what I consider the essentials, memorize a few of these and you are already miles ahead compared to your former self.

  • The rule of three

    • !! which expands to the previous command
    • !needle which expands to the most recent command starting with "needle"
    • !?needle? matches the most recent command containing "needle"
  • The powerful four

    • !$ the last argument of the previous command
    • !:0 when you need to rerun that annoyingly located script
    • !$:h for an easy extraction of the directory part
    • !$:t for when you want the filename and nothing else

I don't trust my exclamation-mark-fu just yet, any advice?

There is shopt -s histverify for bash, and setopt HIST_VERIFY in zsh.

Rather than YOLO-running the command directly, either configuration will expand commands in-place when you press enter — allowing you to edit (or discard) the result if it is not what you aimed for.

You may also use the :p-modifier to print what would run without running it — the expansion lands in your history so a plain !! (or arrow-up + enter) runs it once you are ready.

So, what now?

I guess there is nothing left to do but practice.

Please refrain from yelling at your colleagues, spend the saved keystrokes on watercooler chitchat, and forever remember that exclamation marks are not for shouting — at least not in your shell.

Frequently Asked Questions

If you have a question or feedback of your own, please feel free to shoot me an email at [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='%^#' 
    $ 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:

Documentation relevant to event designators:

联系我们 contact @ memedata.com