Skip to main content
Bash advanced Lesson 22 of 22

Bash Interview Preparation

Top 25 Bash and Shell scripting interview questions with real examples, covering everything from basics to advanced DevOps scenarios.

Q1: What does set -euo pipefail do?

Understanding set -euo pipefail is the single most important thing to know about writing robust Bash scripts. Interviewers ask this because it separates people who write production-quality scripts from those who write scripts that silently fail.

  • set -e — exit immediately if any command returns a non-zero exit code
  • set -u — treat unset variables as errors (prevents silent use of empty strings)
  • set -o pipefail — a pipeline fails if any stage fails, not just the last command
#!/usr/bin/env bash
set -euo pipefail

# Without pipefail, this pipeline exits 0 (grep succeeds even if cat fails)
# With pipefail, the pipeline fails when cat fails
cat /nonexistent | grep "pattern"   # exits non-zero with pipefail

Q2: What is the difference between "$@" and "$*"?

Both expand to all positional parameters, but their quoting behavior differs in a way that matters for arguments containing spaces.

#!/usr/bin/env bash
demo() {
  echo "Using \$@:"
  for arg in "$@"; do echo "  [$arg]"; done

  echo "Using \$*:"
  for arg in "$*"; do echo "  [$arg]"; done
}

demo "hello world" "foo" "bar baz"

# "$@" — each argument is preserved as a separate word:
#   [hello world]
#   [foo]
#   [bar baz]

# "$*" — all arguments joined as one string:
#   [hello world foo bar baz]

Always use "$@" to pass arguments through to another command while preserving argument boundaries.


Q3: How do you check if a variable is set and non-empty?

# Check if set and non-empty
if [[ -n "${MY_VAR:-}" ]]; then
  echo "MY_VAR is set and non-empty: $MY_VAR"
fi

# Check if declared at all (even if empty) — requires bash 4.2+
if [[ -v MY_VAR ]]; then
  echo "MY_VAR is declared"
fi

# Error out immediately if a required variable is missing
: "${DATABASE_URL:?DATABASE_URL must be set}"

# Provide a fallback default value
name="${1:-anonymous}"

Q4: What is the difference between [ ] and [[ ]]?

This question tests whether you understand the history of bash and know which construct to use in modern scripts.

Feature[ ][[ ]]
POSIX portableYesBash only
Word splittingYes (dangerous)No
Glob patterns in ==NoYes
Regex with =~NoYes
Logical && / || insideNoYes
# [[ ]] is safer — no word splitting on unquoted variables
file="my file.txt"
[ -f $file ]    # WRONG — word splits into two arguments
[[ -f $file ]]  # OK — no word splitting

# Pattern matching is only available in [[
[[ "$filename" == *.sh ]] && echo "shell script"

# Regex is only available in [[
[[ "$email" =~ ^[^@]+@[^@]+\.[a-z]{2,}$ ]] && echo "valid email"

Q5: What happens when you pipe to a while loop?

This is a classic gotcha that trips up experienced Bash programmers. The loop runs in a subshell, so variable changes inside it are lost when the pipe’s subshell exits.

count=0
# WRONG — count stays 0 after the loop because the loop runs in a subshell
grep "ERROR" log.txt | while IFS= read -r line; do
  (( count++ ))
done
echo "$count"   # 0 — subshell changes are lost

# CORRECT — process substitution keeps the loop in the current shell
count=0
while IFS= read -r line; do
  (( count++ ))
done < <(grep "ERROR" log.txt)
echo "$count"   # correct value

Q6: How do you redirect both stdout and stderr to a file?

Understanding file descriptor redirection order is important — the order of redirections is significant and getting it wrong is a common source of bugs.

# Method 1 — redirect stdout to file, then redirect stderr to wherever stdout now points
command > output.log 2>&1

# Method 2 — bash shorthand (equivalent)
command &> output.log

# Append both
command >> output.log 2>&1

# Discard all output
command &> /dev/null

# Merge stderr into stdout in a pipeline
command 2>&1 | grep "error"

Order matters: 2>&1 >file is wrong — it redirects stderr to the terminal (where stdout currently points), then redirects stdout to the file. Stderr still goes to the terminal.


Q7: How do you write a function that returns a value?

# Pattern 1: echo + command substitution (most common — clean for strings)
get_os() {
  uname -s | tr '[:upper:]' '[:lower:]'
}
os=$(get_os)

# Pattern 2: nameref variable (no subshell — required for arrays)
get_files() {
  local -n _result="$1"
  mapfile -t _result < <(find . -name "*.sh")
}
declare -a files
get_files files
echo "${#files[@]} shell scripts found"

# Pattern 3: use return code as a boolean
is_running() {
  pgrep -x "$1" > /dev/null
}
is_running nginx && echo "nginx is up"

Q8: What is IFS and why does IFS= read -r matter?

IFS (Internal Field Separator) controls word splitting. IFS= temporarily clears it so that leading and trailing whitespace on each line is preserved exactly. -r prevents backslash interpretation. Together they ensure you read file content with perfect fidelity.

# Without IFS= — leading spaces stripped, backslash sequences consumed
while read -r line; do
  echo "$line"
done < file.txt

# With IFS= — every character is preserved exactly as written
while IFS= read -r line; do
  echo "$line"
done < file.txt

Always use IFS= read -r when reading files line by line.


Q9: How do you trap signals and perform cleanup?

#!/usr/bin/env bash
TMPFILE=$(mktemp)

cleanup() {
  echo "Cleaning up..." >&2
  rm -f "$TMPFILE"
}

# EXIT fires on any exit — success, failure, or signal — the most reliable trap
trap cleanup EXIT

# Handle Ctrl+C with a clear message and correct exit code
trap 'echo "Interrupted"; exit 130' INT

# Handle SIGTERM (sent by kill or systemd stop)
trap 'echo "Terminated"; exit 143' TERM

# ... do work ...

Q10: What is the difference between a subshell and a child process?

A subshell (( ), $( ), pipes) is a copy of the current bash process — it inherits all variables whether exported or not. A child process (external commands like grep, curl) only inherits variables that have been explicitly exported.

x=10
export y=20

# Subshell inherits both x and y — it is a copy of the current shell
(echo "$x $y")   # 10 20

# Child process only gets y — only exported variables cross the process boundary
bash -c 'echo "$x $y"'   # " 20" — x was not exported

Q11: How do you safely handle filenames with spaces?

# Always quote variable expansions — this is the foundation
filename="my file with spaces.txt"
cp "$filename" /backup/

# Use arrays and find -print0 for lists of files
files=()
while IFS= read -r -d '' f; do
  files+=("$f")
done < <(find . -name "*.txt" -print0)   # -print0 uses NUL as separator

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

# Or use find -exec which handles spaces correctly without arrays
find . -name "*.txt" -exec cp {} /backup/ \;

Q12: How do you do arithmetic in Bash?

# Integer arithmetic — built into bash
a=10; b=3
echo $(( a + b ))    # 13
echo $(( a ** b ))   # 1000
(( a++ ))            # increment in place

# Float arithmetic with bc
result=$(echo "scale=2; 22/7" | bc)
echo "$result"   # 3.14

# Float arithmetic with awk (no pipe setup needed)
awk 'BEGIN { printf "%.4f\n", sqrt(2) }'   # 1.4142

Q13: How do you find and replace text in a file in-place?

# GNU sed (Linux) — -i modifies the file directly
sed -i 's/old_value/new_value/g' config.txt

# With a backup file
sed -i.bak 's/old_value/new_value/g' config.txt

# macOS (BSD sed) — -i requires an argument even if empty
sed -i '' 's/old_value/new_value/g' config.txt

# Portable approach — works everywhere
tmp=$(mktemp)
sed 's/old/new/g' config.txt > "$tmp"
mv "$tmp" config.txt

Q14: How do you parse a CSV file in Bash?

# Simple: use IFS with read to split each line
while IFS=, read -r name age city; do
  echo "Name: $name, Age: $age, City: $city"
done < data.csv

# Skip the header line
while IFS=, read -r name age city; do
  [[ "$name" == "Name" ]] && continue   # skip header row
  echo "$name lives in $city"
done < data.csv

# Use awk for more complex parsing or formatted output
awk -F, 'NR>1 { printf "%-10s %-5s %s\n", $1, $2, $3 }' data.csv

Q15: What does 2>&1 mean? Why does order matter?

# Correct: redirect stdout to file FIRST, then redirect stderr to stdout (now the file)
command > file.txt 2>&1

# WRONG: redirect stderr to stdout (terminal) FIRST, then redirect stdout to file
# Stderr still goes to the terminal!
command 2>&1 > file.txt

File descriptors are redirected in left-to-right order. 2>&1 means “send FD 2 to wherever FD 1 currently points.” If FD 1 has not been redirected to the file yet when 2>&1 is evaluated, FD 1 still points to the terminal.


Q16: How do you write a retry loop?

retry() {
  local max="$1"
  local delay="$2"
  shift 2

  local i=1
  until "$@"; do
    if (( i >= max )); then
      echo "Command failed after $max attempts" >&2
      return 1
    fi
    echo "Attempt $i failed. Retrying in ${delay}s..."
    sleep "$delay"
    (( i++ ))
    (( delay = delay * 2 > 60 ? 60 : delay * 2 ))  # exponential backoff, capped at 60s
  done
}

retry 5 2 curl -sf https://api.example.com/health

Q17: How do you check if a command exists?

# command -v — POSIX portable, returns the path if found
if command -v docker &>/dev/null; then
  echo "Docker is installed: $(docker --version)"
fi

# Require a dependency — exit with a clear message if it is missing
require_cmd() {
  command -v "$1" &>/dev/null || {
    echo "ERROR: required command not found: $1" >&2
    exit 1
  }
}

require_cmd aws
require_cmd jq
require_cmd docker

Q18: How does flock work and why is it useful?

flock prevents two instances of the same script from running simultaneously. It is essential for cron jobs that might overlap if they run longer than their schedule interval. The lock is held by a file descriptor and released automatically when the script exits.

# In crontab — simplest approach
*/5 * * * * flock -n /tmp/myjob.lock /usr/local/bin/myjob.sh

# In a script
exec 9>/var/run/myjob.lock
flock -n 9 || { echo "Already running"; exit 0; }

# flock -n: non-blocking — fail immediately if locked
# flock -w 30: wait up to 30 seconds for the lock
# flock -x: exclusive lock (default)
# flock -s: shared lock (allows multiple readers)

Q19: How do you process JSON in Bash?

# Use jq — the standard JSON processor for the command line
response=$(curl -sf https://api.example.com/users)

# Extract a single field
name=$(echo "$response" | jq -r '.[0].name')

# Filter an array by condition
admins=$(echo "$response" | jq -r '[.[] | select(.role == "admin")] | .[].name')

# Build JSON safely from shell variables (no manual escaping needed)
payload=$(jq -n \
  --arg name "$USERNAME" \
  --arg env  "$ENVIRONMENT" \
  '{user: $name, environment: $env, timestamp: now | todate}')

curl -sf -X POST -H "Content-Type: application/json" \
  -d "$payload" https://api.example.com/events

Q20: What is the difference between source and executing a script?

# Execute — runs in a child process; changes to variables and directory do not affect the parent
./myscript.sh
bash myscript.sh

# Source — runs in the current shell; all changes persist in the calling shell
source myscript.sh
. myscript.sh          # POSIX equivalent

# Practical difference:
# myscript.sh contains: export DB_HOST="prod-db"

./myscript.sh
echo "$DB_HOST"   # empty — the child's export does not reach the parent

source myscript.sh
echo "$DB_HOST"   # "prod-db" — runs in the current shell

Q21: How do you debug a Bash script?

# Method 1: run with -x to trace every command as it executes
bash -x script.sh

# Method 2: enable tracing for a section of the script
set -x   # enable command tracing
# ... code to debug ...
set +x   # disable tracing

# Method 3: verbose mode — print each line before executing it
bash -v script.sh

# Method 4: print variable state explicitly
declare -p my_array   # shows type, flags, and current value
echo "DEBUG: user=$user, file=$file" >&2

# Method 5: shellcheck for static analysis before running
shellcheck script.sh

# Useful: trace only a specific function
my_function() {
  set -x
  # ... function body ...
  set +x
}

Q22: What is the difference between &&, ||, and ;?

# && — run second command only if first succeeds (exit 0)
mkdir /tmp/work && cd /tmp/work

# || — run second command only if first fails (exit non-zero)
[[ -d /tmp/work ]] || mkdir /tmp/work

# ; — run second command regardless of first's exit code
echo "step 1"; echo "step 2"   # always runs both

# Combining: create dir and cd, or abort
mkdir /tmp/work && cd /tmp/work || exit 1

Q23: How do you write a script that works correctly when sourced and when executed?

#!/usr/bin/env bash
# This script can be sourced (to load its functions) or executed directly.

greet() {
  echo "Hello, $1!"
}

setup_env() {
  export APP_ENV="${1:-development}"
}

# BASH_SOURCE[0] is the script's own path; $0 is the name it was invoked as.
# They are equal when executed directly; they differ when sourced.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  # Running as a script — execute main logic
  greet "${1:-World}"
  setup_env "${2:-development}"
fi

# When sourced, only the functions are loaded — no side effects:
# source ./script.sh
# greet "Alice"   # call the function directly

Q24: How do you handle configuration from multiple sources?

#!/usr/bin/env bash
# Priority (highest to lowest): environment > user config > system config > defaults

# 1. Hardcoded defaults
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="myapp"
LOG_LEVEL="INFO"

# 2. System config file
CONFIG_FILE="${CONFIG_FILE:-/etc/myapp/config.sh}"
if [[ -f "$CONFIG_FILE" ]]; then
  # shellcheck source=/dev/null
  source "$CONFIG_FILE"
fi

# 3. Environment variables override the config file
DB_HOST="${MYAPP_DB_HOST:-$DB_HOST}"
DB_PORT="${MYAPP_DB_PORT:-$DB_PORT}"
DB_NAME="${MYAPP_DB_NAME:-$DB_NAME}"
LOG_LEVEL="${MYAPP_LOG_LEVEL:-$LOG_LEVEL}"

# 4. Validate that required values are present
: "${DB_HOST:?DB_HOST must be set}"
: "${DB_PORT:?DB_PORT must be set}"

Q25: Write a script that monitors disk usage and sends an alert.

This question tests whether you can write a complete, production-quality script — not just individual commands.

#!/usr/bin/env bash
set -euo pipefail

THRESHOLD="${DISK_ALERT_THRESHOLD:-85}"
ALERT_EMAIL="${ALERT_EMAIL:-ops@example.com}"
HOSTNAME=$(hostname)

check_disk() {
  local mount="$1"
  local usage
  # awk strips the % sign and prints the usage column
  usage=$(df -h "$mount" | awk 'NR==2 {gsub(/%/,""); print $5}')

  if (( usage >= THRESHOLD )); then
    echo "ALERT: ${mount} is ${usage}% full on ${HOSTNAME}"
    return 1
  fi
  return 0
}

alerts=()

# Check every mounted filesystem
while IFS= read -r mount; do
  if ! check_disk "$mount" 2>/dev/null; then
    usage=$(df -h "$mount" | awk 'NR==2 {gsub(/%/,""); print $5}')
    alerts+=("  $mount: ${usage}%")
  fi
done < <(df -h | awk 'NR>1 {print $6}')

if (( ${#alerts[@]} > 0 )); then
  body="Disk usage alert on $HOSTNAME:
${alerts[*]}
Threshold: ${THRESHOLD}%
Time: $(date)"

  echo "$body" | mail -s "DISK ALERT: $HOSTNAME" "$ALERT_EMAIL"
  echo "$body" >&2
  exit 1
fi

echo "All disks OK (threshold: ${THRESHOLD}%)"

Quick Reference Cheat Sheet

A condensed reference for the most common patterns — useful for reviewing before an interview.

# Variables
name="value"            # assign (no spaces around =)
echo "${name}"          # read
echo "${name:-default}" # with fallback default
unset name              # delete

# Conditionals
[[ -f "$f" ]]           # is a regular file
[[ -d "$d" ]]           # is a directory
[[ -z "$s" ]]           # string is empty
[[ -n "$s" ]]           # string is non-empty
[[ $a -eq $b ]]         # numeric equal
[[ "$a" == "$b" ]]      # string equal
[[ "$s" =~ regex ]]     # regex match

# Loops
for item in "${array[@]}"; do ...; done
while IFS= read -r line; do ...; done < file
for (( i=0; i<10; i++ )); do ...; done

# Functions
myfunc() { local x="$1"; echo "$x"; }
result=$(myfunc "arg")

# Error handling
set -euo pipefail
trap 'cleanup' EXIT
die() { echo "ERROR: $*" >&2; exit 1; }

Frequently Asked Questions

What level of Bash knowledge do DevOps interviews expect?
Most DevOps and SRE interviews expect solid knowledge of variables, quoting, pipelines, redirection, error handling, and common tools like grep/awk/sed. Senior roles add process management, signal handling, and security awareness.
Should I memorize exact syntax or understand concepts?
Both matter. Interviewers want to see you can write working code, but they also probe whether you understand why something works — like why pipelines create subshells, or why set -e sometimes surprises people.
Are Bash questions common in FAANG interviews?
Not in algorithm rounds, but in system design, SRE, and DevOps-focused roles they are common. Expect practical scripting tasks, debugging broken scripts, and questions about Unix fundamentals.