0% found this document useful (0 votes)
2 views102 pages

Shell Scripting - 100 Scenarios

EngiDock's 'Shell Scripting — 100 Scenarios Explained' provides practical scenarios for shell scripting, covering various concepts and commands with detailed explanations. It is structured into 11 categories, guiding users from beginner to expert level. Each scenario includes a command playbook, expected output, and key ideas for effective scripting.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views102 pages

Shell Scripting - 100 Scenarios

EngiDock's 'Shell Scripting — 100 Scenarios Explained' provides practical scenarios for shell scripting, covering various concepts and commands with detailed explanations. It is structured into 11 categories, guiding users from beginner to expert level. Each scenario includes a command playbook, expected output, and key ideas for effective scripting.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

🎓 EngiDock

Shell Scripting — 100 Scenarios,


Explained
Real situations a shell scripter faces, each with the concept behind it, the
exact command, the output you should see, and a clear line-by-line
explanation. Built to teach the why, not just the how.

100 scenarios 11 categories Concept + explanation on every card

Beginner → Expert

01 Script Basics & Structure

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 1 of 102


#001 Write and run your first script
SCENARIO

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.

→ chmod +x makes the file executable so ./[Link] works.

→ 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.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 2 of 102


#002 Understand exit codes

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

grep root /etc/passwd >/dev/null


echo "found: $?"
grep nobody-xyz /etc/passwd >/dev/null
echo "missing: $?"

OUTPUT

found: 0
missing: 1

EXPLANATION

→ A successful grep returns 0; no match returns 1.

→ $? captures the exit status of the previous command only.

→ if, &&, and || all branch on these codes automatically.

Key idea: Check $? immediately — the next command overwrites it.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 3 of 102


#003 Set your own exit status

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

→ exit 1 ends the script with a failure status.

→ >&2 sends the error message to stderr, not stdout.

→ A bare exit uses the status of the last command run.

Key idea: Reserve exit 0 for success; use distinct non-zero codes for different
failures.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 4 of 102


#004 Access positional arguments

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

./[Link] Maya hello


script: ./[Link]
args: 2 -> Maya hello
first: Maya

EXPLANATION

→ $1, $2 are the first and second arguments.

→ $# is the argument count; $@ expands to all of them.

→ $0 is how the script was invoked — useful in usage messages.

Key idea: Always quote "$@" so arguments with spaces stay intact.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 5 of 102


#005 Require and validate arguments

SCENARIO

A deploy script must refuse to run without a version argument.

CONCEPT

Parameter expansion with :? aborts with a message when a variable is unset or


empty. It's the concise way to enforce required inputs at the top of a script.

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

→ ${1:?msg} exits immediately if $1 is missing, printing msg.

→ This validates input before any real work happens.

→ The script stops with a non-zero status, so callers notice the failure.

Key idea: ${var:?message} is a one-line guard for required parameters.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 6 of 102


#006 Consume arguments with shift

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

./[Link] clean [Link] [Link]


action=clean
file: [Link]
file: [Link]

EXPLANATION

→ shift removes the first argument so $@ now holds only the files.

→ This separates a command word from its list of targets.

→ shift N drops the first N arguments at once.

Key idea: shift is the classic way to split a leading command from its remaining
arguments.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 7 of 102


#007 Reuse code by sourcing a file

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

→ source ./[Link] loads its definitions into the current shell.

→ Executing [Link] instead would run it in a subshell and lose the functions.

→ Sourcing is how shells load profiles and shared libraries.

Key idea: source shares the current shell; running a script uses a separate subshell.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 8 of 102


#008 Add helpful comments and usage

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

→ Lines starting with # (after the shebang) are comments.

→ A usage() function centralises the help message.

→ [[ $# -eq 1 ]] || usage shows help when the arg count is wrong.

Key idea: A usage() function keeps help text in one place and doubles as input
validation.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 9 of 102


#009 Find the script's own directory

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

→ ${BASH_SOURCE[0]} is the script's path even when sourced.

→ dirname plus cd ... && pwd yields its absolute directory.

→ Now sibling files are addressed by $script_dir/..., independent of cwd.

Key idea: Resolve $script_dir so a script can find its companion files anywhere.

02 Variables & Expansion

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 10 of 102


#010 Assign and quote variables
SCENARIO

A value with spaces keeps splitting into multiple arguments.

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

ls: cannot access '/opt/my': No such file or directory


# quoted form lists '/opt/my app' correctly

EXPLANATION

→ name=value with no spaces around = defines a variable.

→ Unquoted $path splits on the space into two arguments.

→ "$path" keeps the whole value together.

Key idea: Quote variable expansions by default; unquoted values split and glob.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 11 of 102


#011 Capture command output

SCENARIO

You need today's date embedded in a filename inside a script.

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

→ $(date +%F) runs date and captures its output.

→ Prefer $(...) over backticks — it nests and reads more clearly.

→ Trailing newlines in the output are stripped automatically.

Key idea: Use $(...) not backticks — it nests cleanly and is easier to read.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 12 of 102


#012 Provide default values

SCENARIO

A variable may be unset and you want a sensible fallback without an if.

CONCEPT

Parameter expansion offers inline defaults: ${var:-default} uses a fallback if


unset, and ${var:=default} also assigns it. This removes boilerplate conditionals.

COMMAND / PLAYBOOK

terminal

echo "env=${APP_ENV:-dev}"
: "${PORT:=8080}"
echo "port=$PORT"

OUTPUT

env=dev
port=8080

EXPLANATION

→ ${APP_ENV:-dev} yields dev when APP_ENV is unset or empty.

→ ${PORT:=8080} also assigns the default back to PORT.

→ The leading : is a no-op command used just to trigger the assignment.

Key idea: ${v:-x} substitutes a default; ${v:=x} also assigns it for later use.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 13 of 102


#013 Extract substrings and length

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

→ ${sha:0:7} takes 7 characters starting at offset 0.

→ ${#sha} returns the string's length.

→ These native operations are faster than spawning cut or awk.

Key idea: Native ${var:off:len} and ${#var} avoid launching external tools.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 14 of 102


#014 Strip prefixes and suffixes

SCENARIO

You want the filename without its path, and the name without its extension.

CONCEPT

Pattern-removal expansions strip matches: # / ## from the front, % / %% from


the back. They handle path and extension manipulation without basename or sed.

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

→ ${p##*/} removes everything up to the last slash (basename).

→ ${p%.log} removes the trailing .log (a specific suffix).

→ ${base%.*} strips any extension after the last dot.

Key idea: ## and %% strip the longest match; # and % strip the shortest.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 15 of 102


#015 Search and replace within a variable

SCENARIO

You must turn a path's slashes into dashes for a safe filename.

CONCEPT

${var/pattern/repl} replaces the first match; ${var//pattern/repl} replaces all. It's


in-shell find-and-replace without invoking sed.

COMMAND / PLAYBOOK

terminal

path="a/b/c"
echo "${path//\//-}"
name="Build FAILED"
echo "${name/FAILED/ok}"

OUTPUT

a-b-c
Build ok

EXPLANATION

→ ${path//\//-} replaces every slash with a dash (slash escaped).

→ A single / replaces only the first occurrence.

→ No external process is spawned — the shell does it directly.

Key idea: ${v//old/new} is global replace; ${v/old/new} replaces only the first
match.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 16 of 102


#016 Do arithmetic in the shell
SCENARIO

You need to compute a retry count and a percentage inside a script.

CONCEPT

Arithmetic expansion $((...)) evaluates integer math. (( )) is the arithmetic


command, useful for counters and conditions, all without expr.

COMMAND / PLAYBOOK

terminal

count=5
echo $(( count * 2 + 1 ))
(( count-- ))
echo "now $count"

OUTPUT

11
now 4

EXPLANATION

→ $(( )) evaluates and substitutes an integer result.

→ (( count-- )) updates a variable in place.

→ Bash arithmetic is integer-only; use awk or bc for floating point.

Key idea: $(( )) does integer math; reach for bc/awk when you need decimals.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 17 of 102


#017 Work with indexed arrays

SCENARIO

You need to collect a list of servers and iterate over them.

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

servers=(web01 web02 "db 01")


echo "count: ${#servers[@]}"
for s in "${servers[@]}"; do echo "- $s"; done

OUTPUT

count: 3
- web01
- web02
- db 01

EXPLANATION

→ arr=(a b c) defines an indexed array.

→ ${#arr[@]} is the element count; ${arr[@]} is all elements.

→ Quoting "${arr[@]}" keeps elements with spaces intact.

Key idea: Always quote "${arr[@]}" so elements containing spaces don't split.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 18 of 102


#018 Use associative arrays (maps)
SCENARIO

You want to map environment names to their URLs by key.

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

→ declare -A url creates an associative array.

→ Keys are strings: url[prod] stores and retrieves by name.

→ Iterate keys with "${!url[@]}" and values with "${url[@]}".

Key idea: Associative arrays need bash 4+; they replace clunky case-based lookups.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 19 of 102


#019 Understand variable scope and export

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

child sees GREETING= TOKEN=abc123

EXPLANATION

→ GREETING stays local, so the child sees it empty.

→ export TOKEN puts it in the environment, so the child inherits it.

→ Environment variables flow down to children, never back up to parents.

Key idea: export makes a variable visible to child processes; plain vars stay local.

03 Input, Output & Redirection

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 20 of 102


#020 Print reliably with printf
SCENARIO

echo behaves inconsistently with escapes and flags across systems.

CONCEPT

printf formats output with an explicit format string, portably and predictably —
unlike echo, whose flag/escape behaviour varies between shells.

COMMAND / PLAYBOOK

terminal

printf '%s = %d\n' PORT 8080


printf '%-8s %s\n' name Maya

OUTPUT

PORT = 8080
name Maya

EXPLANATION

→ %s and %d are string and integer placeholders.

→ %-8s left-aligns within 8 columns for tidy tables.

→ 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.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 21 of 102


#021 Read input from the user

SCENARIO

A script needs to prompt for a value and read the reply.

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

→ -r keeps backslashes literal; -p shows a prompt.

→ -s hides typed input — use it for passwords and tokens.

→ read without options reads into the default variable REPLY.

Key idea: Always use read -r so backslashes in input aren't mangled.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 22 of 102


#022 Redirect stdout and stderr

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

./[Link] >[Link] 2>[Link]


./[Link] >[Link] 2>&1

OUTPUT

# [Link] = normal output, [Link] = errors


# [Link] = both streams combined

EXPLANATION

→ >[Link] redirects stdout; 2>[Link] redirects stderr.

→ 2>&1 points stderr at stdout's current target.

→ Order matters: put 2>&1 after the stdout redirect to combine them.

Key idea: '&gt;file 2&gt;&amp;1' merges both streams; '2&gt;&amp;1 &gt;file' does
not — order matters.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 23 of 102


#023 Discard unwanted output

SCENARIO

You only care whether a command succeeds, not what it prints.

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

if command -v docker >/dev/null 2>&1; then


echo "docker present"
fi

OUTPUT

docker present

EXPLANATION

→ >/dev/null throws away stdout; 2>&1 also drops stderr.

→ The command still runs; only its output is discarded.

→ The if uses the exit status, which redirection doesn't change.

Key idea: '&gt;/dev/null 2&gt;&amp;1' silences a command while preserving its exit
code.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 24 of 102


#024 Feed multi-line text with a here-document
SCENARIO

You need to write a small config file from inside a script.

CONCEPT

A here-doc (<<EOF) streams a block of text to a command's stdin. Quoting the


delimiter ('EOF') disables variable expansion when you want literal text.

COMMAND / PLAYBOOK

[Link]

#!/usr/bin/env bash
cat > [Link] <<EOF
port=${PORT:-8080}
env=$APP_ENV
EOF

OUTPUT

# [Link] written with variables expanded


port=8080
env=prod

EXPLANATION

→ <<EOF ... EOF sends the enclosed lines as input to cat.

→ Variables expand inside an unquoted here-doc.

→ Use <<'EOF' to keep text literal (no expansion).

Key idea: Quote the delimiter (<<'EOF') when you want the text kept literal.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 25 of 102


#025 Pass a short string as input
SCENARIO

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

read -r a b <<< "first second"


echo "$a | $b"
grep -o 'v[0-9]*' <<< "release v42 shipped"

OUTPUT

first | second
v42

EXPLANATION

→ <<< "..." feeds the string to the command's stdin.

→ Here it splits a line into fields and greps within a string.

→ It avoids an extra echo process and a pipe.

Key idea: <<< is a lightweight way to pipe one string into a command.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 26 of 102


#026 Write to a file and the screen with tee

SCENARIO

A build should show output live and also save it to a log.

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

./[Link] 2>&1 | tee [Link]


echo done | tee -a [Link]

OUTPUT

Compiling...
Build succeeded
# same text saved to [Link]; 'done' appended

EXPLANATION

→ tee [Link] writes the stream to the file and the terminal.

→ 2>&1 | ensures errors are captured too.

→ -a appends rather than truncating the file.

Key idea: Pipe through tee to watch output live while still logging it.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 27 of 102


#027 Use process substitution

SCENARIO

You want to diff the output of two commands without temp files.

CONCEPT

Process substitution <(cmd) presents a command's output as a filename. It lets


tools that expect files consume live command output directly.

COMMAND / PLAYBOOK

terminal

diff <(sort [Link]) <(sort [Link])

OUTPUT

3c3
< web03
---
> web04

EXPLANATION

→ <(sort [Link]) gives diff a file-like handle to sorted output.

→ No temporary files are created or cleaned up.

→ It works with any command expecting file arguments.

Key idea: <(cmd) feeds live command output to tools that want a filename.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 28 of 102


#028 Build pipelines
SCENARIO

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

awk '{print $1}' [Link] | sort | uniq -c | sort -rn | head -3

OUTPUT

842 [Link]
311 [Link]
108 [Link]

EXPLANATION

→ Each | streams output into the next stage.

→ Small tools (awk, sort, uniq) combine into a powerful query.

→ By default a pipeline's exit status is that of the last command.

Key idea: Set 'set -o pipefail' so a failure anywhere in a pipeline is not hidden.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 29 of 102


#029 Turn output lines into arguments with xargs
SCENARIO

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

find . -name '*.tmp' -print0 | xargs -0 rm -v

OUTPUT

removed './cache/[Link]'
removed './b [Link]'

EXPLANATION

→ find -print0 separates names with NUL, not spaces.

→ xargs -0 reads NUL-separated input, so odd filenames are safe.

→ Add -n1 or -P to control batching and parallelism.

Key idea: Use find -print0 | xargs -0 to handle filenames with spaces safely.

04 Conditionals & Tests

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 30 of 102


#030 Branch with if / elif / else
SCENARIO

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

→ if executes the branch whose test succeeds (exit 0).

→ [[ $1 == prod ]] compares strings without word-splitting pitfalls.

→ elif chains additional conditions; else is the fallback.

Key idea: Prefer [[ ]] over [ ] in bash — it's safer with unquoted vars and adds
features.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 31 of 102


#031 Test strings

SCENARIO

You need to check whether a variable is empty or matches a value.

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

→ -z is true when the string is empty; -n when non-empty.

→ == inside [[ ]] also does glob pattern matching (M*).

→ Quote the right side to force a literal comparison instead of a pattern.

Key idea: Inside [[ ]], == does pattern matching unless you quote the right-hand
side.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 32 of 102


#032 Compare numbers
SCENARIO

You must act when a disk usage percentage crosses a threshold.

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

disk critical: 92%


over half

EXPLANATION

→ (( usage >= 90 )) does a clean numeric comparison.

→ -gt and friends are the test-style numeric operators.

→ Never use == for numbers — it compares them as strings.

Key idea: Use (( )) or -gt/-lt for numbers; == is string comparison and will surprise
you.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 33 of 102


#033 Test files and directories
SCENARIO

A script must verify a file exists and is readable before using it.

CONCEPT

File-test operators check existence and attributes: -f (file), -d (directory), -r


(readable), -x (executable), -s (non-empty). They prevent acting on missing or
wrong paths.

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

→ -f is true only for regular files (not directories).

→ -r, -w, -x check read/write/execute permission for you.

→ -e checks mere existence regardless of type.

Key idea: Guard with [[ -f ]] / [[ -d ]] before reading paths to fail early and clearly.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 34 of 102


#034 Match many cases with case
SCENARIO

A control script handles start, stop, restart, and status subcommands.

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 ;;.

→ *) is the catch-all default case.

→ Patterns are globs, so start|stop and wildcards both work.

Key idea: case is the idiomatic dispatcher for subcommands — clearer than if/elif
chains.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 35 of 102


#035 Chain commands with && and ||

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

mkdir -p /opt/app && echo "ready" || echo "failed"


ping -c1 db01 >/dev/null 2>&1 || echo "db unreachable"

OUTPUT

ready
# the ping line prints only if the ping fails

EXPLANATION

→ A && B runs B only when A returns 0.

→ A || B runs B only when A returns non-zero.

→ Be careful chaining all three — A && B || C runs C if B fails too.

Key idea: && and || are inline conditionals; for anything non-trivial, use if for clarity.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 36 of 102


#036 Combine conditions

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

→ && inside [[ ]] requires both conditions to be true.

→ ! negates a test; here 'not paused'.

→ Group with ( ) inside [[ ]] when mixing && and ||.

Key idea: Combine tests inside a single [[ ]] with && , || , ! and parentheses.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 37 of 102


#037 Match with regular expressions
SCENARIO

You must validate that an input looks like a semantic version.

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

→ =~ matches the left side against an extended regex.

→ Do not quote the regex, or it becomes a literal string.

→ Capture groups land in the BASH_REMATCH array.

Key idea: Leave the =~ regex unquoted; quoting turns it into a literal match.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 38 of 102


#038 Give tests a default to avoid errors
SCENARIO

An unset variable in a test causes a syntax error under set -u.

CONCEPT

Expanding a possibly-unset variable with a default inside a test keeps it well-


formed. This pairs with strict mode to avoid 'unbound variable' failures.

COMMAND / PLAYBOOK

terminal

set -u
flag=${DEBUG:-0}
[[ $flag == 1 ]] && echo "debug on" || echo "debug off"

OUTPUT

debug off

EXPLANATION

→ ${DEBUG:-0} supplies 0 when DEBUG is unset.

→ Under set -u, referencing an unset variable would abort the script.

→ Defaulting keeps the test valid and predictable.

Key idea: Default optional vars (${v:-...}) so tests stay safe under set -u.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 39 of 102


#039 Use the exit status directly in if
SCENARIO

A health check should branch on whether an HTTP call succeeds.

CONCEPT

if tests a command's exit code, so you rarely need [[ ]] around a command.


Running the command as the condition is idiomatic and clear.

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

→ if curl ... branches on curl's own exit status.

→ -f makes curl fail on HTTP errors, so the test is meaningful.

→ No brackets are needed — the command itself is the condition.

Key idea: if <command> tests its exit code directly — no [[ ]] wrapper required.

05 Loops

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 40 of 102


#040 Loop over a list

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

for host in web01 web02 db01; do


echo "pinging $host"
done

OUTPUT

pinging web01
pinging web02
pinging db01

EXPLANATION

→ for x in a b c runs the body once per item.

→ The loop variable host takes each value in turn.

→ 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).

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 41 of 102


#041 Loop a fixed number of times
SCENARIO

You want to retry an action up to five times with a counter.

CONCEPT

The C-style for (( ; ; )) loop gives an explicit numeric counter, ideal for fixed
iteration counts and index-based work.

COMMAND / PLAYBOOK

terminal

for (( i=1; i<=3; i++ )); do


echo "attempt $i"
done

OUTPUT

attempt 1
attempt 2
attempt 3

EXPLANATION

→ (( i=1; i<=3; i++ )) initialises, tests, and increments a counter.

→ It reads like C and is best when you need the index itself.

→ Use {1..3} brace expansion for simple ranges instead.

Key idea: C-style for (( )) is best when you need an explicit numeric index.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 42 of 102


#042 Loop while a condition holds
SCENARIO

A script must wait until a service port becomes reachable.

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

waiting for port 8080...


waiting for port 8080...
up!

EXPLANATION

→ until loops while the command keeps failing (the inverse of while).

→ Here it polls the port until nc connects.

→ Always add a sleep so polling doesn't spin the CPU.

Key idea: until CMD; do ...; done polls until CMD succeeds — add a sleep to be kind
to the CPU.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 43 of 102


#043 Read a file line by line
SCENARIO

You need to process each line of a hosts file, preserving spaces.

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

→ IFS= stops leading/trailing whitespace from being stripped.

→ -r keeps backslashes literal.

→ < [Link] feeds the file into the loop's stdin.

Key idea: 'while IFS= read -r line' is the canonical safe way to read lines.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 44 of 102


#044 Iterate over array elements
SCENARIO

You collected results in an array and need to act on each.

CONCEPT

Quoting "${arr[@]}" expands each element as its own word, so a for loop handles
values containing spaces correctly.

COMMAND / PLAYBOOK

terminal

files=("report [Link]" [Link])


for f in "${files[@]}"; do
echo "[$f]"
done

OUTPUT

[report [Link]]
[[Link]]

EXPLANATION

→ "${files[@]}" yields each element intact, spaces and all.

→ Without quotes, 'report [Link]' would split into two iterations.

→ This is why array quoting matters so much in loops.

Key idea: Quote "${arr[@]}" in loops or elements with spaces will split apart.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 45 of 102


#045 Generate ranges and sequences
SCENARIO

You want to create numbered directories or step through numbers.

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

for i in {1..3}; do mkdir -p "data$i"; done


for i in $(seq 0 2 6); do echo -n "$i "; done; echo

OUTPUT

# created data1 data2 data3


0 2 4 6

EXPLANATION

→ {1..3} expands to 1 2 3 before the loop runs.

→ seq 0 2 6 counts from 0 to 6 in steps of 2.

→ 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.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 46 of 102


#046 Skip and stop with continue and break

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).

→ break leaves the loop as soon as a FATAL line is found.

→ Both accept a level (break 2) to affect outer loops.

Key idea: continue skips an iteration; break exits — both can target outer loops by
level.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 47 of 102


#047 Nest loops carefully
SCENARIO

You must run every check against every environment.

CONCEPT

Loops nest freely, but keep bodies small and quote variables. Nested loops
multiply iterations, so watch performance on large inputs.

COMMAND / PLAYBOOK

terminal

for env in dev prod; do


for check in ping http; do
echo "$env:$check"
done
done

OUTPUT

dev:ping
dev:http
prod:ping
prod:http

EXPLANATION

→ The inner loop runs fully for each outer iteration.

→ Total iterations multiply — here 2 × 2 = 4.

→ Use break N/continue N to control specific loop levels.

Key idea: Nested loops multiply work — keep inner bodies cheap on large data sets.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 48 of 102


#048 Loop with an index over an array
SCENARIO

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

steps=(build test deploy)


for i in "${!steps[@]}"; do
printf '%d) %s\n' "$((i+1))" "${steps[i]}"
done

OUTPUT

1) build
2) test
3) deploy

EXPLANATION

→ "${!steps[@]}" expands to the array's indices (0 1 2).

→ ${steps[i]} fetches the value at that index.

→ This is how you number or cross-reference elements.

Key idea: "${!arr[@]}" gives indices; combine with ${arr[i]} when the position
matters.

06 Functions

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 49 of 102


#049 Define and call a function
SCENARIO

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

10:20:01 deploy started


10:20:04 deploy finished

EXPLANATION

→ name() { ... } defines a function.

→ $* is all arguments passed to the function, joined.

→ Call it like any command — log "message".

Key idea: Functions run in the current shell, so they can set variables the caller sees.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 50 of 102


#050 Pass and use arguments
SCENARIO

A function needs to accept parameters like a mini-command.

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

greet() { echo "Hello, ${1:-world} (got $# args)"; }


greet
greet Maya

OUTPUT

Hello, world (got 0 args)


Hello, Maya (got 1 args)

EXPLANATION

→ $1 is the function's first argument, independent of the script's.

→ ${1:-world} supplies a default when none is passed.

→ $# reports how many arguments the function received.

Key idea: A function's $1/$@ are its own arguments, separate from the script's.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 51 of 102


#051 Return status vs return data

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

→ is_even returns a status used directly in &&.

→ double echoes a value captured with command substitution.

→ return only carries a small integer, never a string.

Key idea: return signals status; echo + $(...) is how you 'return' actual data.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 52 of 102


#052 Keep variables local
SCENARIO

A helper function accidentally clobbers a variable used elsewhere.

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

→ local i makes the loop variable private to the function.

→ Without local, the loop would overwrite the outer i.

→ Always declare function-internal variables local.

Key idea: Declare function variables local to avoid clobbering the caller's state.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 53 of 102


#053 Build a reusable retry helper
SCENARIO

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

→ shift separates the retry count from the command to run.

→ "$@" executes the passed command each attempt.

→ (( --n )) || return 1 gives up after n attempts fail.

Key idea: Wrap commands with a retry() helper using "$@" to make any call
resilient.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 54 of 102


#054 Return multiple values
SCENARIO

A function needs to hand back two related values.

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

split_host() { echo "${1%%:*} ${1##*:}"; }


read -r host port < <(split_host web01:8080)
echo "host=$host port=$port"

OUTPUT

host=web01 port=8080

EXPLANATION

→ The function echoes both fields separated by a space.

→ read -r host port splits them into two variables.

→ Process substitution < <(...) feeds the output into read.

Key idea: Echo space-separated values and split with read to 'return' multiples.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 55 of 102


#055 Organise a script library
SCENARIO

Multiple scripts share validation and logging helpers.

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

10:30:00 running as root

EXPLANATION

→ source [Link] imports its functions into this script.

→ Resolving the path via dirname "$0" finds the sibling library.

→ Shared helpers live once, used by many scripts.

Key idea: Keep common helpers in a sourced [Link] so scripts stay small and
consistent.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 56 of 102


#056 Guard a library from direct execution

SCENARIO

A library file should provide functions but do nothing if run directly.

CONCEPT

Comparing ${BASH_SOURCE[0]} to $0 tells whether a file was sourced or


executed, so a library can expose functions yet skip its 'main' block when sourced.

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

→ When sourced, BASH_SOURCE[0] differs from $0, so the block is skipped.

→ When executed, they're equal and the demo/main runs.

→ This lets one file be both a library and a runnable script.

Key idea: [[ "${BASH_SOURCE[0]}" == "$0" ]] detects run-directly vs sourced.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 57 of 102


#057 Handle a variable number of arguments
SCENARIO

A function should accept any number of paths and act on each.

CONCEPT

"$@" expands to every argument, so a function can be variadic. Looping over


"$@" processes each argument safely regardless of count.

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

→ "$@" preserves each argument, including those with spaces.

→ The loop runs once per argument, whatever the count.

→ This makes the function accept 0, 1, or many inputs.

Key idea: Loop over "$@" to write functions that take any number of arguments.

07 Text Processing in Scripts

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 58 of 102


#058 Filter lines with grep
SCENARIO

A script needs to count how many error lines a log contains.

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

errors=$(grep -c -i error [Link])


echo "errors=$errors"
grep -q FATAL [Link] && echo "has fatal"

OUTPUT

errors=12
has fatal

EXPLANATION

→ -c returns the count of matching lines directly.

→ -q is quiet — use it in tests, it just sets the exit status.

→ -i makes the match case-insensitive.

Key idea: grep -q is made for conditions; grep -c gives you counts for variables.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 59 of 102


#059 Extract fields with awk

SCENARIO

You want the average response time from column 10 of a log.

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

awk '{sum+=$10; n++} END{printf "avg=%.1f over %d\n", sum/n, n}'


[Link]

OUTPUT

avg=42.7 over 10428

EXPLANATION

→ Fields are $1..$N; here $10 is the response time.

→ Variables persist across lines, enabling running totals.

→ 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.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 60 of 102


#060 Edit text with sed
SCENARIO

A script must change a config value in place during deployment.

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

sed -[Link] 's/^env=.*/env=production/' [Link]


grep '^env=' [Link]

OUTPUT

env=production
# [Link] preserves the original

EXPLANATION

→ s/old/new/ substitutes on matching lines.

→ -[Link] edits in place and keeps a .bak backup.

→ Anchors like ^ target lines precisely to avoid over-matching.

Key idea: sed -[Link] edits in place but leaves a backup — a safety net in deploys.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 61 of 102


#061 Select columns with cut
SCENARIO

You need just the username and shell from /etc/passwd.

CONCEPT

cut extracts fields by a single-character delimiter or by character position. It's


simpler and faster than awk when you only need fixed fields.

COMMAND / PLAYBOOK

terminal

cut -d: -f1,7 /etc/passwd | head -2

OUTPUT

root:/bin/bash
daemon:/usr/sbin/nologin

EXPLANATION

→ -d: sets colon as the field delimiter.

→ -f1,7 selects the first and seventh fields.

→ cut can't handle multi-space delimiters — use awk for those.

Key idea: cut is perfect for single-delimiter fields; awk handles the messy cases.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 62 of 102


#062 Sort and de-duplicate
SCENARIO

You want a unique, sorted list of IPs seen in a log.

CONCEPT

sort orders lines; uniq removes adjacent duplicates, so sort must come first.
Together they produce unique, ordered data for reports.

COMMAND / PLAYBOOK

terminal

awk '{print $1}' [Link] | sort -u | head -3

OUTPUT

[Link]
[Link]
[Link]

EXPLANATION

→ sort -u sorts and removes duplicates in one step.

→ uniq alone only collapses adjacent duplicates, so sort first.

→ Add -n for numeric sort and -r to reverse.

Key idea: sort -u = sort then unique; uniq needs sorted input to work correctly.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 63 of 102


#063 Translate and squeeze with tr
SCENARIO

You need to normalise input to lowercase and collapse repeated spaces.

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

echo "Hello WORLD" | tr 'A-Z' 'a-z' | tr -s ' '

OUTPUT

hello world

EXPLANATION

→ tr 'A-Z' 'a-z' maps uppercase to lowercase.

→ -s ' ' squeezes repeated spaces into one.

→ -d deletes characters entirely (e.g. carriage returns).

Key idea: tr -d '\r' is the quick fix for stripping Windows carriage returns.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 64 of 102


#064 Count with wc

SCENARIO

A script must know how many entries a file has.

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

→ wc -l counts lines from stdin or a file.

→ grep -c . counts non-empty lines (a common tweak).

→ Capture with $( ) to store a count in a variable.

Key idea: Pipe any command into wc -l to count how many lines it produced.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 65 of 102


#065 Merge files side by side with paste
SCENARIO

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

paste -d, [Link] [Link]

OUTPUT

web01,[Link]
web02,[Link]

EXPLANATION

→ paste merges corresponding lines from each file.

→ -d, sets the delimiter between joined columns.

→ It pairs lines positionally, so files should be the same length.

Key idea: paste stitches files into columns; join merges on a shared key field.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 66 of 102


#066 Format columns with column
SCENARIO

Your script's tabular output is misaligned and hard to read.

CONCEPT

column -t aligns whitespace- or delimiter-separated input into neat columns,


turning ragged output into a readable table for humans.

COMMAND / PLAYBOOK

terminal

printf 'NAME IP\nweb01 [Link]\ndb01 [Link]\n' | column -t

OUTPUT

NAME IP
web01 [Link]
db01 [Link]

EXPLANATION

→ column -t auto-sizes columns for alignment.

→ -s, lets you align delimiter-separated data (like CSV).

→ 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.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 67 of 102


#067 Parse JSON with jq
SCENARIO

A script consumes a JSON API response and needs one field.

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

curl -s [Link] | jq -r '.services[] | select(.up) |


.name'

OUTPUT

auth
payments

EXPLANATION

→ jq -r outputs raw strings (no quotes) for shell use.

→ The filter selects up services and prints their names.

→ jq understands JSON structure, so whitespace and order don't matter.

Key idea: Use jq for JSON — never parse it with grep/sed, which break on
reformatting.

08 Error Handling & Robustness

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 68 of 102


#068 Enable strict mode
SCENARIO

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

cp: cannot stat '[Link]': No such file or directory


# script exits immediately, code 1

EXPLANATION

→ -e aborts the script the moment a command fails.

→ -u treats referencing an unset variable as an error.

→ -o pipefail propagates failures from inside pipelines.

Key idea: Start serious scripts with set -euo pipefail to fail fast instead of silently.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 69 of 102


#069 Clean up with trap on EXIT

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

# $tmp removed on success, error, or Ctrl-C

EXPLANATION

→ trap 'rm -f "$tmp"' EXIT runs the cleanup on any exit path.

→ mktemp creates a unique temp file safely.

→ You can trap specific signals too (INT, TERM) for custom handling.

Key idea: trap ... EXIT guarantees cleanup even on failure or interruption.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 70 of 102


#070 Catch errors with trap ERR
SCENARIO

You want a clear diagnostic whenever any command fails.

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

→ trap ... ERR runs when a command fails.

→ -E ensures the ERR trap is inherited by functions and subshells.

→ $LINENO reports where the failure happened for easy debugging.

Key idea: set -E with a trap ERR gives you a stack-trace-like failure report.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 71 of 102


#071 Provide a fallback with ||
SCENARIO

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

value=$(cat /etc/[Link] 2>/dev/null || echo "default")


echo "value=$value"

OUTPUT

value=default

EXPLANATION

→ The || branch runs because the file is missing.

→ 2>/dev/null hides the expected error message.

→ This deliberately handles a failure without aborting under set -e.

Key idea: 'cmd || fallback' lets you tolerate a specific expected failure under set -e.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 72 of 102


#072 Prevent concurrent runs with a lock
SCENARIO

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

→ exec 9>file opens a file descriptor to lock on.

→ flock -n 9 tries the lock without blocking; fails if held.

→ The lock releases automatically when the script exits.

Key idea: flock -n guards cron jobs against overlapping runs — no manual PID files.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 73 of 102


#073 Bound runtime with timeout
SCENARIO

A command occasionally hangs forever and must be capped.

CONCEPT

timeout runs a command and kills it if it exceeds a duration. It prevents a stuck


step from freezing an entire pipeline or cron job.

COMMAND / PLAYBOOK

terminal

if timeout 5 curl -s [Link] >/dev/null; then


echo ok
else
echo "timed out or failed"
fi

OUTPUT

timed out or failed

EXPLANATION

→ timeout 5 allows five seconds, then sends SIGTERM.

→ A killed command returns status 124, so the else branch runs.

→ Add -k to send SIGKILL if it ignores the first signal.

Key idea: Wrap potentially-hanging commands in timeout so they can't stall the
whole job.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 74 of 102


#074 Validate inputs before acting
SCENARIO

A destructive script must refuse bad or dangerous arguments up front.

CONCEPT

Validating arguments early — checking counts, patterns, and existence —


prevents a script from doing damage with bad input. Fail before side effects, not
after.

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

→ ${1:?} requires the argument to exist.

→ [[ -d $target ]] confirms it's a real directory.

→ An explicit guard against / stops catastrophic mistakes.

Key idea: Validate every input before any destructive action — fail before side
effects.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 75 of 102


#075 Make operations idempotent
SCENARIO

A setup script should be safe to run repeatedly without errors or duplicates.

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

→ mkdir -p succeeds whether or not the directory exists.

→ The grep ... || useradd creates the user only if missing.

→ Re-running changes nothing once the desired state is reached.

Key idea: Check-then-act (or -p flags) makes scripts safe to re-run any number of
times.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 76 of 102


#076 Log with levels and timestamps
SCENARIO

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]

log() { printf '%s [%s] %s\n' "$(date +%FT%T)" "$1" "${*:2}"; }


info() { log INFO "$@"; }
err() { log ERROR "$@" >&2; }
info "starting"; err "disk low"

OUTPUT

2025-07-13T10:40:00 [INFO] starting


2025-07-13T10:40:00 [ERROR] disk low

EXPLANATION

→ A single log function formats timestamp, level, and message.

→ info and err wrap it; err writes to stderr.

→ Consistent format makes logs easy to grep and parse later.

Key idea: Send errors to stderr and info to stdout so callers can separate them.

09 Process & Job Control

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 77 of 102


#077 Run work in the background and wait

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

started 20481, doing other things


task done

EXPLANATION

→ & runs the command in the background.

→ $! captures the PID of that last background job.

→ wait $pid blocks until that specific job finishes.

Key idea: Capture $! right after & — the next background job overwrites it.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 78 of 102


#078 Run tasks in parallel
SCENARIO

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

all downloads complete

EXPLANATION

→ Each fetch ... & starts a job immediately.

→ A bare wait blocks until every background job completes.

→ Great for independent tasks; add limits if you have hundreds.

Key idea: Launch jobs with & in a loop, then a single wait to join them all.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 79 of 102


#079 Isolate work in a subshell
SCENARIO

You need to cd and change variables temporarily without affecting the rest of the
script.

CONCEPT

Parentheses ( ) run commands in a subshell — a child copy of the environment.


Changes there (cwd, variables) don't leak back to the parent.

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

→ ( ... ) runs in a subshell with its own copy of state.

→ The cd inside doesn't change the parent's directory.

→ Use braces { ...; } instead to group without a subshell.

Key idea: ( ) isolates cd and variable changes; { } groups in the current shell.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 80 of 102


#080 Cap parallelism with xargs -P
SCENARIO

You must process 500 files concurrently but only N at a time.

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

cat [Link] | xargs -P4 -I{} curl -sfO {}

OUTPUT

# up to 4 downloads run at once until the list is done

EXPLANATION

→ -P4 runs at most four jobs simultaneously.

→ -I{} substitutes each input line into the command.

→ This bounds load, unlike launching every job with & at once.

Key idea: xargs -P gives controlled parallelism without a job-queue library.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 81 of 102


#081 Handle signals for graceful shutdown
SCENARIO

A long-running script should clean up when it receives a stop signal.

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

# Ctrl-C -> stopping...


clean exit

EXPLANATION

→ trap shutdown INT TERM catches interrupt and terminate signals.

→ The handler flips a flag so the loop exits cleanly.

→ This enables graceful shutdown rather than an abrupt kill.

Key idea: Trap INT/TERM to drain work and clean up before a worker exits.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 82 of 102


#082 Replace the shell with exec

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

→ exec hands the process over to the target command.

→ No child is forked, so signals reach the app directly.

→ Perfect for container entrypoints and setup wrappers.

Key idea: exec in an entrypoint means signals reach your app, not a lingering shell.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 83 of 102


#083 Detach a process from the terminal
SCENARIO

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

setsid ./[Link] >/var/log/[Link] 2>&1 < /dev/null &


echo "detached pid via: $!"

OUTPUT

detached pid via: 20990


# survives the session ending

EXPLANATION

→ setsid runs the command in a new session, free of the terminal.

→ Redirecting all three streams stops it tying to the tty.

→ Prefer a systemd service for supervised, restartable processes.

Key idea: setsid/nohup detach a process, but systemd is better for real services.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 84 of 102


#084 Check whether a process is running
SCENARIO

A script should act only if a given service process exists.

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

if pgrep -f 'python [Link]' >/dev/null; then


echo "worker running"
else
echo "starting worker"
fi

OUTPUT

worker running

EXPLANATION

→ pgrep -f matches against the full command line.

→ Its exit status (0 if found) drives the if cleanly.

→ This avoids the fragile ps | grep pattern.

Key idea: pgrep's exit status makes 'is it running?' checks clean — no ps parsing.

10 Debugging & Best Practices

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 85 of 102


#085 Trace execution with set -x
SCENARIO

A script misbehaves and you need to see each command as it runs.

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

→ set -x echoes each expanded command with a + prefix.

→ You see the resolved values, not the raw source — great for variable bugs.

→ set +x stops tracing to keep output clean.

Key idea: set -x shows commands after expansion — perfect for diagnosing variable
issues.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 86 of 102


#086 Check syntax without running

SCENARIO

You want to catch syntax errors before a script executes anything.

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

bash -n [Link] && echo "syntax ok"

OUTPUT

[Link]: line 12: syntax error near unexpected token `fi'


# fix it, then: syntax ok

EXPLANATION

→ -n does a no-execute syntax check.

→ It catches unclosed blocks, quotes, and typos early.

→ Run it in CI so broken scripts never merge.

Key idea: bash -n is a zero-risk syntax gate — wire it into CI or pre-commit.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 87 of 102


#087 Lint scripts with shellcheck

SCENARIO

You want to catch quoting bugs and pitfalls a syntax check misses.

CONCEPT

shellcheck statically analyses scripts and flags common mistakes (unquoted


variables, useless cats, subtle bugs) with explanations. It's the single best quality
tool for shell.

COMMAND / PLAYBOOK

terminal

shellcheck [Link]

OUTPUT

In [Link] line 8:
cp $src $dst
^-- SC2086: Double quote to prevent globbing and word splitting.

EXPLANATION

→ shellcheck reports issues with an SC#### code and fix.

→ It catches unquoted expansions, unreachable code, and more.

→ Each code links to a wiki page explaining the rationale.

Key idea: Run shellcheck on every script — it catches bugs no syntax check will.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 88 of 102


#088 Enrich traces with PS4
SCENARIO

set -x output is hard to follow in a big script with functions.

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

export PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '


set -x
greet() { echo hi; }
greet

OUTPUT

+ [Link]:greet: echo hi
hi

EXPLANATION

→ PS4 defines the trace prefix (default is + ).

→ Adding LINENO and FUNCNAME pinpoints where each command runs.

→ This turns raw -x noise into a readable execution trace.

Key idea: Set a rich PS4 with LINENO/FUNCNAME to make set -x traces navigable.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 89 of 102


#089 Toggle debug output by environment
SCENARIO

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

→ The script enables tracing only when DEBUG=1 is set.

→ Normal runs stay clean; debugging is one env var away.

→ Callers opt in without modifying the script.

Key idea: Gate set -x behind DEBUG=1 so tracing is opt-in per run.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 90 of 102


#090 Write portable POSIX sh when needed

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

→ #!/bin/sh declares POSIX shell, which may be dash.

→ Use [ ], avoid arrays and [[ ]], quote everything.

→ shellcheck with -s sh flags accidental bashisms.

Key idea: For /bin/sh portability, avoid [[ ]] and arrays — verify with shellcheck -s sh.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 91 of 102


#091 Avoid parsing ls

SCENARIO

A loop over ls output breaks on filenames with spaces or newlines.

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

# fragile splits 'app [Link]' into two


# glob keeps each filename whole

EXPLANATION

→ $(ls ...) word-splits, mangling names with spaces.

→ A glob (*.log) expands to intact filenames.

→ For recursion or NUL-safety, use find -print0 | xargs -0.

Key idea: Never loop over ls output — use globs, or find -print0 for anything
recursive.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 92 of 102


#092 Prefer builtins over external tools
SCENARIO

A hot loop spawns cat/grep/sed thousands of times and is slow.

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

# slow: base=$(echo "$path" | sed 's#.*/##')


base=${path##*/} # builtin, no process
ext=${base##*.}

OUTPUT

# same result, no external process spawned

EXPLANATION

→ ${path##*/} replaces a sed call with pure expansion.

→ Avoiding subprocesses matters most inside loops.

→ Reserve awk/sed for genuinely complex text work.

Key idea: In loops, use parameter expansion instead of echo|sed — no forks, much
faster.

11 Real-World Automation

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 93 of 102


#093 Parse options with getopts

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.

→ $OPTARG holds an option's argument.

→ A leading : enables silent error handling you control in the case.

Key idea: getopts gives scripts a conventional flag interface — use it over hand-
rolled parsing.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 94 of 102


#094 Load settings from a config file

SCENARIO

Environment-specific values should live in a config file, not the script.

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

→ source "$config" loads KEY=value lines as variables.

→ The existence check avoids an error when the file is absent.

→ ${TARGET:?...} enforces that required settings were provided.

Key idea: Source a config file for env-specific values; validate required keys after
loading.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 95 of 102


#095 Add a dry-run mode
SCENARIO

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.

→ Users flip DRY=1 to preview without side effects.

→ It centralises the safety switch for every risky command.

Key idea: Route destructive commands through a run() wrapper so a DRY flag can
preview them.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 96 of 102


#096 Deploy with an atomic symlink switch

SCENARIO

Deploys should be instant and instantly reversible with no half-updated state.

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

→ Each version lives in its own releases/$ver directory.

→ ln -sfn atomically switches current to the new release.

→ Rollback is just re-pointing the symlink at a previous version.

Key idea: Release into versioned dirs and flip a symlink — instant deploys and
rollbacks.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 97 of 102


#097 Back up with rotation

SCENARIO

A nightly backup should keep only the last seven archives.

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

→ date +%F stamps each archive with the day.

→ ls -1t lists newest first; tail -n +8 selects the 8th onward.

→ xargs -r rm deletes them (and -r skips running rm on empty input).

Key idea: List newest-first, skip the count you keep, and rm the rest for simple
rotation.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 98 of 102


#098 Send a webhook notification
SCENARIO

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

→ curl -X POST -d sends a JSON body to the webhook URL.

→ The message is interpolated into the JSON payload.

→ For complex payloads, build the JSON with jq to escape safely.

Key idea: Wrap webhook posts in a notify() function; build tricky JSON with jq to
escape it.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 99 of 102


#099 Write a safe cron wrapper

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

# single instance, full PATH, all output logged with timestamps

EXPLANATION

→ Setting PATH fixes cron's 'command not found' surprises.

→ flock -n prevents overlapping runs.

→ exec >>log 2>&1 redirects all further output to a log file.

Key idea: A cron wrapper should set PATH, lock with flock, and redirect output to a
log.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 100 of 102


#100 Poll a health endpoint until ready

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

healthy after 4 tries

EXPLANATION

→ The loop caps attempts at 30, so it can't wait forever.

→ Each failed check waits 2s before retrying.

→ Success exits 0; exhausting attempts exits non-zero to fail the deploy.

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.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 101 of 102


© EngiDock — [Link] · Practical DevOps education. Built by engineers, for engineers.

EngiDock · Shell Scripting — 100 Scenarios Explained · Page 102 of 102

You might also like