Shell Scripting - 100 Scenarios
Shell Scripting - 100 Scenarios
Beginner → Expert
You want a reusable command file instead of retyping the same lines.
CONCEPT
A shell script is a text file of commands. The shebang on line one tells the OS
which interpreter runs it, and the file must be executable to run as ./script.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
echo "Hello from $(hostname)"
OUTPUT
chmod +x [Link]
./[Link]
Hello from web01
EXPLANATION
→ #!/usr/bin/env bash finds bash via PATH — more portable than a hardcoded
path.
→ Without the +x bit you would have to run it as bash [Link] instead.
Key idea: Use #!/usr/bin/env bash for portability across systems where bash lives
elsewhere.
SCENARIO
You need to know whether the last command succeeded before continuing.
CONCEPT
Every command returns an exit status: 0 means success, non-zero means failure.
The special variable $? holds the last status — the foundation of all shell control
flow.
COMMAND / PLAYBOOK
terminal
OUTPUT
found: 0
missing: 1
EXPLANATION
SCENARIO
A script should signal failure to whatever called it (CI, cron, another script).
CONCEPT
The exit builtin ends a script with a chosen status. Returning meaningful codes
lets callers and pipelines react correctly to success or failure.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
if [[ ! -f /etc/nginx/[Link] ]]; then
echo "config missing" >&2
exit 1
fi
exit 0
OUTPUT
./[Link]; echo $?
config missing
1
EXPLANATION
Key idea: Reserve exit 0 for success; use distinct non-zero codes for different
failures.
SCENARIO
Your script needs to accept a name and an optional greeting from the command
line.
CONCEPT
Arguments arrive as $1, $2, ...; $0 is the script name, $# the count, and $@ the
full list. This is how scripts take input from their caller.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
echo "script: $0"
echo "args: $# -> $@"
echo "first: $1"
OUTPUT
EXPLANATION
Key idea: Always quote "$@" so arguments with spaces stay intact.
SCENARIO
CONCEPT
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
version="${1:?usage: [Link] <version>}"
echo "deploying $version"
OUTPUT
./[Link]
[Link]: line 2: 1: usage: [Link] <version>
./[Link] 2.1.0
deploying 2.1.0
EXPLANATION
→ The script stops with a non-zero status, so callers notice the failure.
SCENARIO
You want to process a fixed first argument, then loop over the rest.
CONCEPT
shift drops $1 and renumbers the remaining arguments down. It lets you peel off
leading arguments and then iterate over whatever is left.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
action=$1; shift
echo "action=$action"
for f in "$@"; do echo "file: $f"; done
OUTPUT
EXPLANATION
→ shift removes the first argument so $@ now holds only the files.
Key idea: shift is the classic way to split a leading command from its remaining
arguments.
SCENARIO
Several scripts share helper functions and constants you don't want to duplicate.
CONCEPT
source (or .) runs another file in the current shell, so its variables and functions
become available. Unlike executing it, sourcing shares the same environment.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
source ./[Link]
log "starting" # log() defined in [Link]
OUTPUT
10:15:02 starting
# log() came from the sourced library
EXPLANATION
→ Executing [Link] instead would run it in a subshell and lose the functions.
Key idea: source shares the current shell; running a script uses a separate subshell.
SCENARIO
Others (and future you) need to understand what a script does and how to call it.
CONCEPT
Comments start with # and are ignored by the shell. A usage function plus a
header comment turn a script into something maintainable and self-documenting.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
# [Link] - archive a directory to /backups
usage() { echo "usage: $0 <dir>"; exit 1; }
[[ $# -eq 1 ]] || usage
OUTPUT
./[Link]
usage: ./[Link] <dir>
EXPLANATION
Key idea: A usage() function keeps help text in one place and doubles as input
validation.
SCENARIO
A script must reference files next to it regardless of where it's run from.
CONCEPT
Scripts are often run from other directories, so relative paths break. Resolving the
script's own location makes it reference sibling files reliably.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
echo "$script_dir"
cat "$script_dir/[Link]"
OUTPUT
/opt/nimbus/bin
# [Link] resolved next to the script, not the caller's cwd
EXPLANATION
Key idea: Resolve $script_dir so a script can find its companion files anywhere.
CONCEPT
Assignment has no spaces around =. On use, the shell word-splits and glob-
expands unquoted values; double-quoting a variable preserves it as a single
argument.
COMMAND / PLAYBOOK
terminal
path="/opt/my app"
ls $path # splits into two args
ls "$path" # one arg, correct
OUTPUT
EXPLANATION
Key idea: Quote variable expansions by default; unquoted values split and glob.
SCENARIO
CONCEPT
Command substitution $(...) runs a command and substitutes its stdout. It's how
scripts feed the output of one command into a variable or another command.
COMMAND / PLAYBOOK
terminal
today=$(date +%F)
backup="nimbus-$[Link]"
echo "$backup"
OUTPUT
[Link]
EXPLANATION
Key idea: Use $(...) not backticks — it nests cleanly and is easier to read.
SCENARIO
A variable may be unset and you want a sensible fallback without an if.
CONCEPT
COMMAND / PLAYBOOK
terminal
echo "env=${APP_ENV:-dev}"
: "${PORT:=8080}"
echo "port=$PORT"
OUTPUT
env=dev
port=8080
EXPLANATION
Key idea: ${v:-x} substitutes a default; ${v:=x} also assigns it for later use.
SCENARIO
You need the first 7 characters of a git commit hash and its length.
CONCEPT
Bash can slice strings with ${var:offset:length} and get length with ${#var},
avoiding a call out to cut or expr for simple string work.
COMMAND / PLAYBOOK
terminal
sha="9f2c1a7e5b3d"
echo "short: ${sha:0:7}"
echo "len: ${#sha}"
OUTPUT
short: 9f2c1a7
len: 12
EXPLANATION
Key idea: Native ${var:off:len} and ${#var} avoid launching external tools.
SCENARIO
You want the filename without its path, and the name without its extension.
CONCEPT
COMMAND / PLAYBOOK
terminal
p="/var/log/[Link]"
echo "${p##*/}" # strip longest leading */
echo "${p%.log}" # strip trailing .log
echo "${p##*/}" | : ; base="${p##*/}"; echo "${base%.*}"
OUTPUT
[Link]
/var/log/app
app
EXPLANATION
Key idea: ## and %% strip the longest match; # and % strip the shortest.
SCENARIO
You must turn a path's slashes into dashes for a safe filename.
CONCEPT
COMMAND / PLAYBOOK
terminal
path="a/b/c"
echo "${path//\//-}"
name="Build FAILED"
echo "${name/FAILED/ok}"
OUTPUT
a-b-c
Build ok
EXPLANATION
Key idea: ${v//old/new} is global replace; ${v/old/new} replaces only the first
match.
CONCEPT
COMMAND / PLAYBOOK
terminal
count=5
echo $(( count * 2 + 1 ))
(( count-- ))
echo "now $count"
OUTPUT
11
now 4
EXPLANATION
Key idea: $(( )) does integer math; reach for bc/awk when you need decimals.
SCENARIO
CONCEPT
Bash arrays hold multiple values. Quoting "${arr[@]}" expands to each element
as a separate word — the safe way to loop over lists with spaces.
COMMAND / PLAYBOOK
terminal
OUTPUT
count: 3
- web01
- web02
- db 01
EXPLANATION
Key idea: Always quote "${arr[@]}" so elements containing spaces don't split.
CONCEPT
Associative arrays (declare -A) are key/value maps, available in bash 4+. They're
ideal for lookups instead of long if/case chains.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
declare -A url
url[dev]="[Link]
url[prod]="[Link]
echo "${url[prod]}"
OUTPUT
[Link]
EXPLANATION
Key idea: Associative arrays need bash 4+; they replace clunky case-based lookups.
SCENARIO
A child process doesn't see a variable you set in the parent script.
CONCEPT
Shell variables are local to the current shell unless exported. export marks a
variable so child processes inherit it — the difference between a shell var and an
environment var.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
GREETING=hi
export TOKEN=abc123
bash -c 'echo "child sees GREETING=$GREETING TOKEN=$TOKEN"'
OUTPUT
EXPLANATION
Key idea: export makes a variable visible to child processes; plain vars stay local.
CONCEPT
printf formats output with an explicit format string, portably and predictably —
unlike echo, whose flag/escape behaviour varies between shells.
COMMAND / PLAYBOOK
terminal
OUTPUT
PORT = 8080
name Maya
EXPLANATION
→ printf reuses the format if you supply more arguments — handy for lists.
Key idea: Prefer printf over echo for anything with escapes, formatting, or portability.
SCENARIO
CONCEPT
read pulls a line from stdin into variables. Prompts, silent input for secrets, and
timeouts make it flexible for interactive scripts.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
read -rp "Environment: " env
read -rsp "Token: " token; echo
echo "env=$env token-length=${#token}"
OUTPUT
Environment: prod
Token:
env=prod token-length=12
EXPLANATION
SCENARIO
You want a command's normal output in one file and errors in another.
CONCEPT
Programs write to stdout (fd 1) and stderr (fd 2). Redirection sends each stream to
a file; 2>&1 merges stderr into wherever stdout currently points.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
→ Order matters: put 2>&1 after the stdout redirect to combine them.
Key idea: '>file 2>&1' merges both streams; '2>&1 >file' does
not — order matters.
SCENARIO
CONCEPT
/dev/null is a sink that discards anything written to it. Redirecting noisy output
there keeps scripts quiet while still using the exit code.
COMMAND / PLAYBOOK
terminal
OUTPUT
docker present
EXPLANATION
Key idea: '>/dev/null 2>&1' silences a command while preserving its exit
code.
CONCEPT
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
cat > [Link] <<EOF
port=${PORT:-8080}
env=$APP_ENV
EOF
OUTPUT
EXPLANATION
Key idea: Quote the delimiter (<<'EOF') when you want the text kept literal.
You want to feed a single line into a command without echo | pipe.
CONCEPT
A here-string (<<<) sends one string to stdin. It's a concise alternative to echo
piped into a command for single-line input.
COMMAND / PLAYBOOK
terminal
OUTPUT
first | second
v42
EXPLANATION
Key idea: <<< is a lightweight way to pipe one string into a command.
SCENARIO
CONCEPT
tee copies stdin to both stdout and a file. Piping through it lets you watch output
while capturing it, and -a appends instead of overwriting.
COMMAND / PLAYBOOK
terminal
OUTPUT
Compiling...
Build succeeded
# same text saved to [Link]; 'done' appended
EXPLANATION
→ tee [Link] writes the stream to the file and the terminal.
Key idea: Pipe through tee to watch output live while still logging it.
SCENARIO
You want to diff the output of two commands without temp files.
CONCEPT
COMMAND / PLAYBOOK
terminal
OUTPUT
3c3
< web03
---
> web04
EXPLANATION
Key idea: <(cmd) feeds live command output to tools that want a filename.
You want the top three client IPs from a large access log in one line.
CONCEPT
A pipeline connects one command's stdout to the next command's stdin with |.
Composing small filters is the core Unix philosophy behind shell power.
COMMAND / PLAYBOOK
terminal
OUTPUT
842 [Link]
311 [Link]
108 [Link]
EXPLANATION
Key idea: Set 'set -o pipefail' so a failure anywhere in a pipeline is not hidden.
You have a list of files and want to run a command on each safely.
CONCEPT
xargs builds command lines from stdin. Paired with -0 and find -print0 it handles
filenames with spaces or newlines without breaking.
COMMAND / PLAYBOOK
terminal
OUTPUT
removed './cache/[Link]'
removed './b [Link]'
EXPLANATION
Key idea: Use find -print0 | xargs -0 to handle filenames with spaces safely.
A script should behave differently for prod, staging, and everything else.
CONCEPT
if runs a branch based on a command's exit status. [[ ]] is bash's test construct for
string and file conditions, safer than the older [ ].
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
if [[ $1 == prod ]]; then
echo "careful: production"
elif [[ $1 == staging ]]; then
echo "staging run"
else
echo "dev/default"
fi
OUTPUT
./[Link] prod
careful: production
EXPLANATION
Key idea: Prefer [[ ]] over [ ] in bash — it's safer with unquoted vars and adds
features.
SCENARIO
CONCEPT
String tests inside [[ ]] cover equality (==), inequality (!=), and emptiness (-z / -n).
These drive most validation logic in scripts.
COMMAND / PLAYBOOK
terminal
name=""
[[ -z $name ]] && echo "name is empty"
name="Maya"
[[ $name == M* ]] && echo "starts with M"
OUTPUT
name is empty
starts with M
EXPLANATION
Key idea: Inside [[ ]], == does pattern matching unless you quote the right-hand
side.
CONCEPT
Numeric comparisons use -eq, -ne, -lt, -le, -gt, -ge (or arithmetic (( ))). Using string
operators on numbers is a classic bug, so pick the numeric form.
COMMAND / PLAYBOOK
terminal
usage=92
if (( usage >= 90 )); then echo "disk critical: ${usage}%"; fi
[[ $usage -gt 50 ]] && echo "over half"
OUTPUT
EXPLANATION
Key idea: Use (( )) or -gt/-lt for numbers; == is string comparison and will surprise
you.
A script must verify a file exists and is readable before using it.
CONCEPT
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
conf=/etc/nimbus/[Link]
[[ -f $conf ]] || { echo "missing $conf" >&2; exit 1; }
[[ -r $conf ]] && echo "readable"
OUTPUT
readable
EXPLANATION
Key idea: Guard with [[ -f ]] / [[ -d ]] before reading paths to fail early and clearly.
CONCEPT
case matches a value against glob patterns, cleaner than a long if/elif chain. It's
the standard structure for subcommand dispatch.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
case "$1" in
start) echo starting ;;
stop) echo stopping ;;
restart) echo restarting ;;
*) echo "usage: $0 {start|stop|restart}"; exit 1 ;;
esac
OUTPUT
./[Link] restart
restarting
EXPLANATION
→ Each pattern ends with ) and its block ends with ;;.
Key idea: case is the idiomatic dispatcher for subcommands — clearer than if/elif
chains.
SCENARIO
You want to run a second command only if the first succeeds, or a fallback if it
fails.
CONCEPT
&& runs the next command only on success; || only on failure. They express
simple conditional flow inline, without a full if block.
COMMAND / PLAYBOOK
terminal
OUTPUT
ready
# the ping line prints only if the ping fails
EXPLANATION
Key idea: && and || are inline conditionals; for anything non-trivial, use if for clarity.
SCENARIO
A task should run only when it's production AND a flag file is absent.
CONCEPT
Inside [[ ]] you combine tests with && and || and group with parentheses. This
keeps compound logic in one readable expression.
COMMAND / PLAYBOOK
terminal
env=prod
if [[ $env == prod && ! -f /tmp/paused ]]; then
echo "running in prod"
fi
OUTPUT
running in prod
EXPLANATION
Key idea: Combine tests inside a single [[ ]] with && , || , ! and parentheses.
CONCEPT
The =~ operator inside [[ ]] matches a value against an ERE regex, with captures
in BASH_REMATCH. It's built-in input validation without grep.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
v=$1
if [[ $v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "major=${BASH_REMATCH[1]}"
else
echo "bad version"; exit 1
fi
OUTPUT
./[Link] 2.10.4
major=2
EXPLANATION
Key idea: Leave the =~ regex unquoted; quoting turns it into a literal match.
CONCEPT
COMMAND / PLAYBOOK
terminal
set -u
flag=${DEBUG:-0}
[[ $flag == 1 ]] && echo "debug on" || echo "debug off"
OUTPUT
debug off
EXPLANATION
→ Under set -u, referencing an unset variable would abort the script.
Key idea: Default optional vars (${v:-...}) so tests stay safe under set -u.
CONCEPT
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
if curl -fsS [Link] >/dev/null; then
echo healthy
else
echo unhealthy; exit 1
fi
OUTPUT
healthy
EXPLANATION
Key idea: if <command> tests its exit code directly — no [[ ]] wrapper required.
05 Loops
SCENARIO
You need to run the same action for several named servers.
CONCEPT
A for loop iterates over words. Iterating a quoted array or an explicit list is the safe
form; iterating unquoted command output invites word-splitting bugs.
COMMAND / PLAYBOOK
terminal
OUTPUT
pinging web01
pinging web02
pinging db01
EXPLANATION
→ For file lists, loop over a glob (for f in *.log) rather than ls output.
Key idea: Loop over globs or arrays, never over the output of ls or $(cat file).
CONCEPT
The C-style for (( ; ; )) loop gives an explicit numeric counter, ideal for fixed
iteration counts and index-based work.
COMMAND / PLAYBOOK
terminal
OUTPUT
attempt 1
attempt 2
attempt 3
EXPLANATION
→ It reads like C and is best when you need the index itself.
Key idea: C-style for (( )) is best when you need an explicit numeric index.
CONCEPT
while repeats its body as long as a command succeeds. It's the tool for polling and
waiting on external conditions.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
until nc -z localhost 8080 2>/dev/null; do
echo "waiting for port 8080..."
sleep 2
done
echo "up!"
OUTPUT
EXPLANATION
→ until loops while the command keeps failing (the inverse of while).
Key idea: until CMD; do ...; done polls until CMD succeeds — add a sleep to be kind
to the CPU.
CONCEPT
while IFS= read -r line reads input safely one line at a time. Setting IFS= and using
-r prevents trimming and backslash mangling.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
while IFS= read -r line; do
echo "host: $line"
done < [Link]
OUTPUT
host: [Link]
host: [Link]
EXPLANATION
Key idea: 'while IFS= read -r line' is the canonical safe way to read lines.
CONCEPT
Quoting "${arr[@]}" expands each element as its own word, so a for loop handles
values containing spaces correctly.
COMMAND / PLAYBOOK
terminal
OUTPUT
[report [Link]]
[[Link]]
EXPLANATION
Key idea: Quote "${arr[@]}" in loops or elements with spaces will split apart.
CONCEPT
Brace expansion {1..5} and seq generate sequences to loop over. Brace
expansion is faster and native; seq handles variable bounds and formatting.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
→ Brace ranges can't use variables; use seq (or C-style for) when bounds vary.
Key idea: Use {1..N} for literal ranges; use seq when the bounds are variables.
SCENARIO
While processing files you want to skip empty ones and stop after the first error.
CONCEPT
continue jumps to the next iteration; break exits the loop entirely. They give fine
control over iteration without complex conditionals.
COMMAND / PLAYBOOK
terminal
for f in *.log; do
[[ -s $f ]] || continue # skip empty files
grep -q FATAL "$f" && { echo "fatal in $f"; break; }
done
OUTPUT
fatal in [Link]
EXPLANATION
→ continue skips the rest of the body for empty files (-s is false).
Key idea: continue skips an iteration; break exits — both can target outer loops by
level.
CONCEPT
Loops nest freely, but keep bodies small and quote variables. Nested loops
multiply iterations, so watch performance on large inputs.
COMMAND / PLAYBOOK
terminal
OUTPUT
dev:ping
dev:http
prod:ping
prod:http
EXPLANATION
Key idea: Nested loops multiply work — keep inner bodies cheap on large data sets.
You need both the position and the value of each array element.
CONCEPT
Iterating the index keys "${!arr[@]}" gives you positions, while ${arr[i]} gives
values — useful when the index matters, not just the value.
COMMAND / PLAYBOOK
terminal
OUTPUT
1) build
2) test
3) deploy
EXPLANATION
Key idea: "${!arr[@]}" gives indices; combine with ${arr[i]} when the position
matters.
06 Functions
You repeat the same logging line everywhere and want it in one place.
CONCEPT
A function groups commands under a name. It runs in the current shell, sees
positional args as its own $1, $2, and returns an exit status — the building block of
maintainable scripts.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
log() {
printf '%s %s\n' "$(date +%T)" "$*"
}
log "deploy started"
log "deploy finished"
OUTPUT
EXPLANATION
Key idea: Functions run in the current shell, so they can set variables the caller sees.
CONCEPT
Inside a function, $1..$N are its arguments, $# the count, and "$@" the full list.
Functions are argument-driven, just like scripts.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
Key idea: A function's $1/$@ are its own arguments, separate from the script's.
SCENARIO
You want a function that both computes a value and signals success/failure.
CONCEPT
return sets a numeric exit status (0-255) for success/failure; to return data, echo it
and capture with $(...). Confusing the two is a common beginner mistake.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
is_even() { (( $1 % 2 == 0 )); }
double() { echo $(( $1 * 2 )); }
is_even 4 && echo "even"
x=$(double 21); echo "x=$x"
OUTPUT
even
x=42
EXPLANATION
Key idea: return signals status; echo + $(...) is how you 'return' actual data.
CONCEPT
By default variables are global. Declaring them local inside a function scopes them
to that call, preventing surprising side effects.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
i=99
count() { local i; for i in 1 2 3; do :; done; echo "inner i=$i"; }
count
echo "outer i=$i"
OUTPUT
inner i=3
outer i=99
EXPLANATION
Key idea: Declare function variables local to avoid clobbering the caller's state.
Flaky network calls should be retried a few times before giving up.
CONCEPT
Functions can wrap any command, adding cross-cutting behaviour like retries.
Using "$@" lets the helper run whatever command you pass it.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
retry() {
local n=$1; shift
until "$@"; do
(( --n )) || return 1
sleep 2
done
}
retry 3 curl -fsS [Link] >/dev/null && echo ok
OUTPUT
ok
EXPLANATION
Key idea: Wrap commands with a retry() helper using "$@" to make any call
resilient.
CONCEPT
Shell functions return one status, so multiple values are passed via stdout (and
split by the caller) or by assigning to named variables the caller reads.
COMMAND / PLAYBOOK
terminal
OUTPUT
host=web01 port=8080
EXPLANATION
Key idea: Echo space-separated values and split with read to 'return' multiples.
CONCEPT
Put reusable functions in a library file and source it. This keeps individual scripts
short and shares one implementation across a codebase.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
source "$(dirname "$0")/[Link]"
require_root # from [Link]
log "running as root"
OUTPUT
EXPLANATION
→ Resolving the path via dirname "$0" finds the sibling library.
Key idea: Keep common helpers in a sourced [Link] so scripts stay small and
consistent.
SCENARIO
CONCEPT
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
hello() { echo hi; }
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
echo "run directly: demo"
hello
fi
OUTPUT
./[Link]
run directly: demo
hi
EXPLANATION
CONCEPT
COMMAND / PLAYBOOK
terminal
archive() {
for p in "$@"; do echo "adding $p"; done
}
archive [Link] "b [Link]" [Link]
OUTPUT
adding [Link]
adding b [Link]
adding [Link]
EXPLANATION
Key idea: Loop over "$@" to write functions that take any number of arguments.
CONCEPT
grep filters lines by pattern and is scriptable via its exit status and -c count. It's
the first-choice tool for finding and counting matches.
COMMAND / PLAYBOOK
terminal
OUTPUT
errors=12
has fatal
EXPLANATION
Key idea: grep -q is made for conditions; grep -c gives you counts for variables.
SCENARIO
CONCEPT
awk splits lines into fields and can compute across them. It shines for column
extraction and aggregation that would be awkward in pure bash.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
→ The END block runs once after all lines — perfect for summaries.
Key idea: awk's per-line fields plus END block make it ideal for quick aggregations.
CONCEPT
sed applies edits line by line non-interactively. In scripts it's used for substitutions
and in-place edits, often with a backup suffix for safety.
COMMAND / PLAYBOOK
terminal
OUTPUT
env=production
# [Link] preserves the original
EXPLANATION
Key idea: sed -[Link] edits in place but leaves a backup — a safety net in deploys.
CONCEPT
COMMAND / PLAYBOOK
terminal
OUTPUT
root:/bin/bash
daemon:/usr/sbin/nologin
EXPLANATION
Key idea: cut is perfect for single-delimiter fields; awk handles the messy cases.
CONCEPT
sort orders lines; uniq removes adjacent duplicates, so sort must come first.
Together they produce unique, ordered data for reports.
COMMAND / PLAYBOOK
terminal
OUTPUT
[Link]
[Link]
[Link]
EXPLANATION
Key idea: sort -u = sort then unique; uniq needs sorted input to work correctly.
CONCEPT
tr translates, deletes, or squeezes characters from stdin. It's a fast filter for
character-level transforms that don't need regex.
COMMAND / PLAYBOOK
terminal
OUTPUT
hello world
EXPLANATION
Key idea: tr -d '\r' is the quick fix for stripping Windows carriage returns.
SCENARIO
CONCEPT
wc counts lines (-l), words (-w), and bytes (-c). Piping into wc -l is the standard
idiom for counting output lines in scripts.
COMMAND / PLAYBOOK
terminal
n=$(grep -c . [Link])
echo "non-empty lines: $n"
ls *.log | wc -l
OUTPUT
non-empty lines: 4
7
EXPLANATION
Key idea: Pipe any command into wc -l to count how many lines it produced.
You have parallel lists of names and IPs to combine into rows.
CONCEPT
paste joins lines from multiple files column-wise, the horizontal counterpart to cat.
It's handy for stitching related data together.
COMMAND / PLAYBOOK
terminal
OUTPUT
web01,[Link]
web02,[Link]
EXPLANATION
Key idea: paste stitches files into columns; join merges on a shared key field.
CONCEPT
COMMAND / PLAYBOOK
terminal
OUTPUT
NAME IP
web01 [Link]
db01 [Link]
EXPLANATION
→ It's a display aid — don't feed its pretty output back into scripts.
Key idea: column -t makes report output readable; keep raw data for further
processing.
CONCEPT
jq is a JSON processor for the shell. It parses and queries JSON robustly — far safer
than grep/sed hacks that break on formatting changes.
COMMAND / PLAYBOOK
terminal
OUTPUT
auth
payments
EXPLANATION
Key idea: Use jq for JSON — never parse it with grep/sed, which break on
reformatting.
Your scripts keep running past errors and producing corrupt results.
CONCEPT
set -euo pipefail makes bash fail fast: -e on any error, -u on unset variables, -o
pipefail on any pipeline stage failing. It turns silent bugs into immediate stops.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
cp [Link] /tmp/ # fails here
echo "this line never runs"
OUTPUT
EXPLANATION
Key idea: Start serious scripts with set -euo pipefail to fail fast instead of silently.
SCENARIO
A temp file must be removed whether the script succeeds, fails, or is interrupted.
CONCEPT
trap registers a handler for signals or the EXIT event. An EXIT trap runs no matter
how the script ends — the shell's finally block.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
echo data > "$tmp"
process "$tmp"
OUTPUT
EXPLANATION
→ trap 'rm -f "$tmp"' EXIT runs the cleanup on any exit path.
→ You can trap specific signals too (INT, TERM) for custom handling.
Key idea: trap ... EXIT guarantees cleanup even on failure or interruption.
CONCEPT
A trap on ERR fires when a command returns non-zero (under set -e). It centralises
error reporting, printing context like the failing line number.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -Eeuo pipefail
trap 'echo "error on line $LINENO" >&2' ERR
false
echo unreachable
OUTPUT
error on line 4
# then the script exits non-zero
EXPLANATION
Key idea: set -E with a trap ERR gives you a stack-trace-like failure report.
A non-critical command may fail and you want a default action, not a crash.
CONCEPT
Appending || to a command supplies a fallback that runs only on failure. It lets you
tolerate specific errors even under set -e.
COMMAND / PLAYBOOK
terminal
OUTPUT
value=default
EXPLANATION
Key idea: 'cmd || fallback' lets you tolerate a specific expected failure under set -e.
A cron job sometimes overlaps with a previous slow run, corrupting data.
CONCEPT
flock takes an exclusive lock on a file, so a second instance can't start while one is
running. It's the standard guard against overlapping cron jobs.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
exec 9>/tmp/[Link]
flock -n 9 || { echo "already running"; exit 0; }
echo "doing work..."
sleep 5
OUTPUT
doing work...
# a second run prints 'already running' and exits
EXPLANATION
Key idea: flock -n guards cron jobs against overlapping runs — no manual PID files.
CONCEPT
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
Key idea: Wrap potentially-hanging commands in timeout so they can't stall the
whole job.
CONCEPT
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
target=${1:?usage: [Link] <dir>}
[[ -d $target ]] || { echo "not a dir: $target" >&2; exit 2; }
[[ $target != / ]] || { echo "refusing /" >&2; exit 2; }
OUTPUT
./[Link] /
refusing /
EXPLANATION
Key idea: Validate every input before any destructive action — fail before side
effects.
CONCEPT
Idempotent steps check state before changing it, so re-running is harmless. This
makes scripts safe for retries, cron, and configuration management.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
mkdir -p /opt/app/data # -p: no error if exists
grep -q '^appuser:' /etc/passwd || useradd appuser
id appuser >/dev/null && echo "user ready"
OUTPUT
user ready
EXPLANATION
Key idea: Check-then-act (or -p flags) makes scripts safe to re-run any number of
times.
Your scripts need consistent, greppable logs that separate info from errors.
CONCEPT
A small logging function with timestamps and levels (INFO/ERROR to stderr) gives
uniform, filterable output across all your scripts.
COMMAND / PLAYBOOK
[Link]
OUTPUT
EXPLANATION
Key idea: Send errors to stderr and info to stdout so callers can separate them.
SCENARIO
You want to start a task, do other work, then wait for it to finish.
CONCEPT
Appending & backgrounds a command, freeing the shell. $! holds its PID, and wait
blocks until it (or all jobs) complete — the basis of shell concurrency.
COMMAND / PLAYBOOK
terminal
long_task &
pid=$!
echo "started $pid, doing other things"
wait "$pid"
echo "task done"
OUTPUT
EXPLANATION
Key idea: Capture $! right after & — the next background job overwrites it.
Three independent downloads should run at once, not one after another.
CONCEPT
Starting several commands with & and then wait runs them concurrently and
blocks until all finish. It cuts wall-clock time for independent work.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
for url in a b c; do
fetch "$url" &
done
wait
echo "all downloads complete"
OUTPUT
EXPLANATION
Key idea: Launch jobs with & in a loop, then a single wait to join them all.
You need to cd and change variables temporarily without affecting the rest of the
script.
CONCEPT
COMMAND / PLAYBOOK
terminal
dir=/etc
( cd /tmp; touch scratch; echo "inside: $PWD" )
echo "outside: $PWD, dir=$dir"
OUTPUT
inside: /tmp
outside: /home/deploy, dir=/etc
EXPLANATION
Key idea: ( ) isolates cd and variable changes; { } groups in the current shell.
CONCEPT
xargs -P runs commands in parallel with a bounded worker count. It's a simple,
dependency-free way to get controlled concurrency over a list.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
→ This bounds load, unlike launching every job with & at once.
CONCEPT
Trapping SIGINT/SIGTERM lets a script finish current work and release resources
before exiting, instead of dying abruptly — important for daemons and workers.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
running=true
shutdown() { echo "stopping..."; running=false; }
trap shutdown INT TERM
while $running; do work; sleep 1; done
echo "clean exit"
OUTPUT
EXPLANATION
Key idea: Trap INT/TERM to drain work and clean up before a worker exits.
SCENARIO
A wrapper script sets up the environment then hands off to the real program.
CONCEPT
exec replaces the current shell process with another command — no extra
process, and signals go straight to the target. It's ideal for entrypoint wrappers.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
export APP_ENV=prod
ulimit -n 65536
exec /opt/nimbus/bin/api "$@"
OUTPUT
# the shell becomes the api process; same PID, no wrapper left behind
EXPLANATION
Key idea: exec in an entrypoint means signals reach your app, not a lingering shell.
You need a task to keep running after you close the SSH session.
CONCEPT
setsid (or nohup) detaches a process from the controlling terminal so it survives
logout. For anything important, a service manager is still better.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
Key idea: setsid/nohup detach a process, but systemd is better for real services.
CONCEPT
pgrep matches processes by name/attributes and sets its exit status, so it plugs
directly into conditionals without parsing ps output.
COMMAND / PLAYBOOK
terminal
OUTPUT
worker running
EXPLANATION
Key idea: pgrep's exit status makes 'is it running?' checks clean — no ps parsing.
CONCEPT
set -x prints every command (after expansion) before executing it, revealing
exactly what the shell does. set +x turns it off again.
COMMAND / PLAYBOOK
terminal
set -x
name=Maya
echo "hi $name"
set +x
OUTPUT
+ name=Maya
+ echo 'hi Maya'
hi Maya
EXPLANATION
→ You see the resolved values, not the raw source — great for variable bugs.
Key idea: set -x shows commands after expansion — perfect for diagnosing variable
issues.
SCENARIO
CONCEPT
bash -n parses a script and reports syntax errors without running it. It's a fast,
side-effect-free gate for CI or a pre-commit hook.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
SCENARIO
You want to catch quoting bugs and pitfalls a syntax check misses.
CONCEPT
COMMAND / PLAYBOOK
terminal
shellcheck [Link]
OUTPUT
In [Link] line 8:
cp $src $dst
^-- SC2086: Double quote to prevent globbing and word splitting.
EXPLANATION
Key idea: Run shellcheck on every script — it catches bugs no syntax check will.
CONCEPT
PS4 is the prefix printed before each traced line. Customising it to include line
numbers and function names makes -x output far more useful.
COMMAND / PLAYBOOK
terminal
OUTPUT
+ [Link]:greet: echo hi
hi
EXPLANATION
Key idea: Set a rich PS4 with LINENO/FUNCNAME to make set -x traces navigable.
You want optional verbose tracing without editing the script each time.
CONCEPT
Gating set -x behind an environment variable lets you enable tracing on demand
from the caller, keeping normal runs quiet.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
[[ ${DEBUG:-0} == 1 ]] && set -x
echo "working"
OUTPUT
./[Link]
working
DEBUG=1 ./[Link]
+ echo working
working
EXPLANATION
Key idea: Gate set -x behind DEBUG=1 so tracing is opt-in per run.
SCENARIO
A script must run under dash/sh on minimal systems, not just bash.
CONCEPT
Bashisms like [[ ]], arrays, and ${var//} don't exist in POSIX sh. If a script must be
portable, stick to POSIX features and use a /bin/sh shebang.
COMMAND / PLAYBOOK
[Link]
#!/bin/sh
# POSIX-safe: use [ ] not [[ ]], no arrays
if [ "$1" = start ]; then
echo starting
fi
OUTPUT
./[Link] start
starting
EXPLANATION
Key idea: For /bin/sh portability, avoid [[ ]] and arrays — verify with shellcheck -s sh.
SCENARIO
CONCEPT
ls output isn't safe to parse — filenames can contain spaces and newlines. Globs
and find -print0 handle every filename correctly.
COMMAND / PLAYBOOK
terminal
# fragile:
for f in $(ls *.log); do echo "$f"; done
# safe:
for f in *.log; do echo "$f"; done
OUTPUT
EXPLANATION
Key idea: Never loop over ls output — use globs, or find -print0 for anything
recursive.
CONCEPT
Each external command is a new process. For simple string work, bash builtins
(parameter expansion, [[ ]], read) avoid fork/exec overhead and run far faster.
COMMAND / PLAYBOOK
terminal
OUTPUT
EXPLANATION
Key idea: In loops, use parameter expansion instead of echo|sed — no forks, much
faster.
11 Real-World Automation
SCENARIO
Your script needs proper -v, -o file, and -h flags like a real CLI.
CONCEPT
getopts parses single-letter options and their arguments in a standard loop, giving
scripts a conventional, robust command-line interface.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
verbose=0; out=[Link]
while getopts ":vo:h" opt; do
case $opt in
v) verbose=1 ;;
o) out=$OPTARG ;;
h) echo "usage: $0 [-v] [-o file]"; exit 0 ;;
*) echo "bad option"; exit 1 ;;
esac
done
echo "verbose=$verbose out=$out"
OUTPUT
./[Link] -v -o [Link]
verbose=1 out=[Link]
EXPLANATION
→ The option string ":vo:h" declares flags; the : after o means it takes an
argument.
Key idea: getopts gives scripts a conventional flag interface — use it over hand-
rolled parsing.
SCENARIO
CONCEPT
Sourcing a config file pulls its variable assignments into the script. Guarding with
existence checks and defaults keeps it robust when the file is missing.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
config=${1:-/etc/nimbus/[Link]}
[[ -f $config ]] && source "$config"
echo "deploying to ${TARGET:?TARGET not set in config}"
OUTPUT
./[Link] [Link]
deploying to prod-cluster
EXPLANATION
Key idea: Source a config file for env-specific values; validate required keys after
loading.
A destructive cleanup script should preview actions before really doing them.
CONCEPT
A dry-run flag routes destructive commands through a wrapper that either prints
or executes them. It lets users verify intent safely before committing.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
DRY=${DRY:-0}
run() { [[ $DRY == 1 ]] && echo "[dry] $*" || "$@"; }
run rm -rf /tmp/old-cache
run systemctl restart nginx
OUTPUT
DRY=1 ./[Link]
[dry] rm -rf /tmp/old-cache
[dry] systemctl restart nginx
EXPLANATION
→ The run wrapper prints commands in dry mode, executes them otherwise.
Key idea: Route destructive commands through a run() wrapper so a DRY flag can
preview them.
SCENARIO
CONCEPT
Extracting each release into its own directory and atomically re-pointing a 'current'
symlink makes deploys instant and rollbacks trivial — the release-directory
pattern.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
ver=${1:?version}
rel=/opt/nimbus/releases/$ver
mkdir -p "$rel"; tar -xzf "nimbus-$[Link]" -C "$rel"
ln -sfn "$rel" /opt/nimbus/current
systemctl reload nimbus
OUTPUT
./[Link] 2.3.0
# current -> releases/2.3.0; reload picks it up
EXPLANATION
Key idea: Release into versioned dirs and flip a symlink — instant deploys and
rollbacks.
SCENARIO
CONCEPT
A backup script creates a timestamped archive, then prunes old ones to cap
retention and disk use — a self-maintaining routine safe to run from cron.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
dir=/backups; mkdir -p "$dir"
tar -czf "$dir/db-$(date +%F).tgz" /var/lib/nimbus
ls -1t "$dir"/db-*.tgz | tail -n +8 | xargs -r rm -v
OUTPUT
removed '/backups/[Link]'
# newest 7 kept, older ones pruned
EXPLANATION
Key idea: List newest-first, skip the count you keep, and rm the rest for simple
rotation.
A job should post a message to Slack (or any webhook) when it finishes or fails.
CONCEPT
curl can POST JSON to a webhook, letting scripts report status to chat or alerting
systems. A helper function keeps the call reusable across scripts.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
notify() {
curl -sf -X POST -H 'Content-Type: application/json' \
-d "{\"text\":\"$1\"}" "$WEBHOOK_URL" >/dev/null
}
notify "deploy of 2.3.0 succeeded" && echo sent
OUTPUT
sent
EXPLANATION
Key idea: Wrap webhook posts in a notify() function; build tricky JSON with jq to
escape it.
SCENARIO
A cron job needs locking, logging, and a sane environment all at once.
CONCEPT
Cron runs with a minimal environment and no locking. A wrapper that sets PATH,
takes a lock, and logs output turns a fragile cron entry into a reliable job.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
export PATH=/usr/local/bin:/usr/bin:/bin
exec 9>/tmp/[Link]; flock -n 9 || exit 0
exec >>/var/log/[Link] 2>&1
echo "$(date +%FT%T) starting"
/opt/nimbus/[Link]
OUTPUT
EXPLANATION
Key idea: A cron wrapper should set PATH, lock with flock, and redirect output to a
log.
SCENARIO
A deploy step must block until the new app reports healthy, then continue.
CONCEPT
A bounded polling loop with a timeout waits for eventual readiness without
hanging forever — combining retries, a delay, and an overall attempt cap.
COMMAND / PLAYBOOK
[Link]
#!/usr/bin/env bash
set -euo pipefail
for i in {1..30}; do
if curl -fsS [Link] >/dev/null; then
echo "healthy after $i tries"; exit 0
fi
sleep 2
done
echo "never became healthy" >&2; exit 1
OUTPUT
EXPLANATION
Key idea: Bound readiness polling with a max attempt count so a stuck deploy still
fails.
🎓 EngiDock
Shell Scripting — 100 Scenarios Explained · ★ Learn once, own for life.