Tricks from Usenet archives, Bell Labs culture, and 30+ years of terminal living.
Readline maintains a ring buffer of killed text:
Ctrl-w -- kill word backwardCtrl-k -- kill to end of lineCtrl-u -- kill to beginning of lineCtrl-y -- yank (paste) most recent killAlt-y -- cycle through kill ring after a yank (the trick nobody knows)Alt-3 Alt-b -- numeric argument: move backward 3 wordsTargeted completions:
Ctrl-x ~ -- username completionCtrl-x / -- filename completionCtrl-x $ -- variable name completionCtrl-x @ -- hostname completionCtrl-x ! -- command name completionfc -- open last command in $EDITOR, execute on savefc -3 -- open 3 commands backfc 100 110 -- edit commands 100-110 as a batch scriptfc -s old=new -- re-run last command with substitutionfc -l -20 -- list last 20 commandsAlias: r='fc -s' then r cc re-runs last command starting with "cc"
Opens current half-typed command in $EDITOR. Different from fc:
Event designators:
!! -- last command!-3 -- 3 commands ago!grep -- most recent command starting with "grep"!?config? -- most recent containing "config"Word designators:
!$ -- last argument of previous command!^ -- first argument!* -- all arguments!!:2 -- second argument!!:2-4 -- arguments 2 through 4Modifiers (chain these):
:h -- head (dirname): /a/b/c.txt -> /a/b:t -- tail (basename): /a/b/c.txt -> c.txt:r -- root (strip extension): file.tar.gz -> file.tar:e -- extension only: file.tar.gz -> .gz:p -- print but don't execute:s/old/new/ -- substitute first:gs/old/new/ -- global substituteKiller combos:
!!:gs/src/build/ # rerun with all src -> build
cd !$:h # cd to dirname of last arg
Press Alt-. to insert the last argument of the previous command.
Repeat to cycle through older commands' last arguments.
This is the single most useful keystroke for avoiding path retyping.
diff <(sort file1) <(sort file2) # diff two commands
diff <(ssh server cat /etc/hosts) /etc/hosts # compare remote vs local
comm -23 <(sort deployed) <(sort healthy) # set difference
# Variable survival (no subshell):
while read -r line; do count=$((count+1)); done < <(grep ERROR log)
# vs pipe (subshell, variables lost):
grep ERROR log | while read -r line; do count=$((count+1)); done
# Fan-out with tee:
cmd | tee >(gzip > backup.gz) >(jq . > pretty.json) > /dev/null
mkfifo /tmp/pipe
# Terminal 1: tail -f /var/log/app.log > /tmp/pipe
# Terminal 2: grep ERROR < /tmp/pipe
# Skip temp file for large data:
mkfifo /tmp/dbpipe
gunzip -c dump.sql.gz > /tmp/dbpipe &
mysql mydb < /tmp/dbpipe
# Persistent fd (avoids blocking-on-open):
mkfifo /tmp/pipe
exec 3<>/tmp/pipe
echo "msg" >&3
read -r line <&3
# Swap stdout and stderr:
command 3>&1 1>&2 2>&3
# Persistent stderr redirect:
exec 2>error.log # ALL subsequent stderr goes to file
# Bash TCP sockets (no netcat):
exec 3<>/dev/tcp/example.com/80
echo -e "GET / HTTP/1.0\r\n\r\n" >&3
cat <&3
exec 3>&-
# Close fd to prevent child inheritance:
some_command 3<&-
# The definitive cleanup pattern:
tempfile=$(mktemp) || exit
trap 'rm -f "$tempfile"' EXIT # fires on ANY exit
# Correct SIGINT handler (preserves caller semantics):
trap 'rm -f "$tempfile"; trap - INT; kill -s INT "$$"' INT
# Daemon reload:
trap 'read_config' HUP
# Background process reaping:
trap '[[ $pid ]] && kill "$pid"' EXIT
long_cmd & pid=$!
wait "$pid"
export LC_ALL=C # before sort/grep/awk on large files
# Bypasses locale-aware Unicode processing. 5-10x faster.
false | true | false
echo ${PIPESTATUS[@]} # "1 0 1" -- every pipe stage's exit code
POSIX guaranteed. Preserves inodes (unlike sed -i).
# Uncomment a line:
ed -s file.conf <<'EOF'
/^#ServerName/s/^#//
w
q
EOF
# Insert after a pattern:
ed -s file <<'EOF'
/pattern/a
new line here
.
w
q
EOF
# Delete matching lines:
printf '%s\n' 'g/pattern/d' w q | ed -s file
cp file.txt{,.bak} # backup
mv file.{old,new} # rename
mkdir -p app/{src,lib,test,doc} # scaffold
diff config.{orig,modified} # quick diff
echo {01..10} # zero-padded sequence
echo {a,b}{1,2} # cartesian: a1 a2 b1 b2
touch test_{a,b,c}_{1..5}.txt # 15 files
${var:-default} # use default if unset
${var:=default} # assign default if unset
${var:+alternate} # use alternate if set
${var:?error} # exit with error if unset
${#var} # string length
${var:5:3} # substring
${var#*/} # strip shortest prefix
${var##*/} # strip longest prefix (basename)
${var%/*} # strip shortest suffix (dirname)
${var%%.*} # strip longest suffix (all extensions)
${var/old/new} # replace first
${var//old/new} # replace all
${var^^} # UPPERCASE
${var,,} # lowercase
${!name} # indirect: value of variable named by $name
shopt -s autocd # type dir name to cd into it
shopt -s cdspell # auto-correct cd typos
shopt -s dirspell # auto-correct tab completion typos
shopt -s globstar # enable ** recursive glob
shopt -s histverify # !! shows expansion before executing
shopt -s histappend # append to history, don't overwrite
shopt -s extglob # enable !(pattern), +(pattern), etc.
shopt -s nullglob # unmatched globs expand to nothing
export CDPATH=".:~:~/dev"
cd srice # jumps to ~/dev/srice from anywhere
HISTCONTROL='ignorespace:erasedups' # dedup entire history
HISTSIZE=1000000
HISTFILESIZE=1000000
HISTTIMEFORMAT="%F %T " # ISO timestamps
# Sync history across terminals:
PROMPT_COMMAND='history -a; history -c; history -r'
# Pre-command timer:
trap 'timer_start' DEBUG
PROMPT_COMMAND='timer_stop'
set -o vi
# In command mode:
# k/j -- history navigation
# /string -- search history
# # -- comment out current line and save to history
# v -- open in $EDITOR
The # trick: type command, Escape, # -- saves it commented in history. Do other work. Recall with k, 0x to uncomment, Enter.
Suffix aliases:
alias -s py=nvim # typing "script.py" opens in nvim
alias -s md=glow # typing "README.md" renders it
Global aliases:
alias -g G='| grep'
alias -g L='| less'
alias -g J='| jq .'
alias -g C='| wc -l'
Named directories:
hash -d dev=~/dev
hash -d ky=~/dev/krisyotam.com
cd ~dev # jumps to ~/dev
Hook functions: precmd, preexec, chpwd (auto-ls after cd)