~kris/dots

srice

ref: 347c183a3d4728b74c9a3be765bb8e97386b2ae8 srice/doc/shell-mastery.md -rw-r--r-- 7.3 KiB
347c183a — Kris Yotam wallpapermenu: make CAPS call optional so it doesn't error where CAPS is absent 2 months ago

#Shell Mastery Reference

Tricks from Usenet archives, Bell Labs culture, and 30+ years of terminal living.

#Readline Kill Ring

Readline maintains a ring buffer of killed text:

  • Ctrl-w -- kill word backward
  • Ctrl-k -- kill to end of line
  • Ctrl-u -- kill to beginning of line
  • Ctrl-y -- yank (paste) most recent kill
  • Alt-y -- cycle through kill ring after a yank (the trick nobody knows)
  • Alt-3 Alt-b -- numeric argument: move backward 3 words

Targeted completions:

  • Ctrl-x ~ -- username completion
  • Ctrl-x / -- filename completion
  • Ctrl-x $ -- variable name completion
  • Ctrl-x @ -- hostname completion
  • Ctrl-x ! -- command name completion

#fc (Fix Command)

  • fc -- open last command in $EDITOR, execute on save
  • fc -3 -- open 3 commands back
  • fc 100 110 -- edit commands 100-110 as a batch script
  • fc -s old=new -- re-run last command with substitution
  • fc -l -20 -- list last 20 commands

Alias: r='fc -s' then r cc re-runs last command starting with "cc"

#Ctrl-x Ctrl-e

Opens current half-typed command in $EDITOR. Different from fc:

  • fc edits the PREVIOUS command after it ran
  • Ctrl-x Ctrl-e edits the CURRENT command before execution
  • In vi mode: hit Escape then v

#History Expansion

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 4

Modifiers (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 substitute

Killer combos:

!!:gs/src/build/        # rerun with all src -> build
cd !$:h                 # cd to dirname of last arg

#Alt-. (Insert Last Argument)

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.

#Process Substitution

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

#Named Pipes (FIFOs)

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

#File Descriptor Tricks

# 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<&-

#Signal Traps

# 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"

#LC_ALL=C for Performance

export LC_ALL=C    # before sort/grep/awk on large files
# Bypasses locale-aware Unicode processing. 5-10x faster.

#PIPESTATUS

false | true | false
echo ${PIPESTATUS[@]}    # "1 0 1" -- every pipe stage's exit code

#ed for Scripted Editing

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

#Brace Expansion

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

#Parameter Expansion

${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 Options That Matter

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

#CDPATH

export CDPATH=".:~:~/dev"
cd srice    # jumps to ~/dev/srice from anywhere

#HISTCONTROL

HISTCONTROL='ignorespace:erasedups'    # dedup entire history
HISTSIZE=1000000
HISTFILESIZE=1000000
HISTTIMEFORMAT="%F %T  "              # ISO timestamps

#PROMPT_COMMAND and trap DEBUG

# Sync history across terminals:
PROMPT_COMMAND='history -a; history -c; history -r'

# Pre-command timer:
trap 'timer_start' DEBUG
PROMPT_COMMAND='timer_stop'

#Vi Mode Deep Tricks

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.

#Zsh-Specific Power

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)