Bash Scripting

0%
25 minbashscriptingautomation

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; env finds 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 EXIT guarantees 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 mktemp for 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?

-e exits on the first failed command, -u treats unset variables as errors (catches typos), and -o pipefail makes 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 failed cd followed 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 $file where 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 should exit with 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 -r prevents 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 older test builtin with more pitfalls. Use -eq/-lt/-gt for numeric comparisons and ==/!= for strings — mixing them (e.g., == on numbers or -eq on strings) is a common bug.

Q6. How do you make sure temp files are cleaned up even if the script fails?

Create them with mktemp and register a cleanup with trap 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 >&2 so 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 — flock on a lock file (e.g., flock -n /var/lock/job.lock -c 'job') so a second run exits instead of clobbering the first. Add set -euo pipefail, trap cleanup, meaningful exit codes, and run it through shellcheck in CI.


Next: 10 — Performance & Resource Monitoring — finding what’s slow or hungry.