Bash Scripting
Bash Scripting
Learn bash scripting concepts for DevOps interviews
09 — Bash Scripting
DevOps runs on Bash. Interviewers ask you to read or write a script, and they judge whether it’s safe (strict mode, quoting, error handling) or a foot-gun. This file makes your scripts production-grade and your answers fluent.
1. Anatomy & strict mode (start every script with this)
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
#!/usr/bin/env bash— the shebang;envfinds bash in PATH (portable).set -e— exit immediately if any command fails (non-zero exit).set -u— error on unset variables (catches typos like$fielname).set -o pipefail— a pipeline fails if any stage fails, not just the last.IFS=$'\n\t'— safer word-splitting (spaces in filenames won’t break loops).
🎯 Interview signal: opening with set -euo pipefail immediately marks you as someone who
writes safe scripts. Explain why: without -e, a script keeps running after a failed
command (e.g., cd /nonexistent; rm -rf * — catastrophic); without pipefail, false | true
“succeeds.”
⚠️ set -e has gotchas: it doesn’t trigger inside if/&&/|| conditions, and some commands
“fail” legitimately (e.g., grep returns 1 on no match). Handle those explicitly.
2. Variables & quoting (the #1 bug source)
name="arpit" # NO spaces around =
echo "$name" # ALWAYS quote expansions
path="/data/$name/logs"
readonly MAX=5 # constant
local x=1 # inside a function only
count=$(ls | wc -l) # command substitution
echo "${name:-default}" # default if unset/empty
echo "${name:?must be set}" # error out if unset
echo "${#name}" # length
echo "${name^^}" "${name,,}" # upper / lower
⚠️ Always double-quote "$var" and "$(cmd)". Unquoted variables undergo word-splitting
and globbing:
file="my file.txt"
rm $file # ❌ tries to rm 'my' and 'file.txt' (two args!)
rm "$file" # ✅ correct
🎯 “Why quote variables?” → to prevent word splitting and glob expansion. This single habit prevents a huge class of bugs, and interviewers specifically look for it in code you write.
3. Exit codes ($? and their meaning)
Every command returns an exit status: 0 = success, non-zero = failure.
some_command
echo $? # exit code of the last command
command && echo "ok" # run second only if first SUCCEEDED (exit 0)
command || echo "fail" # run second only if first FAILED (non-zero)
command1 && command2 || echo "handling failure"
exit 0 # your script's own exit code — set it deliberately
🎯 Interview signal: “Exit codes are how scripts and CI pipelines know success vs failure —
0 is success, non-zero is failure, and $? reads the last one. CI treats a non-zero exit as
a failed step.” Conventions: 1 general error, 2 misuse, 126 not executable, 127 not
found, 130 Ctrl-C (128+SIGINT).
4. Conditionals
if [[ -f "$file" ]]; then
echo "file exists"
elif [[ -d "$path" ]]; then
echo "it's a directory"
else
echo "neither"
fi
Common [[ ]] tests:
| Test | True when |
|---|---|
-f file |
regular file exists |
-d dir |
directory exists |
-e path |
path exists (any type) |
-r/-w/-x |
readable/writable/executable |
-z "$s" |
string is empty |
-n "$s" |
string is non-empty |
"$a" == "$b" |
strings equal |
"$a" != "$b" |
strings not equal |
$a -eq/-ne/-lt/-gt/-le/-ge $b |
numeric comparisons |
🎯 Prefer [[ ]] (bash builtin) over [ ] (test): it’s safer with unquoted vars,
supports &&/||/== pattern matching and =~ regex. ⚠️ Use -eq for numbers, == for
strings — mixing them is a classic bug.
5. Loops
for f in *.log; do # glob loop
echo "processing $f"
done
for i in {1..5}; do echo "$i"; done # range
for ((i=0; i<5; i++)); do echo "$i"; done # C-style
while read -r line; do # read a file line by line (-r = don't mangle backslashes)
echo "$line"
done < input.txt
while true; do check_health || break; sleep 5; done
⚠️ Never for line in $(cat file) — it word-splits on spaces, not lines. Use
while read -r line; do ... done < file. This is a frequent interview correction.
6. Functions, arguments, and input
usage() { echo "usage: $0 <env> <count>"; exit 2; }
deploy() {
local env="$1" count="$2" # positional args; always 'local'
[[ -n "$env" ]] || { echo "env required" >&2; return 1; }
echo "deploying $count to $env"
}
# Script arguments:
# $0 script name, $1..$9 positional, $# count, $@ all (quoted individually), $* all as one
[[ $# -eq 2 ]] || usage
deploy "$1" "$2"
🎯 Use "$@" (not $*) to pass all arguments through preserving individual quoting.
7. Error handling & traps
set -euo pipefail
cleanup() { rm -f "$tmpfile"; echo "cleaned up"; }
trap cleanup EXIT # run cleanup on ANY exit (success, error, or signal)
trap 'echo "error on line $LINENO"' ERR
tmpfile="$(mktemp)" # safe temp file
command_that_might_fail || { echo "failed, aborting" >&2; exit 1; }
trap cleanup EXITguarantees cleanup runs even if the script errors or is interrupted — the right way to remove temp files / release locks.- Send errors to stderr (
>&2) so they’re separable from normal output. - Use
mktempfor temp files (avoids predictable-name races/collisions).
🎯 Interview signal: “I use trap ... EXIT so temp files and locks are cleaned up no matter
how the script ends.” That’s a senior scripting habit.
8. A production-quality mini-script (the template)
#!/usr/bin/env bash
set -euo pipefail
readonly LOG="/var/log/backup.log"
log() { echo "$(date '+%F %T') $*" | tee -a "$LOG"; }
backup() {
local src="$1" dest="$2"
[[ -d "$src" ]] || { log "ERROR: src '$src' missing"; return 1; }
mkdir -p "$dest"
tar czf "$dest/backup-$(date +%F).tar.gz" -C "$src" . \
&& log "backup of $src OK" \
|| { log "ERROR: backup failed"; return 1; }
}
main() {
[[ $# -eq 2 ]] || { echo "usage: $0 <src> <dest>" >&2; exit 2; }
backup "$1" "$2"
}
main "$@"
⚠️ Run scripts through shellcheck (a static analyzer) — it catches quoting, unset-var,
and portability bugs. Mentioning shellcheck signals maturity.
Interview Questions
Q1. What does set -euo pipefail do and why use it?
-eexits on the first failed command,-utreats unset variables as errors (catches typos), and-o pipefailmakes a pipeline fail if any stage fails, not just the last. Together they turn silent failures into loud ones, so a script doesn’t blunder past an error (e.g., a failedcdfollowed by a destructive command). It’s the baseline for safe scripts.
Q2. Why must you quote variables in bash?
Unquoted expansions undergo word splitting and glob expansion.
rm $filewhere file is “my file.txt” tries to remove two things;rm "$file"is correct. Quoting"$var"and"$(cmd)"prevents a whole class of bugs with spaces, empty values, and wildcards.
Q3. How do scripts and CI know whether a command succeeded?
Exit codes: 0 is success, non-zero is failure, and
$?holds the last command’s code.&&runs the next command only on success,||only on failure. CI systems treat a non-zero exit from a step as a failed build, so scripts shouldexitwith meaningful codes.
Q4. What’s wrong with for line in $(cat file)?
It splits on the IFS (spaces/tabs/newlines), not on lines, so any line with spaces breaks into multiple iterations and globs may expand. The correct idiom is
while read -r line; do ...; done < file, which reads one line at a time and-rprevents backslash mangling.
Q5. [[ ]] vs [ ], and -eq vs ==?
[[ ]]is a bash keyword that’s safer with unquoted variables and supports&&,||, pattern matching, and=~regex;[ ]is the oldertestbuiltin with more pitfalls. Use-eq/-lt/-gtfor numeric comparisons and==/!=for strings — mixing them (e.g.,==on numbers or-eqon strings) is a common bug.
Q6. How do you make sure temp files are cleaned up even if the script fails?
Create them with
mktempand register a cleanup withtrap cleanup EXIT, which runs on any exit path — normal, error, or signal. That guarantees temp files are removed and locks released regardless of how the script terminates.
Q7. How do you write error messages and pass all arguments to a function?
Send errors to stderr with
>&2so they’re separable from stdout, and return non-zero. Pass arguments through with"$@"(quoted) so each argument keeps its own quoting — unlike$*, which joins them into one string.
Q8. (Senior) How do you make a script safe to run repeatedly and only once at a time?
Idempotency: make operations safe to re-run (
mkdir -p, check-before-create, converge to a desired state rather than assuming a starting state). For single-instance execution use a lock —flockon a lock file (e.g.,flock -n /var/lock/job.lock -c 'job') so a second run exits instead of clobbering the first. Addset -euo pipefail,trapcleanup, meaningful exit codes, and run it throughshellcheckin CI.
Next: 10 — Performance & Resource Monitoring — finding what’s slow or hungry.