| A command exits non-zero | NEVER dismiss it as "a quirk". Reproduce the smallest failing form to find the deterministic cause, or fix the command. |
| A command "succeeds" (exit 0) | Exit 0 is necessary but NOT sufficient. ALSO verify the real artifact/output (file written, content/size correct, options actually applied) - some tools exit 0 while writing nothing or silently ignoring options (e.g. the vips out.tif[opts] bracket form). Check the result, not only the status. |
| Critical command + a check in one call | Run the mutation in its OWN call (or join with &&). A trailing command's exit masks or misattributes the real one. |
Self-authored text inside a double-quoted arg (git -m, a --hook/--title) | NEVER include backticks or $(...): bash command-substitutes them, RUNNING the word as a command (this once ran a real shutdown) and corrupting the text. Use plain words, single quotes, or pass the text via a file (-F/--body-file). |
| Grep to find EVERY site to change | NEVER pipe the enumeration through head/tail -N: the cap is silent and becomes a false "all N sites updated" claim. Count first with grep -rc, then list uncapped and reconcile against that count. |
Capturing a grep -c count into a variable | grep EXITS 1 when it matches nothing, so the "safe" idiom n=$(grep -c PATTERN file || echo 0) fires the fallback on a zero count and sets n to the TWO-LINE string 0\n0, silently misformatting every comparison and report line built from it. Write n=$(grep -c PATTERN file 2>/dev/null); n=${n:-0} - grep -c already prints 0 itself, so no fallback is needed. |
cmd | tail/head/grep | The pipeline's exit is the LAST stage's, not cmd's. Use set -o pipefail or check ${PIPESTATUS[0]}. |
| Check/kill a process by name | pgrep/pkill -f PATTERN matches your OWN shell. Prefer a pidfile + kill -0, a port/unit/cgroup signal, or bracket the first char ([p]attern) AND keep the keyword out of echo labels in the same command. |
| Backgrounding a long job (run_in_background) | Make the long command ITSELF the background task. NEVER put cmd & INSIDE a run_in_background call: the wrapper reaches its next line and EXITS at once, firing a FALSE completion notification, while the &-detached child is reparented to init and runs ORPHANED + untracked - so its real end-signal never comes and you cannot stop it by task id. Run ONE command per run_in_background; for parallel jobs make SEPARATE calls, never one wrapper that backgrounds several with &. If a job does end up orphaned, poll ground truth (process alive + output-file growth) - no notification will fire. |
| Waiting for an event | Set an EXPECTED-duration ceiling BEFORE you wait (a quick estimate or one-shot instrumentation). This applies to every wait, INCLUDING waiting on a background job's completion signal (a task notification, a log line, a flag, a port) - a signal is not a licence to wait unboundedly. If the event overruns the ceiling by ~2x, STOP and INVESTIGATE (hung? mis-scoped? wrong command/marker? contended?) instead of continuing to wait for the signal. Otherwise wait the measured time plus a small margin (~1.3-1.5x, or a few seconds), or on the concrete signal, never an arbitrary long sleep (over-waiting compounds across cycles). Record measured timings so they are reused, not rediscovered. |
| Judging current state from output/logs | Read the freshest lines and check their timestamps; never conclude from a stale capture. |
| Keep / prune the NEWEST timestamped file(s) | Sort by MTIME, not by name: ls -t (newest first), or find DIR -maxdepth 1 -printf '%T@ %p\n' | sort -zrn. NEVER rely on plain ls/glob order (lexical) - a varying prefix breaks it (bak-dream-... sorts before bak-dreamtest-...), so the alphabetically-last STALE file is kept and a newer one deleted. |