Bash interviews test your shell scripting fluency, Linux command knowledge, process management, text processing, and ability to automate real-world tasks. This guide covers the 50 most common Bash interview questions with concise answers and working code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Basics | shebang, variables, quoting, special variables |
| Control flow | if/elif/else, case, for, while, until |
| Functions | definition, arguments, return values, local scope |
| Arrays | indexed, associative, iteration, manipulation |
| Strings | length, substring, search/replace, split |
| File I/O | read, redirect, pipes, heredoc, process substitution |
| Process control | background jobs, wait, trap, exit codes |
| Text processing | grep, sed, awk, cut, sort, uniq |
| Scripting patterns | argument parsing, logging, error handling, locking |
| Advanced | eval, source, subshells, parameter expansion |
Basics
1. What is a shebang line and why does it matter?
The first line of a script that tells the OS which interpreter to use:
#!/usr/bin/env bash
Using /usr/bin/env bash instead of /bin/bash finds bash in $PATH, making the script portable across different systems (macOS, Linux distros) where bash may live in different locations.
Without a shebang the OS uses the current shell to run the script, which may not be bash — causing unexpected behaviour if you rely on bash-specific features.
2. What is the difference between $* and $@?
Both expand to all positional parameters, but quoting changes the behaviour:
function demo() {
echo "Using \"\$*\":"
for arg in "$*"; do echo " [$arg]"; done # one string
echo "Using \"\$@\":"
for arg in "$@"; do echo " [$arg]"; done # individual args
}
demo "hello world" "foo bar"
# "$*" → [hello world foo bar] (joined by IFS, one item)
# "$@" → [hello world] [foo bar] (two separate items)
Rule: Always use "$@" when forwarding arguments to preserve word splitting.
3. What is the difference between single quotes and double quotes in Bash?
| Quote | Variable expansion | Command substitution | Escape sequences |
|---|---|---|---|
'text' |
No | No | No |
"text" |
Yes ($VAR) |
Yes ($(cmd)) |
Partial (\n, \\, \") |
| No quotes | Yes | Yes | Yes (+ globbing, word splitting) |
VAR="world"
echo 'Hello $VAR' # → Hello $VAR
echo "Hello $VAR" # → Hello world
echo Hello $VAR # → Hello world (+ risk of globbing/splitting)
4. What are special variables $?, $$, $!, $0?
| Variable | Meaning |
|---|---|
$? |
Exit code of last command (0 = success) |
$$ |
PID of current shell |
$! |
PID of last background process |
$0 |
Name/path of the script |
$# |
Number of positional parameters |
$_ |
Last argument of previous command |
ls /nonexistent 2>/dev/null
echo "Exit: $?" # → Exit: 2
sleep 5 &
echo "BG PID: $!"
5. What is the difference between = and == in Bash?
Inside [[ ]] both work for string comparison:
[[ "$a" = "$b" ]] # POSIX string equality
[[ "$a" == "$b" ]] # bash string equality (same result)
[[ "$a" == foo* ]] # glob pattern match (right side unquoted)
[[ "$a" =~ ^foo.* ]] # extended regex match
Inside (( )) use == for numeric equality:
(( a == 5 )) # numeric
(( a = 5 )) # assignment — do not use for comparison!
6. What is the difference between [ ], [[ ]], and (( ))?
| Construct | Purpose | Notes |
|---|---|---|
[ ] |
POSIX test | External command, limited operators |
[[ ]] |
Bash extended test | Built-in, supports &&, ||, =~, glob |
(( )) |
Arithmetic evaluation | C-style operators, no $ needed for vars |
a=5
[ "$a" -gt 3 ] && echo "yes" # POSIX
[[ $a -gt 3 ]] && echo "yes" # bash (no quotes needed for integers)
(( a > 3 )) && echo "yes" # arithmetic
7. Explain variable scoping in Bash.
By default all variables are global in Bash. Use local inside functions to limit scope:
GLOBAL="global"
my_func() {
local LOCAL="local"
GLOBAL="modified"
echo "$LOCAL" # visible
}
my_func
echo "$GLOBAL" # modified
echo "$LOCAL" # empty — out of scope
8. What does set -euo pipefail do?
#!/usr/bin/env bash
set -euo pipefail
| Option | Meaning |
|---|---|
-e |
Exit immediately if any command fails (non-zero exit) |
-u |
Treat unset variables as errors |
-o pipefail |
Pipe fails if any command in pipeline fails (not just last) |
Without pipefail: false | true exits 0. With pipefail: exits 1.
This is the recommended header for production scripts.
Control Flow
9. Write an if/elif/else with multiple conditions.
#!/usr/bin/env bash
score=75
if (( score >= 90 )); then
echo "Grade: A"
elif (( score >= 80 )); then
echo "Grade: B"
elif (( score >= 70 )); then
echo "Grade: C"
else
echo "Grade: F"
fi
# Compound conditions
if [[ -f "$file" && -r "$file" ]]; then
echo "File exists and is readable"
fi
10. What is a case statement? Give an example.
read -rp "Enter OS: " os
case "$os" in
ubuntu|debian)
echo "apt-based"
;;
centos|rhel|fedora)
echo "dnf/yum-based"
;;
arch*)
echo "pacman-based"
;;
*)
echo "Unknown OS"
;;
esac
case is cleaner than long if-elif chains when matching a single value against multiple patterns.
11. What is the difference between for, while, and until?
# for — iterate over list or range
for i in {1..5}; do echo "$i"; done
for file in *.log; do rm "$file"; done
# C-style for
for (( i=0; i<5; i++ )); do echo "$i"; done
# while — loop while condition is true
count=0
while (( count < 5 )); do
echo "$count"
(( count++ ))
done
# while read — process file line by line
while IFS= read -r line; do
echo "Line: $line"
done < /etc/passwd
# until — loop until condition becomes true
until ping -c1 google.com &>/dev/null; do
echo "Waiting for network..."
sleep 2
done
12. How do break and continue work in loops?
for i in {1..10}; do
(( i % 2 == 0 )) && continue # skip even
(( i > 7 )) && break # stop after 7
echo "$i"
done
# Output: 1 3 5 7
break N exits N levels of nested loops. continue N continues the Nth enclosing loop.
Functions
13. How do you define and call a function in Bash?
# Two valid syntaxes:
greet() {
local name="${1:-World}"
echo "Hello, $name!"
}
function greet2 {
echo "Hello, ${1:-World}!"
}
greet "Alice" # → Hello, Alice!
greet # → Hello, World!
14. How do functions return values in Bash?
Functions can only return an exit code (0–255). To return a string, use command substitution:
get_timestamp() {
date +"%Y-%m-%d %H:%M:%S"
}
ts=$(get_timestamp)
echo "Time: $ts"
# Return codes for error handling:
divide() {
if (( $2 == 0 )); then
echo "Error: division by zero" >&2
return 1
fi
echo $(( $1 / $2 ))
}
result=$(divide 10 2) && echo "Result: $result"
divide 5 0 || echo "Division failed"
15. What is the difference between return and exit in a function?
| Command | Scope | Use |
|---|---|---|
return [N] |
Function only | Return exit status from function |
exit [N] |
Entire script | Terminate the script with exit code N |
Calling exit inside a function terminates the whole script — often a bug. Use return inside functions.
Arrays
16. How do you create and use arrays in Bash?
# Indexed array
fruits=("apple" "banana" "cherry")
fruits+=( "date") # append
echo "${fruits[0]}" # → apple
echo "${fruits[@]}" # → all elements
echo "${#fruits[@]}" # → 4 (length)
echo "${fruits[@]:1:2}" # → banana cherry (slice)
unset fruits[1] # remove element
# Iterate
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Array from command output
files=( $(ls *.sh 2>/dev/null) ) # word splitting — ok for filenames without spaces
mapfile -t lines < /etc/passwd # safer: read file into array
17. What is an associative array and how do you use one?
declare -A config # must declare explicitly
config[host]="localhost"
config[port]="5432"
config[db]="mydb"
echo "${config[host]}" # → localhost
echo "${!config[@]}" # → keys: host port db
echo "${config[@]}" # → values
# Iterate key-value
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# Check key exists
[[ -v config[host] ]] && echo "host key exists"
Associative arrays require Bash 4.0+.
String Manipulation
18. How do you get the length of a string?
str="hello world"
echo "${#str}" # → 11
# Also works for array length
arr=(a b c)
echo "${#arr[@]}" # → 3
19. How do you extract a substring?
str="Hello, World!"
echo "${str:7}" # → World! (from index 7)
echo "${str:7:5}" # → World (5 chars from index 7)
echo "${str: -6}" # → orld! (from end, note space before -)
20. How do you replace text in a string using parameter expansion?
path="/home/user/documents/file.txt"
# Replace first occurrence
echo "${path/user/alice}" # → /home/alice/documents/file.txt
# Replace all occurrences
text="foo foo foo"
echo "${text//foo/bar}" # → bar bar bar
# Remove prefix
echo "${path#/home/}" # → user/documents/file.txt (shortest match)
echo "${path##*/}" # → file.txt (longest prefix match — basename)
# Remove suffix
echo "${path%.*}" # → /home/user/documents/file (remove extension)
echo "${path%%/*}" # → empty (longest suffix match)
# Case conversion (Bash 4+)
lower="hello world"
echo "${lower^^}" # → HELLO WORLD
echo "${lower^}" # → Hello world (first char only)
upper="HELLO"
echo "${upper,,}" # → hello
21. How do you split a string by a delimiter?
csv="one,two,three,four"
# Method 1: IFS splitting
IFS=',' read -ra parts <<< "$csv"
echo "${parts[0]}" # → one
echo "${parts[@]}" # → one two three four
# Method 2: parameter expansion (remove prefix/suffix)
# Method 3: tr + read
echo "$csv" | tr ',' '\n'
22. Useful string comparison operators
[[ -z "$str" ]] # true if empty string
[[ -n "$str" ]] # true if non-empty
[[ "$a" == "$b" ]] # string equality
[[ "$a" != "$b" ]] # string inequality
[[ "$a" < "$b" ]] # lexicographic less-than
[[ "$a" > "$b" ]] # lexicographic greater-than
[[ "$str" == *"sub"* ]] # contains substring
[[ "$str" =~ ^[0-9]+$ ]] # regex match (integer check)
File I/O and Redirections
23. Explain the different types of I/O redirection.
cmd > file # stdout to file (overwrite)
cmd >> file # stdout to file (append)
cmd < file # stdin from file
cmd 2> file # stderr to file
cmd 2>&1 # stderr to stdout
cmd &> file # stdout and stderr to file (bash shorthand)
cmd 2>/dev/null # discard stderr
cmd | other # pipe stdout to stdin of other
24. What is a heredoc and process substitution?
# Heredoc — multi-line string as stdin
cat << 'EOF'
This $VAR is not expanded (single-quoted delimiter)
EOF
DEST="/tmp/config.yaml"
cat > "$DEST" << EOF
host: ${HOST:-localhost}
port: ${PORT:-8080}
EOF
# Process substitution — treat command output as file
diff <(sort file1.txt) <(sort file2.txt)
while IFS= read -r line; do
echo ">> $line"
done < <(find . -name "*.sh")
25. How do you read a file line by line safely?
while IFS= read -r line; do
echo "Line: $line"
done < /etc/passwd
# IFS= — preserves leading/trailing whitespace
# -r — disables backslash interpretation
# Handle last line without newline:
while IFS= read -r line || [[ -n "$line" ]]; do
process "$line"
done < file.txt
26. How do you check if a file or directory exists?
[[ -f "$path" ]] # regular file exists
[[ -d "$path" ]] # directory exists
[[ -e "$path" ]] # file or dir exists (any type)
[[ -s "$path" ]] # file exists and is non-empty
[[ -r "$path" ]] # readable
[[ -w "$path" ]] # writable
[[ -x "$path" ]] # executable
[[ -L "$path" ]] # symbolic link
if [[ ! -f "$config" ]]; then
echo "Config not found: $config" >&2
exit 1
fi
Process Control
27. How do you run processes in background and wait for them?
# Background with &
sleep 10 &
pid=$!
echo "Started PID $pid"
# Wait for specific PID
wait "$pid"
echo "Done, exit: $?"
# Parallel execution with wait
for host in server1 server2 server3; do
ssh "$host" "df -h" > "/tmp/${host}.txt" &
done
wait # wait for all background jobs
# jobs — list background jobs in current shell
jobs
fg 1 # bring job 1 to foreground
bg 1 # resume stopped job 1 in background
28. What is trap and how do you use it?
trap runs commands when the script receives a signal or exits:
#!/usr/bin/env bash
TMPFILE=$(mktemp)
cleanup() {
echo "Cleaning up..."
rm -f "$TMPFILE"
}
trap cleanup EXIT # always runs on exit
trap 'cleanup; exit 1' INT TERM # on Ctrl+C or kill
# Trap ERR — runs on any error (use with set -e)
trap 'echo "Error on line $LINENO"' ERR
# Ignore SIGHUP (keep running after terminal closes)
trap '' HUP
29. What are exit codes and how do you use them?
# Convention:
# 0 = success
# 1 = general error
# 2 = misuse of shell built-in
# 126 = command found but not executable
# 127 = command not found
# 130 = script terminated by Ctrl+C (128 + SIGINT 2)
command_that_might_fail || {
echo "Failed with exit code: $?" >&2
exit 1
}
# Check multiple exit codes
grep "pattern" file.txt
case $? in
0) echo "Found" ;;
1) echo "Not found" ;;
2) echo "Error" ;;
esac
30. What is the difference between source (.) and executing a script?
| Method | New subshell | Variables visible after | Use case |
|---|---|---|---|
./script.sh or bash script.sh |
Yes | No | Run isolated script |
source script.sh or . script.sh |
No | Yes | Load functions/vars into current shell |
# config.sh
DB_HOST="localhost"
DB_PORT=5432
# In main script:
source ./config.sh
echo "$DB_HOST" # → localhost
Text Processing
31. How do you use grep in scripts?
# Check if pattern exists (silent, use exit code)
if grep -q "error" logfile.txt; then
echo "Errors found"
fi
# Extract matching lines
grep -E "^ERROR|^WARN" app.log
# Count occurrences
grep -c "timeout" app.log
# Show context (3 lines before/after)
grep -B3 -A3 "FATAL" app.log
# Recursive, show filename and line number
grep -rn "TODO" ./src/
# Invert match (non-matching lines)
grep -v "^#" config.txt # remove comment lines
32. How do you use sed for text transformation?
# Substitute (s command)
sed 's/old/new/' file.txt # first occurrence per line
sed 's/old/new/g' file.txt # all occurrences
sed 's/old/new/gi' file.txt # case-insensitive, all
# In-place edit (backup to .bak)
sed -i.bak 's/localhost/db.prod/g' config.txt
# Delete lines matching pattern
sed '/^#/d' config.txt # delete comment lines
sed '/^$/d' config.txt # delete empty lines
# Print specific lines
sed -n '5,10p' file.txt # lines 5-10
sed -n '/START/,/END/p' file.txt # between patterns
# Insert/append
sed '3i\inserted line' file.txt # insert before line 3
sed '3a\appended line' file.txt # append after line 3
33. How do you use awk for data extraction?
# Print specific columns
awk '{print $1, $3}' data.txt # columns 1 and 3
awk -F: '{print $1}' /etc/passwd # colon-separated, print username
# Conditional printing
awk '$3 > 100 {print $0}' data.txt # lines where col 3 > 100
awk 'NR==1 || $5=="ERROR" {print}' log.txt # header + error lines
# Sum a column
awk '{sum += $2} END {print "Total:", sum}' data.txt
# Count occurrences
awk '{count[$1]++} END {for (k in count) print k, count[k]}' file.txt
# Multi-file with filename
awk 'FNR==1 {print "==> " FILENAME}' *.txt
34. How do you sort and deduplicate output?
# Sort alphabetically
sort file.txt
# Sort numerically (-n), reverse (-r), unique (-u)
sort -rn numbers.txt
sort -u file.txt # unique lines (sorted)
# Sort by field
sort -t: -k3 -n /etc/passwd # sort by UID (field 3, : delimiter)
# Deduplicate (must be sorted first)
sort file.txt | uniq
sort file.txt | uniq -c | sort -rn # frequency count, most common first
sort file.txt | uniq -d # only duplicate lines
sort file.txt | uniq -u # only unique lines
# Remove duplicates without sorting (preserve order)
awk '!seen[$0]++' file.txt
Scripting Patterns
35. How do you parse command-line arguments?
#!/usr/bin/env bash
usage() {
cat << EOF
Usage: $(basename "$0") [OPTIONS] <input>
Options:
-o, --output FILE Output file (default: output.txt)
-v, --verbose Enable verbose mode
-h, --help Show this help
EOF
exit "${1:-0}"
}
OUTPUT="output.txt"
VERBOSE=false
# Manual parsing
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output) OUTPUT="$2"; shift 2 ;;
-v|--verbose) VERBOSE=true; shift ;;
-h|--help) usage 0 ;;
--) shift; break ;;
-*) echo "Unknown option: $1" >&2; usage 1 ;;
*) INPUT="$1"; shift ;;
esac
done
[[ -z "${INPUT:-}" ]] && { echo "Error: input required" >&2; usage 1; }
$VERBOSE && echo "Input: $INPUT, Output: $OUTPUT"
36. How do you implement logging in a Bash script?
#!/usr/bin/env bash
LOGFILE="/var/log/myscript.log"
log() {
local level="$1"; shift
printf "[%s] [%-5s] %s\n" "$(date +"%Y-%m-%d %H:%M:%S")" "$level" "$*" | tee -a "$LOGFILE"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@" >&2; }
log_error() { log "ERROR" "$@" >&2; }
log_info "Script started"
log_warn "Config file missing, using defaults"
log_error "Cannot connect to database"
37. How do you create a lockfile to prevent concurrent execution?
LOCKFILE="/tmp/myscript.lock"
# Method 1: mkdir (atomic on most filesystems)
if ! mkdir "$LOCKFILE" 2>/dev/null; then
echo "Already running (lock: $LOCKFILE)" >&2
exit 1
fi
trap 'rmdir "$LOCKFILE"' EXIT
# Method 2: flock (preferred — handles stale locks)
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "Already running" >&2
exit 1
fi
# Lock released automatically on script exit
38. How do you handle errors robustly in a script?
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t' # safer IFS (no word splitting on spaces)
die() {
echo "[ERROR] $*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
require_cmd jq
require_cmd curl
# Check required env vars
: "${DB_HOST:?DB_HOST is required}"
: "${API_KEY:?API_KEY is required}"
# Retry logic
retry() {
local max=$1; shift
local delay=$1; shift
local attempt=1
until "$@"; do
(( attempt++ > max )) && return 1
echo "Attempt $attempt/$max failed, retrying in ${delay}s..." >&2
sleep "$delay"
done
}
retry 3 5 curl -sf "https://api.example.com/health"
39. How do you write a script that sends an email or Slack notification?
# Email via sendmail/mail
send_alert() {
local subject="$1"
local body="$2"
echo "$body" | mail -s "$subject" ops@example.com
}
# Slack webhook
slack_notify() {
local message="$1"
local webhook="${SLACK_WEBHOOK:?SLACK_WEBHOOK not set}"
curl -sf -X POST "$webhook" \
-H "Content-Type: application/json" \
-d "{\"text\": \"$message\"}" >/dev/null
}
# Usage
slack_notify ":white_check_mark: Backup completed successfully"
slack_notify ":x: Deployment failed on $(hostname)"
Advanced Bash
40. What is parameter expansion and what are useful forms?
VAR="hello world"
# Default values
echo "${VAR:-default}" # use default if VAR unset or empty
echo "${VAR:=default}" # assign default if VAR unset or empty
echo "${VAR:?error msg}" # error and exit if unset or empty
echo "${VAR:+alternate}" # use alternate if VAR is set
# String length
echo "${#VAR}" # → 11
# Substring
echo "${VAR:6}" # → world
echo "${VAR:6:3}" # → wor
# Pattern removal
FILE="/home/user/file.tar.gz"
echo "${FILE##*/}" # → file.tar.gz (basename)
echo "${FILE%/*}" # → /home/user (dirname)
echo "${FILE##*.}" # → gz (extension)
echo "${FILE%.*}" # → /home/user/file.tar (remove last ext)
echo "${FILE%%.*}" # → /home/user/file (remove all ext)
# Replacement
echo "${VAR/world/bash}" # → hello bash
echo "${VAR// /_}" # → hello_world (all spaces to underscore)
41. What is the difference between a subshell and a child process?
# Subshell ( ) — runs in subshell, variable changes don't propagate
(
cd /tmp
VAR="local"
echo "$VAR" # → local
)
echo "$VAR" # → (original value — subshell change lost)
# Curly brace group { } — runs in SAME shell
{
VAR="changed"
echo "$VAR"
}
echo "$VAR" # → changed
# Pipe creates subshell — common gotcha
count=0
cat file.txt | while read -r line; do
(( count++ )) # count change lost — runs in subshell!
done
echo "$count" # → 0 (!) — use process substitution instead
# Fix with process substitution:
while read -r line; do
(( count++ ))
done < <(cat file.txt)
echo "$count" # → correct
42. How does eval work and when should you avoid it?
# eval re-evaluates a string as a command
VAR="echo hello"
eval "$VAR" # → hello
# Useful for dynamic variable names (prefer nameref in bash 4.3+)
PREFIX="DB"
eval "${PREFIX}_HOST=localhost"
echo "$DB_HOST" # → localhost
# Safer alternative: declare -n (nameref)
declare -n ref="${PREFIX}_HOST"
ref="localhost"
echo "$DB_HOST" # → localhost
Avoid eval with untrusted input — it executes arbitrary code. Prefer declare -n or associative arrays.
43. How do you write a function that modifies a caller variable (by reference)?
# Bash 4.3+: declare -n nameref
trim() {
declare -n _result="$1"
local value="${2#"${2%%[![:space:]]*}"}" # strip leading
_result="${value%"${value##*[![:space:]]}"}" # strip trailing
}
trim result " hello world "
echo "[$result]" # → [hello world]
44. What is IFS and why does it matter?
IFS (Internal Field Separator) controls word splitting. Default is space, tab, newline.
# Default IFS: splits on whitespace
line="one two three"
read -ra parts <<< "$line"
echo "${parts[1]}" # → two
# Custom IFS
IFS=: read -ra fields <<< "root:x:0:0:root:/root:/bin/bash"
echo "${fields[0]}" # → root
echo "${fields[6]}" # → /bin/bash
# Dangerous: unquoted "$@" with modified IFS
IFS=,
arr=($*) # BUG: splits on comma now
Best practice: declare IFS locally or restore it after use.
45. How do you use xargs efficiently?
# Run command for each item
find . -name "*.log" | xargs rm
# With spaces in filenames — use -print0 and -0
find . -name "*.log" -print0 | xargs -0 rm
# Parallel execution — run 4 at a time
cat urls.txt | xargs -P4 -I{} curl -sf {} -o /dev/null
# Pass multiple args at once (-n)
cat list.txt | xargs -n2 echo # pass 2 items at a time
# Replace {} placeholder
find . -name "*.sh" | xargs -I{} bash -n {} # syntax check all scripts
Real-World Scripts
46. Write a script to backup a directory with timestamped archives.
#!/usr/bin/env bash
set -euo pipefail
SOURCE="${1:?Usage: $0 <source-dir> <backup-dir>}"
BACKUP_DIR="${2:?Usage: $0 <source-dir> <backup-dir>}"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
ARCHIVE="${BACKUP_DIR}/backup_${TIMESTAMP}.tar.gz"
KEEP_DAYS="${KEEP_DAYS:-30}"
[[ -d "$SOURCE" ]] || { echo "Source not found: $SOURCE" >&2; exit 1; }
mkdir -p "$BACKUP_DIR"
echo "Backing up $SOURCE..."
tar -czf "$ARCHIVE" -C "$(dirname "$SOURCE")" "$(basename "$SOURCE")"
echo "Backup created: $ARCHIVE ($(du -sh "$ARCHIVE" | cut -f1))"
# Remove backups older than KEEP_DAYS
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime "+${KEEP_DAYS}" -delete
echo "Cleaned up backups older than ${KEEP_DAYS} days"
47. Write a script to monitor disk usage and alert when threshold exceeded.
#!/usr/bin/env bash
set -euo pipefail
THRESHOLD="${THRESHOLD:-80}"
ALERT_EMAIL="${ALERT_EMAIL:-}"
df -h --output=pcent,target | tail -n +2 | while read -r usage mountpoint; do
pct="${usage%%%}" # strip % sign
if (( pct >= THRESHOLD )); then
msg="ALERT: $mountpoint is ${pct}% full (threshold: ${THRESHOLD}%)"
echo "$msg"
if [[ -n "$ALERT_EMAIL" ]]; then
echo "$msg" | mail -s "Disk Alert: $(hostname)" "$ALERT_EMAIL"
fi
fi
done
48. Write a script to process CSV data.
#!/usr/bin/env bash
# Input: CSV with header: name,department,salary
# Output: average salary per department
FILE="${1:?Usage: $0 <csv-file>}"
awk -F',' '
NR==1 { next } # skip header
{
dept=$2
sal=$3
sum[dept] += sal
count[dept]++
}
END {
printf "%-20s %10s %10s\n", "Department", "Count", "Avg Salary"
printf "%-20s %10s %10s\n", "----------", "-----", "----------"
for (d in sum)
printf "%-20s %10d %10.2f\n", d, count[d], sum[d]/count[d]
}
' "$FILE" | sort -t$'\t' -k3 -rn
49. How do you use Bash for API calls and JSON parsing?
#!/usr/bin/env bash
set -euo pipefail
API_URL="https://api.github.com"
REPO="${1:?Usage: $0 <owner/repo>}"
# Fetch and parse with jq
response=$(curl -sf \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GITHUB_TOKEN:-}" \
"${API_URL}/repos/${REPO}")
name=$(jq -r '.full_name' <<< "$response")
stars=$(jq -r '.stargazers_count' <<< "$response")
forks=$(jq -r '.forks_count' <<< "$response")
lang=$(jq -r '.language // "N/A"' <<< "$response")
printf "Repo: %s\nStars: %d\nForks: %d\nLanguage: %s\n" \
"$name" "$stars" "$forks" "$lang"
# Paginated API call
page=1
while true; do
data=$(curl -sf "${API_URL}/repos/${REPO}/issues?page=${page}&per_page=100")
count=$(jq 'length' <<< "$data")
(( count == 0 )) && break
jq -r '.[] | [.number, .title] | @tsv' <<< "$data"
(( page++ ))
done
50. What are common Bash script anti-patterns to avoid?
| Anti-pattern | Problem | Fix |
|---|---|---|
ls | grep pattern |
Parses ls output (breaks with special filenames) | find . -name "pattern" or glob |
cat file | grep |
Useless use of cat | grep pattern file |
for line in $(cat file) |
Word splitting, glob expansion | while IFS= read -r line |
if [ $? -eq 0 ] |
Loses context, verbose | if command; then directly |
Unquoted $VAR |
Word splitting and glob expansion | Always quote: "$VAR" |
rm -rf $DIR |
If DIR is empty → rm -rf / |
rm -rf "${DIR:?DIR not set}" |
eval "$(user_input)" |
Arbitrary code execution | Use arrays, read, or nameref |
Ignoring set -e pipeline |
Silent errors propagate | set -euo pipefail at top |
Bash vs other scripting languages
| Dimension | Bash | Python | Ruby | PowerShell |
|---|---|---|---|---|
| Best for | System glue, CLI tools, DevOps | Complex logic, data, APIs | Scripting, automation | Windows/Azure admin |
| Startup overhead | Minimal | ~30–100ms | ~50–100ms | ~500ms+ |
| Data structures | Strings, arrays, assoc arrays | Full (dict, list, class) | Full (Hash, Array, class) | Full (.NET objects) |
| Error handling | Explicit (set -e, ` |
, trap`) |
Exceptions | |
| Cross-platform | Linux/macOS (limited Windows) | Excellent | Good | Windows-first |
| Readability | Low (terse) | High | High | Medium |
| Available everywhere | Yes (Linux/Unix) | Almost | No | Windows only |
| String manipulation | Painful | Excellent | Excellent | Good |
Rule of thumb: If your script grows beyond 100 lines and needs complex logic, data structures, or HTTP calls → switch to Python.
Common mistakes
| Mistake | Why it fails | Correct approach |
|---|---|---|
if [ $VAR == "foo" ] |
Fails if VAR empty or has spaces | if [[ "$VAR" == "foo" ]] |
while read line |
Strips leading/trailing whitespace | while IFS= read -r line |
count++ |
Not arithmetic syntax in bash | (( count++ )) or count=$(( count + 1 )) |
[ -z $VAR ] |
Unquoted fails if VAR has spaces | [ -z "$VAR" ] |
2>&1 > file |
Order matters: stderr still goes to terminal | > file 2>&1 |
Forgetting local |
Function variables pollute global scope | Always local inside functions |
for i in $(seq 1 100) |
Forks subshell, slow | for (( i=1; i<=100; i++ )) |
Not using mktemp |
Hardcoded temp files collide | TMP=$(mktemp); trap 'rm -f "$TMP"' EXIT |
Bash vs related terms
| Term | What it is |
|---|---|
| Bash | Bourne Again SHell — most common Linux shell, scripting language |
| sh | POSIX shell — minimal, no arrays or [[]]; Bash runs as sh with #!/bin/sh |
| zsh | Z shell — Bash-compatible + features (used by macOS default since Catalina) |
| dash | Debian Almquist Shell — fast POSIX sh, Ubuntu's /bin/sh; no Bash extensions |
| fish | Friendly Interactive Shell — user-friendly but not POSIX compatible |
| Shell | Generic term for any command interpreter (bash/zsh/sh/fish/etc.) |
| Terminal | Application that hosts a shell (iTerm2, GNOME Terminal, Windows Terminal) |
| CLI | Command-Line Interface — what shell scripts automate |
FAQ
Q: Should I use #!/bin/bash or #!/usr/bin/env bash?
Use #!/usr/bin/env bash for portability — on macOS, Homebrew bash may be in /usr/local/bin or /opt/homebrew/bin, not /bin/bash.
Q: When should I use Bash vs Python for scripting? Bash for: shell glue, file operations, running other commands, simple text processing under 100 lines. Python for: complex logic, JSON/API handling, data structures, anything that grows large.
Q: What is the safest way to handle filenames with spaces?
Always quote variables ("$file"), use find -print0 | xargs -0, or use mapfile -t / while IFS= read -r to populate arrays from file lists.
Q: How do I debug a Bash script?
bash -n script.sh — syntax check (no execution). bash -x script.sh — trace execution (print each command). set -x / set +x inside script to trace specific sections. shellcheck script.sh — static analysis tool (install separately).
Q: How do I make a Bash script exit on first error?
Add set -euo pipefail after the shebang. -e exits on error, -u errors on unset vars, -o pipefail catches pipe failures. Add trap 'echo "Error on line $LINENO"' ERR for line number reporting.
Q: What is ShellCheck?
ShellCheck (shellcheck.net) is a static analysis tool that catches common Bash bugs — unquoted variables, [ ] vs [[ ]], deprecated syntax, portability issues. Run it as part of CI on all shell scripts.