Shell Productivity in Three Layers: Readline, the Language, and the Ecosystem
Source: hackernews
Shell productivity guides tend to dump everything into one list: aliases next to set -euo pipefail next to zoxide, as if they all live in the same place and require the same kind of attention, but they don’t. The shell productivity stack has at least three distinct layers, and recognizing which layer a trick belongs to tells you how hard it is to learn, how durable it is across machines, and how much it compounds over time.
The three layers are: the readline editing layer that operates below the shell itself, the shell language layer covering features baked into bash and zsh, and the ecosystem layer of external tools. A recent article on Hacker News collected a broad range of these tricks and generated substantial community discussion; what follows is a map of why certain tricks stick and others don’t, organized by where they actually live.
The Readline Layer
Most shells use GNU Readline for line editing, and readline is a shared layer: the same keystrokes that work in bash work in the Python REPL, psql, GDB, and the MySQL client. Learning readline is not “learning a shell trick,” it is learning an editor that pervades your entire command-line existence.
The most important readline binding is Ctrl-X Ctrl-E, which opens the current line in $EDITOR. If you are building a long find invocation or a multi-stage jq pipeline, you open it in vim or whatever your editor is, write it properly, save, quit, and the shell executes it. This changes the threshold at which you switch from “type it in the terminal” to “write a script,” and that threshold rises considerably.
Other readline bindings worth internalizing:
Alt-.inserts the last argument of the previous command. Press it again to walk further back in history. This is the safe, repeatable version of!$.Ctrl-Wdeletes the word behind the cursor.Alt-Ddeletes the word ahead. Together withCtrl-U(kill to start) andCtrl-K(kill to end), you have a kill ring without leaving the command line.Ctrl-X Ctrl-Xtoggles between the cursor’s current position and the mark, letting you select regions the way you would in Emacs.
Vi mode (set -o vi in .bashrc) is a reasonable alternative if you already think in vi motions. Normal mode gives you v to open the line in $EDITOR, the equivalent of Ctrl-X Ctrl-E. The readline layer supports both paradigms fully.
The Shell Language Layer
This is the layer most guides cover partially. The features here are built into the shell binary itself: no dependencies, no version concerns beyond bash 4.x, no startup time. The syntax was not designed for readability, which is why uptake is uneven.
Brace expansion is the most immediately useful:
cp config.yaml{,.bak} # expands to: cp config.yaml config.yaml.bak
mkdir -p src/{components,utils,hooks,tests}
mv src/{old,new}_handler.go
echo {01..20..2} # 01 03 05 ... 19 (step notation, bash 4+)
The cp file{,.bak} pattern for creating a backup before editing is something you use multiple times per day once you know it. Brace expansion is absent from POSIX sh and dash, which matters for portable scripts, but for interactive use and bash-specific scripts it is safe and reliable.
Parameter expansion is where most shell users leave significant value on the table. The typical habit is to reach for sed or awk whenever string manipulation is needed. The shell has a mini-language for this built in, and it runs without forking a subprocess:
file="path/to/archive.tar.gz"
${file##*/} # archive.tar.gz (longest prefix match stripped: basename)
${file%.*} # path/to/archive.tar (shortest suffix match stripped)
${file%%.*} # path/to/archive (longest suffix match stripped)
# Case conversion (bash 4.0+, released 2009)
name="hello world"
echo ${name^^} # HELLO WORLD
echo ${name^} # Hello world
# Default values and error guards
${var:-default} # use default if var is unset or empty
${var:?error msg} # abort script with error if var is unset
${#var} gives the string length. ${var//pattern/replacement} replaces all occurrences. None of this requires spawning a subprocess. For tight loops processing many filenames or strings, the difference is measurable; for interactive use, the benefit is fewer moving parts and fewer failure modes around empty strings.
Process substitution eliminates a whole class of temp-file boilerplate. <(command) presents the output of a command as a file path, backed by a file descriptor on Linux or a named pipe on macOS:
diff <(sort file1) <(sort file2)
diff <(ssh remote ls /path) <(ls /path)
paste <(cut -d, -f1 data.csv) <(cut -d, -f3 data.csv)
The output form >(command) handles fan-out pipelines:
command | tee >(filter1 > out1) >(filter2 > out2) > /dev/null
Process substitution dates to ksh and bash 1.14 in the early 1990s. It does not work in POSIX sh, which is the main portability caveat to carry. For scripts that need to run on minimal systems, named pipes (mkfifo) or temp files remain the portable fallback.
History expansion is the most divisive corner of the shell language. !! re-runs the last command (sudo !! is the classic), !$ is the last argument, ^old^new substitutes a string in the last command. These have a reputation for being fragile: ! characters in strings trigger expansion unexpectedly unless you have set +H or carefully quote everything. For interactive use they are serviceable; in scripts they are off by default and should stay that way.
The Ecosystem Layer
The ecosystem layer is where external tools extend what the shell language cannot do cleanly on its own, primarily around history search and directory navigation.
fzf, created by Junegunn Choi in 2013 and now approaching 70,000 GitHub stars, replaces Ctrl-R history search with a fuzzy-searchable, visual interface. Setup is minimal:
# bash, after installing fzf
source /usr/share/doc/fzf/examples/key-bindings.bash
# or via the fzf installer
$(brew --prefix)/opt/fzf/install
After that, Ctrl-R opens an interactive picker over your entire history. The original readline Ctrl-R cycles through one match at a time; fzf shows all matches ranked by relevance and lets you navigate with arrow keys. For long histories the difference is qualitative, not just ergonomic.
Directory navigation is the other major gap. cd requires typing exact paths; zoxide, a Rust implementation of the ideas behind fasd and autojump, learns from your navigation history and lets you jump with partial names:
eval "$(zoxide init bash)" # or zsh
# after visiting ~/projects/my-big-webapp several times:
z webapp # jumps there directly
zi webapp # opens fzf to select among matches interactively
The genealogy here is worth tracing. autojump (2009) introduced the “frecency” concept, ranking directories by a combination of frequency and recency. fasd (2011) extended the idea to files and introduced a richer command vocabulary. Both have been superseded by zoxide, which initializes in under a millisecond compared to 50ms or more for the Python-based predecessors. The pattern mirrors a broader wave: bat replaced cat for casual file viewing, ripgrep replaced grep for search, fd replaced find for most interactive use. These are not compatibility-breaking rewrites; they are drop-in speedups with better defaults, and zoxide fits the same mold.
Scripts vs. Interactive Use
One thread worth keeping separate: many shell tricks discussed in these posts apply differently to interactive sessions versus scripts. For scripts, set -euo pipefail and trap 'cleanup' EXIT are foundational:
#!/usr/bin/env bash
set -euo pipefail
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
# rest of script
-e exits on error, -u exits on unset variable reference, -o pipefail makes a pipeline fail if any stage fails. Without pipefail, false | true exits 0. The trap ensures cleanup even on errors. These do not belong in .bashrc but in every non-trivial script you write.
ShellCheck and shfmt round out the scripting toolchain. ShellCheck catches a large class of bugs at write time, including subtle quoting errors and POSIX portability issues. shfmt handles consistent formatting. Neither requires configuration to be immediately useful.
Where to Invest
If you are building shell fluency deliberately, the readline layer has the highest compounding return: it is cross-application, it survives shell changes, and learning it once improves every readline-enabled tool you use. The shell language layer rewards sustained study; parameter expansion and brace expansion reduce subprocess overhead and simplify scripts. The ecosystem layer, particularly fzf and zoxide, has the highest immediate daily-use return but requires installation on each machine you work on.
The trick that surfaces in top-voted comments across years of these discussions is Ctrl-X Ctrl-E, and the reason is straightforward: encountering it once tends to be sufficient to make it stick permanently. Starting with the readline layer is a reasonable place.