116 comments

[ 3.4 ms ] story [ 107 ms ] thread
Not a fan of the LLM-flavoured headings, and the tips seem like a real mixed bag (and it'd be nice to give credit specifically to the readline library where appropriate as opposed to the shell), but there are definitely a few things in here I'll have to play around with.

One thing I dislike about brace expansions is that they don't play nicely with tab completion. I'd rather have easy ways to e.g. duplicate the last token (including escaped/quoted spaces), and delete a filename suffix. And, while I'm on that topic, expand variables and `~` immediately (instead of after pressing enter).

Readline is close enough to being part of bash that it’s not really inaccurate to call these all shell features imo.
Not just the heading is LLM-flavoured. So is the writing, e.g. "The shell is a toolbox, not an obstacle course."
My favourite shell trick is to comment my code:

  $ some_long_command -with -args -easily -forgotten # thatspecialthing
... Some weeks later ..

  $ CTRL-R<specialthing>
.. finds:

  $ some_long_command -with -args -easily -forgotten # thatspecialthing

Need to see all the special things you've done this week/whenever?

  $ history | grep "\#"
...

Makes for a definite return of sanity ..

I once saw this pattern referred to as a bashtag, which I think was an excellent name (no matter if you actually run bash as your shell or not).
One of the peer comments alludes to this and mentions that it's bash only. Doesn't work in zsh by default apparently.
Never heard of instant truncate, nor `fc`, nor `Esc .`

Quite a few useful ones

A much larger base for ksh (as a pdksh descendent) is Android. OpenBSD is a tiny community in comparison, although Android has acquired code directly from OpenBSD, notably the C library.

The vi editing mode is always present in ksh, but is optional in dash. If present, the POSIX standard requires that "set -o vi" enable this mode, although other methods to enable it are not prohibited (such as inputrc for bash/readline), and as such is a "universal trick."

The article is relying on some Emacs mode, which is not POSIX.

$_ is not POSIX if I remember correctly.

History in vi mode is easier, just escape, then forward slash (or question mark) and the search term (regex?), then either "n" or "N" to search the direction or its reverse.

I've seen a lot of people who don't like vi mode, but its presence is the most deeply standardized.

CTRL + W usually deletes everything until the previous whitespace, so it would delete the whole '/var/log/nginx/' string in OP's example. Alt + backspace usually deletes until it encounters a non-alphanumeric character.

Be careful working CTRL + W into muscle memory though, I've lost count of how many browser tabs I've closed by accident...

I've installed "More Better Ctrl-W" for Chromium, and mapped Ctrl-W to do nothing, and Ctrl-D to close the current tab
...which is why I recently went to about:keyboard and removed that hotkey. I love that page.

That, and Ctrl-N. No more forest of blank browser windows when using a terminal emulator in a web page!

(Firefox only)

Depends on the shell - bash on my Ubuntu deletes entire '/var/log/nginx/', while after switching to sh it deletes only nginx
'man readline' contains all the useful key combinations.
> Be careful working CTRL + W into muscle memory though, I've lost count of how many browser tabs I've closed by accident...

You're telling me!!!

(I use vim daily, with multiple splits in a single instance.)

> Be careful working CTRL + W into muscle memory though, I've lost count of how many browser tabs I've closed by accident...

I still maintain this is why macOS is the best OS for terminal work -- all the common keybindings for GUI tools use a different modifier key, so e.g. ⌘C and ⌘W work the same in your terminal as they do in your browser.

(Lots of the readline/emacs-style editing keybindings work everywhere in macos as well -- ^A, ^E, ^K, ^Y, but not ^U for some reason)

100% agree, and I am surprised I do not see this mentioned more often. I came up on Linux and then had to use MacOS for a job and got used to the cmd / ctrl separation and now I cannot use a terminal on Linux without major pain. I've tried a few of the key rebinding options and they all feel klunky.
> The “Works (Almost) Everywhere” Club

> The Backspace Replacements

Also known as "emacs editing mode". Funnily enough, what POSIX mandates is the support for "vi editing mode" which, to my knowledge, almost nobody ever uses. But it's there in most shells, and you can enable it with "set -o vi" in e.g. bash.

With ctrl+r, if you press it twice, it will autofill the search with whatever you last searched for. pressing it more will go back through the history. Been using that a lot recently when doing docker stuff. ctrl+r, type the container name, keep going until I get the compose build command. ctrl+r, ctrl+r, repeat until the log command. Then I can just mash ctrl+r to get the build and log commands. Ctrl+r is your friend. ctrl+r
I love this, from a comment on the article:

  He had in his path a script called `\#` that he used to comment out pipe elements like `mycmd1 | \# mycmd2 | mycmd3`. This was how the script was written:
 
  ```
  #!/bin/sh
  cat
  ```
Wow I hate* that. I use bracket comments. They're cool cause they are bracket comments, so I use it in scripts to document pipelines. They are annoying cause they are bracket comments, in an interactive shell I have to type more and in TWO places. It's fun to reason-out how it works ;)

  $ echo foo | tr fo FO | sed 's/FOO/BAR/'
  BAR
  $ echo foo | ${IFS# tr fo FO | } sed 's/FOO/BAR/'
  foo
It's nice to have a way to both /* ... */ and // ... in shell scripts though:

  foo \
  | bar ${IFS Do the bar. Do it. } \
  | baz
* in the best possible way, like it's awful - I hate I didn't think of that
I think the script would be named as `#` so that it can be called via `\# mycmd` instead of `\\# mycmd`.
> cd -: The classic channel-flipper. Perfect for toggling back and forth.

And not only cd. Gotta love 'git checkout -'

I just open, agent in tui, and ask it to do what I want and make a plan, i read the plan edit it and run it.

Simple, no need to learn any commandline these days.

I used to use arch and all, and managed many big projects. I find little value in learning new tools anymore, just feed it docs and it generated working plan most of the time

Now I've moved to coding in Haskell, which i find suits me better than wasting my time with cli and exploring what options all these cli tools have.

There is no value gained if you never learn.
Using the terminal becomes much more cozy and comfortable after I activate vim-mode.

A mistake 3 words earlier? No problem: <esc>3bcw and I'm good to go.

Want to delete the whole thing? Even easier: <esc>cc

I can even use <esc>v to open the command inside a fully-fledged (neo)vim instance for more complex rework.

If you use (neo)vim already, this is the best way to go as there are no new shortcuts to learn and memorize.

I use vim a lot but not on the shell

A mistake 3 words earlier?

meta-bbbd (not as elegant, I admit)

delete the whole thing?

ctrl-ak (this is even quicker than vim, especially if capslock is mapped to ctrl)

the control-based emacs movements work system-wide on macos btw. I am using ctrl-p and ctrl-n to go up and down lines, ctrl-a and ctrl-e to go to beginning and end of lines while writing this comment in by browser (which has vimium extension)

Sometimes I wish vim just had full emacs bindings while in insert mode. But I don't like to mess with defaults too much.

I keep thinking I should give vim readline a try though, so maybe today. Thanks for the comment.

I've never understood why emacs mode became the default. "set -o vi" is the _first_ command I type in a new shell.
i went even further and use neovim as my multiplexer
Eh, it's not that different from the default readline commands

A mistake 3 words earlier? No problem: <esc>3 Alt-b Alt-d

Want to delete the whole thing? Even easier: Ctrl-U

I can even use Ctrl-X Ctrl-e to open the command inside a fully-fledged EDITOR instance for more complex rework.

Something that should be mentioned is starting a command with a space doesn't add it to your history in most shells, really useful for one-off commands that you don't want cluttering your history.

Also, increase your `$HISTSIZE` to more than you think you would need, there have been cases where it helped me find some obscure command I ran like 3 years before.

Also a good thing to remember if you are ever dealing with credentials:

  $  APIKEY=asdfdfds
  $ curl -H "x-api-key: $APIKEY" https://example.com
Ctrl-r works well at searching character trigrams, which can include space. Trigrams without space work well with auto_resume=substring .

`| sudo tee file` when current user does not have permission to >file

(comment deleted)
What confuses me is that Ctrl+Y "yank" means the opposite of what it means in Vim. Certainly does not help with keeping my sanity.
It all depends on your perspective.

Are you yanking into your kill ring or yanking out of your kill ring? I had trouble with yanking and killing until I realized the complement to yanking, killing, only makes sense in the into-the-kill-ring" direction, so yanking must be out of the kill ring.

When I use vim, which I don't think has a kill ring but registers, I think I am yanking into a register and then pasting from a register later.

So, just ask yourself this: "are you using a kill ring or register to store your text?" and the answer becomes clear.

That is because the terminology (and the keybindings) come from the Emacs tradition, not vim. Most shells come with “vim mode” as well, but at least in my experience, the dual mode editing paradigm of does not feel like a good fit for the shell.
One trick I use all the time:

You're typing a long command, then before running it you remember you have to do some stuff first. Instead of Ctrl-C to cancel it, you push it to history in a disabled form.

Prepend the line with # to comment it, run the commented line so it gets added to history, do whatever it is you remembered, then up arrow to retrieve the first command.

$ long_command

<Home, #>

$ #long_command

<Enter>

$ stuff_1 $ stuff_2

<Up arrow a few times>

$ #long_command

<home, del>

$ long_command

You missed an easier alternative that was in the article: ctrl-u saves and clears the current line, then you can input new commands, then use ctrl-y to yank the saved command.

With zsh, I prefer to use alt-q which does this automatically (store the current line, display a new prompt, then, after the new command is sent, restore the stored line). It can also stack the paused commands, e.g.:

$ cp foo/bar dest/ <alt-q>

$ wcurl -o foo/bar "$URL" <alt-q>

$ mkdir foo <enter> <enter> <enter>

Ctrl+q in zsh is much better, I don't remember if bash has it.

You write a command, you remember that you need to do something else first, press ctrl+q/the lines gets cleared, write a different command and after you press enter to run it the old command appears again :)

I'd advise against using sudo !! though since it adds the command to history and then it's very easy to accidentally trigger, running some undesired command as root without any prior confirmation. IMO pressing up, Ctrl-A and typing "sudo " isn't much longer but saves you from running unknown commands as root by accident
in zsh pressing esc twice appends sudo to the last command
Pressing up, Ctrl-A and typing "sudo " adds the command to history, too. What's the difference?
My favourite trick is either commenting out a whole command or placing a comment at the end of a command to make it easier to find in my persistent history (thanks eliben) [0], using the # character.

I tried this in zsh and it wasn't the default behaviour which immediately made me nope from the shell altogether, among all the other quirks. I've just been using bash for far too long to switch to something different.

[0] https://eli.thegreenplace.net/2013/06/11/keeping-persistent-...

What I hate is that if you start a command with a space it is not recorded in the history. This happens often when copy+pasting commands. I know you can turn it off but still ... this drives me mad.
There's one thing you need to only think about once, and has the potential to save you a ton of time: profile your ZSH startup time!

Stuff like NVM or Oh My ZSH will add a few seconds to your shell startup time.

Agreed. I lazy-load NVM to get around that:

  lazy_nvm() {
    unset -f nvm node npm npx
    [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
  }
  nvm()  { lazy_nvm; nvm "$@"; }
  node() { lazy_nvm; node "$@"; }
  npm()  { lazy_nvm; npm "$@"; }
  npx()  { lazy_nvm; npx "$@"; }
I didn't know the `ALT + .` trick to repeat the last argument, but what is even more neat (and not mentioned in the article) is that it cycles through your history. At least it does in my shell.
> We’ve all been there.

Close tab.

I ought to migrate away from shell scripting and just keep the shell for interactive use. Unfortunately I have cursed myself by getting competent-ish with P. shell and Bash scripting. Meaning I end up creating maintenance headaches for my future self.

(Echoes of future self: ... so I asked an LLM to migrate my shell scripts to Rust and)

Anyway with the interactive shell stuff. Yeah the I guess Readline features are great. And beyond that I can use the shortcut to open the current line in an editor and get that last mile of interactivity when I want it. I don’t really think I need more than that?

I tried Vim mode in Bash but there didn’t seem to be a mode indicator anywhere. So dropped that.

Edit: I just tested in my Starship.rs terminal: `set -o vi`. Then I got mode indicators. Just with a little lag.

I struggle with this all the time. It's easy to start with some shell commands, and nothing beats the direct terminal running nature of a shell script. But at some point you get to where you need to do things shell isn't good at, and the cost of maintaining that part can put weight the ease of direct terminal access. Especially if you then throw in the need to make your solution runnable on a lot of systems, you now have a problem with compatibility and possibly environment setup. On top of that, if you're really experienced and good with shell, the bar for "this is difficult" in shell gets further and further away, even if it's hard for someone else to maintain.

I've been investigating alternative options with bring-your-own-tool for this situation at $JOB, mostly making use of shebangs to trigger environment setup from self-contained utilities packaged with the scripts, and there are some promising options. Things like `uv` to setup a self-contained one-off python venv automatically so you can write in Python instead, possibly with one of the packages that helps with the many subcommands footguns, or to write `xonsh` if you want something more shell script leaning but with Python power. Or you can alternatively go with a language agnostic solution and use shebang triggered `nix-portable` to construct a whole isolated custom environment automatically from a flake.nix and run whatever tools and mishmash of languages you want.

Thank you.

For some reason shell and Bash is very alluring to me, but Python isn’t. I guess it’s the combination of both having to orchestrate subprocesses and having to deal with all the typical dynamic language bugs that I introduce in the process of writing in a language I usually do not use.

So then I use some static language. Which has even more of an activation cost.

I should just try a better shell than the vaguely Posix compliant ones.

Is it just me, or is it an LLM language? The article tries very hard to be correct but somehow lacks experience.

I've never used the majority of these tricks for decades, except for brace expansion, process substitutions, and complex redirections.

I think the keybinding suggestions are really nice. My shell is configured by default such that Alt+Left and Alt+Right move by a word, but having things that work out of the box, basically always, is really useful whenever I need to do things inside a docker container
My header on top of every script

            #!/usr/bin/env bash
            set -eEuo pipefail
            # shellcheck disable=SC2034
            DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
            #######################################################
Wait... Most of my shell scripts have zero unused variables: I prefer to comment them if I may need them later on.

Why do you disable SC2034?

I don't think not having unused variables prevent me from doing things in my scripts!?

I understand if it's a preference but SC2034 is basically one of my biggest timesavers: in my case unused variables are typically a bug. Except, maybe, ANSI coloring variables at the top of the script.

It depends too on whether you use shellcheck as a primary tool or not. I prefer to have no shellcheck errors/warnings by default so when they do appear it's very obvious. But having a consistent opening block on a bunch of scripts is often more important, so setting a shellcheck disable on that one variable that may or may not be used is a better solution.
I'd suggest `pwd -P` to resolve symlinks too. (if you use DIR to call/source neighbouring scripts).
If you're using `set -e` you almost always want a trap on ERR to print where you suddenly exited from your program. Otherwise there's no way to tell.

Also worth mentioning, but not including permanently, is `set -x` which will print every command to stderr with a `+ ` prefix before running it.

For `DIR` I usually pick a name less likely to conflict with one randomly selected in my script, like `SCRIPT_DIR`.

You also need to think about symlinks and how you want to handle them. Your current `DIR` resolution gives you the directory name without any symlinks resolved. If you're possibly a script that got symlinked to by someone, this may not be the directory of your actual script anymore, but the directory containing the symlink. As long as you want that it's fine, but a lot of the time you want the script to see it's resolved folder so it can call scripts in the real folder with it instead