Shell Scripting Cheat Sheet
Scannable Bash reference for the Shell Scripting for DevOps Engineers track. Prefer tutorials when you need why, not only how.
Overview
| Audience | When to use |
| Linux admins, DevOps, SRE, platform engineers | Daily scripting, cron jobs, CI glue, quick ops automation |
Quick reference
| Topic | Pattern |
| Shebang | #!/usr/bin/env bash |
| Strict mode | set -euo pipefail |
| Quote | "$var" · "$@" · never bare $var for paths |
| Defaults | ${var:-default} · ${var:?required} |
| Arithmetic | $(( i + 1 )) · (( i++ )) |
| Tests | [[ -f $f ]] · [[ $a == "$b" ]] |
| case | case "$1" in start) ;; *) usage; exit 2;; esac |
| Loops | for f in "${arr[@]}"; do · while IFS= read -r line; do |
| Functions | fn() { local x="$1"; } |
| Arrays | a=(x y) · a+=("z") · declare -A m=([k]=v) |
| Redirection | cmd >out 2>err · cmd >>log 2>&1 · cmd \| tee |
| Trace | bash -x ./script.sh · PS4='+${LINENO}: ' |
| Temp/clean | tmp=$(mktemp -d) · trap 'rm -rf "$tmp"' EXIT |
| Lock | exec 9>lockfile · flock -n 9 \|\| exit 0 |
| Cron | set PATH=; absolute paths; no aliases |
Bash syntax
#!/usr/bin/env bash
set -euo pipefail
usage() { echo "Usage: $0 <arg>" >&2; exit 2; }
[[ $# -ge 1 ]] || usage
| Construct | Notes |
[[ ]] | Bash conditional — prefer over [ ] in Bash scripts |
(( )) | Integer arithmetic / numeric tests |
$( ) | Command substitution (quote when needed) |
source file / . file | Load library functions |
Bash vs sh
On many Debian/Ubuntu systems /bin/sh is dash. Avoid Bashisms when the shebang is #!/bin/sh.
Variables
name="Ada"
readonly CONST="prod"
export PATH="/usr/local/bin:/usr/bin:/bin"
echo "${name:-world}"
: "${REQUIRED:?REQUIRED must be set}"
# Indirect / lengths — avoid MkDocs-breaking length macros in docs;
# in real scripts, prefer counting via loops or tools when publishing Markdown.
count=0
for _ in "${items[@]+"${items[@]}"}"; do count=$((count + 1)); done
| Form | Meaning |
"$var" | Safe expansion |
"$@" | All positional params, separately quoted |
"$*" | Joined as one word (rarely what you want) |
${var#prefix} | Strip shortest prefix |
${var##prefix} | Strip longest prefix |
${var%suffix} | Strip shortest suffix |
${var/pattern/repl} | Replace once |
Loops
for host in web db cache; do
echo "$host"
done
while IFS= read -r line; do
printf 'line=%s\n' "$line"
done < file.txt
until ping -c1 -W1 "$host" >/dev/null; do
sleep 2
done
| Keyword | Use |
break | Leave loop |
continue | Next iteration |
nullglob | shopt -s nullglob so unmatched globs expand empty |
Functions
greet() {
local whom="${1:-world}"
echo "Hello, ${whom}"
}
log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >&2; }
die() { log "ERROR: $*"; exit "${2:-1}"; }
- Prefer
local for function variables - Return status via
return / exit; print data on stdout - Share helpers with
source "${REPO_ROOT}/lib/common.sh"
Redirection and pipes
| Pattern | Effect |
>file | stdout overwrite |
>>file | stdout append |
2>file | stderr only |
&>file / >file 2>&1 | both streams |
cmd1 \| cmd2 | pipe stdout |
cmd <file | stdin from file |
cmd <<< "text" | here-string |
grep ERROR app.log | tee -a errors.txt | wc -l
# Always enable pipefail in automation so mid-pipe failures surface
Text processing
| Tool | Typical use |
grep -E | Match lines |
sed -E | Stream edit |
awk | Columns / reports |
cut -d -f | Field split |
tr | Translate/delete chars |
sort \| uniq -c | Frequency |
xargs -r | Build command lines (GNU) |
df -P -x tmpfs | awk 'NR>1 {print $5, $6}'
journalctl -u nginx -n 200 --no-pager | grep -E 'error|crit'
jq (JSON)
jq -r '.env' status.json
jq -r '.services[] | select(.status!="ok") | .name' status.json
jq -r '.services[] | [.name,.status] | @tsv' status.json
| Filter | Meaning |
.key | Field |
.[] | Iterate array |
select(expr) | Filter |
@tsv / @csv | Encode row |
-e | Exit non-zero if false/null result (jq 1.6+) |
yq (YAML)
Prefer mikefarah/yq v4 syntax:
yq -r '.app.name' app.yaml
yq -r '.app.image + ":" + .app.tag' app.yaml
yq -e '.app.replicas >= 1' app.yaml
Multiple yq tools exist
Confirm yq --version. Python yq wrappers differ from mikefarah/yq.
Cron and timers
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
*/5 * * * * /home/ops/bin/check-disk.sh 80 >>/home/ops/logs/disk.log 2>&1
| Tip | Why |
| Absolute paths | Cron PATH is minimal |
| Redirect both streams | Capture failures |
flock | Prevent overlap |
| systemd timer | Better calendar + journaling on modern hosts |
# oneshot sketch
# ExecStart=/usr/local/bin/backup.sh
# OnCalendar=*-*-* 02:30:00
Shell debugging
| Technique | Command / pattern |
| Trace | bash -x script.sh |
| Verbose | set -v / set -x regionally |
| Line prompt | export PS4='+${BASH_SOURCE}:${LINENO}: ' |
| Syntax check | bash -n script.sh |
| Static analysis | shellcheck script.sh |
| Exit probe | echo exit=$? after commands |
Troubleshooting table
| Problem | Cause | Resolution |
| Works in terminal, fails in cron | PATH/cwd/env | Set PATH, use absolute paths |
command not found mid-script | set -e after failed lookup | Check command -v |
| Empty vars blow up | set -u | Provide defaults or ${var-} carefully |
| Pipeline “succeeds” wrongly | No pipefail | set -o pipefail |
| Word-splitting bugs | Unquoted $var | Quote; use arrays / read -r |
Production tips
- Log humans to stderr; emit
RESULT key=value on stdout for CI - Validate paths with prefix checks before
rm/mv - Prefer idempotent installs and dry-run flags
- Keep secrets in the environment or a secrets manager — never in git
- Run ShellCheck in CI
Common mistakes
| Mistake | Correct approach |
rm -rf $TARGET | rm -rf -- "$TARGET" after validation |
Bashisms under #!/bin/sh | Use env bash or stay POSIX |
| Relying on aliases in scripts | Use full commands / functions |
Interactive read in cron | Fail fast; require args/config |
Interview nuggets
- Explain
set -euo pipefail in one minute - Difference between
$@ and $* - Why cron jobs see a different environment
- When to leave Bash for Python
References