Shell Scripting

Shell Scripting

A comprehensive question and answer guide for Shell Scripting — from the very basics to advanced topics. Use it to study, review, or prepare for interviews.


Beginner

Q1: What is a shell?

A shell is a command-line interpreter that provides an interface between the user and the operating system kernel. It reads commands typed by the user and executes them. Common shells include bash, sh, zsh, fish, and dash.

Q2: What is a shell script?

A shell script is a plain text file containing a sequence of shell commands that are executed in order. Shell scripts are used to automate repetitive tasks, manage files, and configure systems.

Q3: What is a shebang and why is it important?

The shebang (#!) is the first line of a shell script that tells the operating system which interpreter to use to execute the script.

#!/bin/bash
echo "Hello, World!"

Without the shebang the script may be run with the wrong shell or fail to execute directly.

Q4: How do you make a shell script executable?

chmod +x script.sh
./script.sh

chmod +x adds execute permission, and ./ runs it from the current directory.

Q5: How do you print output to the terminal?

echo "Hello, World!"
printf "Name: %s\n" "Alice"

echo is simple and appends a newline automatically. printf gives more control over formatting (similar to C's printf).

Q6: What are the differences between echo and printf?

# echo with escape sequences (requires -e flag)
echo -e "Line1\nLine2"     # prints two lines
echo -n "No newline"       # suppress trailing newline

# printf — always interprets escape sequences, no flag needed
printf "Line1\nLine2\n"    # prints two lines
printf "%-10s %5d\n" "age" 25  # formatted columns
printf "%s\n" "$@"         # safely print each argument on its own line

Prefer printf in scripts — it behaves consistently across shells and does not interpret leading dashes or escape sequences by default.

Q7: How do you define and use a variable?

name="Alice"
echo "Hello, ${name}"
  • No spaces around = when assigning.
  • Prefix the variable name with $ to read its value.

Q8: What are environment variables and how do you manage them?

Environment variables are variables inherited by child processes.

# Set and export a variable to child processes
export MY_VAR="hello"

# View all environment variables
env
printenv

# View a single variable
printenv HOME
echo "$PATH"

# Unset a variable
unset MY_VAR

# Set a variable only for one command (inline)
MY_VAR="world" ./script.sh

Variables set without export are local to the current shell and not visible in child processes.

Q9: How does redirection work in shell?

# Redirect stdout to a file (overwrite)
echo "hello" > output.txt

# Redirect stdout to a file (append)
echo "world" >> output.txt

# Redirect stdin from a file
sort < names.txt

# Redirect stderr to a file
ls /bad 2> errors.txt

# Redirect both stdout and stderr to a file
./script.sh > all.log 2>&1
./script.sh &> all.log       # bash shorthand

# Discard output entirely
./noisy-script.sh > /dev/null 2>&1

The order of redirections matters: 2>&1 must come after >.

Q10: What is the difference between single quotes and double quotes?

name="World"
echo 'Hello $name'   # prints: Hello $name  (no expansion)
echo "Hello $name"   # prints: Hello World  (variable expanded)

Single quotes treat everything literally. Double quotes allow variable and command substitution.

Q11: How do pipes and command chaining work?

# Pipe — pass stdout of one command as stdin to the next
ls -l | grep ".sh" | wc -l

# && — run next command only if previous succeeded (exit 0)
mkdir /tmp/mydir && cd /tmp/mydir

# || — run next command only if previous failed (non-zero exit)
cd /missing || echo "Directory not found"

# ; — run commands sequentially regardless of exit code
echo "start" ; sleep 2 ; echo "done"

Q12: How do you read input from the user?

read -p "Enter your name: " name
echo "Hello, $name"

# Silent input (e.g. passwords)
read -s -p "Password: " pass
echo ""

# Read with timeout
read -t 10 -p "Answer within 10s: " answer

read stores the input in a variable. -p displays a prompt, -s hides input, -t sets a timeout in seconds.

Q13: How do you write comments in a shell script?

# This is a single-line comment
echo "Running script..."  # inline comment

Only single-line comments (#) exist in shell. There is no native multi-line comment syntax, but a heredoc trick is sometimes used.

Q14: How do you perform arithmetic in shell?

a=5
b=3
sum=$((a + b))
echo "Sum: $sum"        # prints: Sum: 8

# Alternative with expr
result=$(expr $a \* $b)
echo "Product: $result" # prints: Product: 15

$((...)) is the preferred modern syntax for integer arithmetic.

Q15: What are common comparison operators for numbers?

Operator Meaning
-eq Equal
-ne Not equal
-lt Less than
-le Less than or equal
-gt Greater than
-ge Greater than or equal
if [ $a -gt $b ]; then
  echo "$a is greater than $b"
fi

Q16: How do you write an if-else statement?

age=18
if [ $age -ge 18 ]; then
  echo "Adult"
elif [ $age -ge 13 ]; then
  echo "Teenager"
else
  echo "Child"
fi

Q17: What are the basic loop types in shell?

# for loop
for i in 1 2 3 4 5; do
  echo "Number: $i"
done

# C-style for loop
for ((i=0; i<5; i++)); do
  echo "i=$i"
done

# while loop
count=1
while [ $count -le 5 ]; do
  echo "Count: $count"
  ((count++))
done

# until loop (runs until condition becomes true)
until [ $count -gt 5 ]; do
  echo "Count: $count"
  ((count++))
done

Q18: What is $0, $1, $2 … $@ and $#?

#!/bin/bash
echo "Script name : $0"
echo "First arg   : $1"
echo "Second arg  : $2"
echo "All args    : $@"
echo "Arg count   : $#"
Variable Meaning
$0 Name of the script
$1 to $9 Positional parameters (arguments)
$@ All arguments as separate strings
$* All arguments as a single string
$# Number of arguments

Q19: What does $? represent?

$? holds the exit status (return code) of the last executed command. 0 means success; any non-zero value means failure.

ls /nonexistent
echo "Exit code: $?"  # prints a non-zero value (e.g. 2)

Intermediate

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

[ ] is the POSIX-compliant test command available in all shells. [[ ]] is a bash/ksh/zsh extension that is more powerful and less error-prone.

# [[ ]] supports regex matching and logical operators without quoting issues
name="Alice"
if [[ $name == A* ]]; then
  echo "Name starts with A"
fi

# Regex matching with =~
if [[ "$name" =~ ^[A-Z][a-z]+$ ]]; then
  echo "Name looks like a proper noun"
fi

Prefer [[ ]] in bash scripts for safety and additional features.

Q21: How do you use regular expressions in shell?

# =~ operator inside [[ ]] for regex matching
email="user@example.com"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
  echo "Valid email"
fi

# grep with regex
echo "foo123" | grep -E '^[a-z]+[0-9]+$'

# Case-insensitive regex match
grep -iE 'error|warn' logfile.txt

Captured groups from =~ are stored in the BASH_REMATCH array: ${BASH_REMATCH[0]} is the full match, ${BASH_REMATCH[1]} is the first group.

Q22: How do you define and call a function?

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

greet "Alice"
greet "Bob"

Use local to limit a variable's scope to the function. Functions return an exit code (0–255), not a value — use echo to return data.

Q23: How do you capture the output of a command?

# Command substitution
today=$(date +%Y-%m-%d)
echo "Today is: $today"

# Backtick style (older, avoid)
files=`ls`

Prefer $(...) over backticks — it is more readable and supports nesting.

Q24: What is a heredoc and when do you use it?

A heredoc lets you pass a multi-line string to a command without creating a temporary file.

cat <<EOF
Line 1
Line 2
Today is $(date)
EOF

# Indented heredoc (bash 4.4+ with <<-)
cat <<-EOF
        Indented line 1
        Indented line 2
EOF

Use <<'EOF' (single-quoted delimiter) to disable variable expansion inside the heredoc.

Q25: What is a here-string?

A here-string (<<<) passes a single string as stdin to a command — a compact alternative to echo+pipe.

# Instead of: echo "hello world" | read -r a b
read -r a b <<< "hello world"
echo "$a"   # hello
echo "$b"   # world

# Useful with grep, tr, etc.
grep "foo" <<< "foo bar baz"

# Pass a variable as stdin
tr '[:lower:]' '[:upper:]' <<< "$str"

Q26: How do arrays work in bash?

# Declare an array
fruits=("apple" "banana" "cherry")

echo "${fruits[0]}"     # apple
echo "${fruits[@]}"     # all elements
echo "${#fruits[@]}"    # number of elements

# Append an element
fruits+=("date")

# Loop over array
for fruit in "${fruits[@]}"; do
  echo "$fruit"
done

Q27: How do you read a file into an array with mapfile / readarray?

# Read all lines of a file into an array
mapfile -t lines < /etc/hosts
# or equivalently:
readarray -t lines < /etc/hosts

echo "${lines[0]}"          # first line
echo "${#lines[@]}"         # total number of lines

# Loop over lines
for line in "${lines[@]}"; do
  echo "$line"
done

The -t flag strips the trailing newline from each line. Both mapfile and readarray are synonyms and require bash 4.0+.

Q28: What is the difference between $@ and $* in arrays and functions?

When double-quoted:

  • "$@" expands each element as a separate quoted word — safe for filenames with spaces.
  • "$*" expands all elements as a single word joined by the first character of IFS.

Always prefer "$@" when iterating over arguments or array elements.

Q29: What are special variables $$ and $!?

Variable Meaning
$$ PID of the current shell/script
$! PID of the last background command
$- Current shell option flags
$_ Last argument of the previous command

Q30: How do you handle errors and exit codes?

#!/bin/bash
set -e          # exit immediately on error
set -u          # treat unset variables as errors
set -o pipefail # catch errors in pipelines

cp source.txt dest.txt || { echo "Copy failed"; exit 1; }

Using set -euo pipefail at the top of scripts is considered best practice.

Q31: What is process substitution?

Process substitution allows you to treat the output of a command as a file.

# Compare output of two commands
diff <(ls dir1) <(ls dir2)

# Read from a process as if it were a file
while IFS= read -r line; do
  echo "Line: $line"
done < <(grep "ERROR" logfile.txt)

Q32: What is the difference between source (.) and executing a script?

source script.sh  # or: . script.sh
./script.sh
  • source / . runs the script in the current shell — variable changes persist.
  • ./script.sh runs the script in a subshell — changes do not affect the parent shell.

Q33: How do you work with files and directories?

# Test conditions
[ -f file.txt ]   && echo "Is a regular file"
[ -d /tmp ]       && echo "Is a directory"
[ -r file.txt ]   && echo "Is readable"
[ -w file.txt ]   && echo "Is writable"
[ -x script.sh ]  && echo "Is executable"
[ -s file.txt ]   && echo "Is non-empty"
[ -e path ]       && echo "Exists"
[ -L link ]       && echo "Is a symbolic link"
[ -nt newer.txt ] && echo "Is newer than"
[ -ot older.txt ] && echo "Is older than"

Q34: How do you use grep, sed, and awk?

# grep — search for patterns in text
grep "error" logfile.txt          # lines containing "error"
grep -i "error" logfile.txt       # case-insensitive
grep -r "TODO" ./src/             # recursive search
grep -v "debug" logfile.txt       # invert match (exclude lines)
grep -c "error" logfile.txt       # count matching lines
grep -n "error" logfile.txt       # show line numbers

# sed — stream editor (find & replace, delete, insert)
sed 's/foo/bar/' file.txt         # replace first occurrence per line
sed 's/foo/bar/g' file.txt        # replace all occurrences
sed -i 's/foo/bar/g' file.txt     # edit file in place
sed '/^#/d' file.txt              # delete comment lines
sed -n '5,10p' file.txt           # print lines 5 to 10

# awk — pattern scanning and processing
awk '{print $1}' file.txt         # print first field (column)
awk -F: '{print $1}' /etc/passwd  # use : as delimiter
awk '$3 > 100' data.txt           # print rows where column 3 > 100
awk '{sum += $1} END {print sum}' numbers.txt  # sum a column
awk 'NR==5' file.txt              # print line 5

Q35: How do you use cut, tr, sort, uniq, and wc?

# cut — extract fields or characters
cut -d',' -f1,3 data.csv          # fields 1 and 3, comma-delimited
cut -c1-10 file.txt               # first 10 characters of each line

# tr — translate or delete characters
echo "Hello World" | tr 'a-z' 'A-Z'    # uppercase
echo "hello   world" | tr -s ' '       # squeeze multiple spaces
echo "hello" | tr -d 'l'               # delete character

# sort — sort lines
sort file.txt                     # alphabetical
sort -n numbers.txt               # numeric sort
sort -r file.txt                  # reverse sort
sort -u file.txt                  # sort and remove duplicates
sort -t',' -k2 data.csv           # sort CSV by second field

# uniq — filter duplicate adjacent lines (use after sort)
sort file.txt | uniq              # remove duplicates
sort file.txt | uniq -c           # count occurrences
sort file.txt | uniq -d           # show only duplicates

# wc — word/line/character count
wc -l file.txt                    # line count
wc -w file.txt                    # word count
wc -c file.txt                    # byte count

Q36: How do you use the find command?

# Find files by name
find /home -name "*.log"

# Find files by type (f=file, d=dir, l=symlink)
find /tmp -type f
find /var -type d -name "cache"

# Find by size
find . -size +10M                 # larger than 10 MB
find . -size -1k                  # smaller than 1 KB

# Find by modification time
find . -mtime -7                  # modified in the last 7 days
find . -mtime +30                 # modified more than 30 days ago

# Execute a command on found files
find . -name "*.tmp" -delete
find . -name "*.sh" -exec chmod +x {} \;

# Find and print with null delimiter (safe for filenames with spaces)
find . -name "*.txt" -print0 | xargs -0 grep "TODO"

Q37: How does xargs work?

xargs builds and executes commands from standard input — useful for processing lists of files or items.

# Basic usage — pass stdin as arguments
echo "file1 file2 file3" | xargs rm

# One argument per line (-L 1)
cat filelist.txt | xargs -L 1 echo "Processing:"

# Use null delimiter to handle filenames with spaces
find . -name "*.log" -print0 | xargs -0 rm -f

# Run in parallel (-P)
cat urls.txt | xargs -P 4 -I{} curl -O {}

# -I{} — replace {} with each input item
ls *.txt | xargs -I{} cp {} /backup/

Q38: How do you use the select statement for interactive menus?

select generates a numbered menu from a list and loops until explicitly broken.

#!/bin/bash
PS3="Choose an option: "   # the select prompt variable

select choice in "Start" "Stop" "Restart" "Quit"; do
  case $choice in
    Start)   echo "Starting..." ;;
    Stop)    echo "Stopping..." ;;
    Restart) echo "Restarting..." ;;
    Quit)    break ;;
    *)       echo "Invalid option $REPLY" ;;
  esac
done

REPLY holds the raw number the user typed. PS3 sets the prompt shown below the menu.

Q39: How does string manipulation work in bash?

str="Hello, World!"

echo "${#str}"           # Length: 13
echo "${str:7:5}"        # Substring: World
echo "${str,,}"          # Lowercase: hello, world!
echo "${str^^}"          # Uppercase: HELLO, WORLD!
echo "${str/World/Bash}" # Replace first: Hello, Bash!
echo "${str//l/L}"       # Replace all:   HeLLo, WorLd!
echo "${str#Hello, }"    # Remove prefix: World!
echo "${str%!}"          # Remove suffix: Hello, World

# Default value if variable is unset or empty
echo "${name:-"stranger"}"

# Assign default if unset
: "${config_file:=/etc/app.conf}"

Q40: How do you use case statements?

read -p "Enter a fruit: " fruit
case "$fruit" in
  apple)
    echo "You chose apple."
    ;;
  banana | mango)
    echo "You chose a tropical fruit."
    ;;
  [0-9]*)
    echo "That looks like a number, not a fruit."
    ;;
  *)
    echo "Unknown fruit."
    ;;
esac

Q41: What is IFS and how does it affect word splitting?

IFS (Internal Field Separator) controls how bash splits words. Default value is space, tab, and newline.

IFS=',' read -ra parts <<< "one,two,three"
for part in "${parts[@]}"; do
  echo "$part"
done

# Save and restore IFS
OLD_IFS="$IFS"
IFS=':'
read -ra paths <<< "$PATH"
IFS="$OLD_IFS"

Always restore IFS after changing it to avoid side effects.


Advanced

Q42: What is the difference between soft links and hard links?

ln -s target.txt symlink.txt  # soft (symbolic) link
ln   target.txt hardlink.txt  # hard link
  • A soft link is a pointer to a path — it breaks if the target is deleted or moved.
  • A hard link is another directory entry pointing to the same inode — it survives deletion of the original filename.

Q43: How do you write robust scripts using traps?

trap lets you catch signals and errors to perform cleanup before the script exits.

#!/bin/bash
tmpfile=$(mktemp)

cleanup() {
  echo "Cleaning up..."
  rm -f "$tmpfile"
}

trap cleanup EXIT              # runs on any exit
trap 'echo "Interrupted"' INT  # runs on Ctrl+C (SIGINT)
trap 'echo "Terminated"' TERM  # runs on kill (SIGTERM)
trap 'echo "Line $LINENO"' ERR # runs on any error (with set -e)

echo "Working..." > "$tmpfile"
# ... rest of script ...

Q44: What are Linux signals and how do you send them?

Signals are software interrupts sent to processes to notify them of events.

Signal Number Default action Common use
SIGHUP 1 Terminate Reload config (daemons)
SIGINT 2 Terminate Ctrl+C — interrupt
SIGQUIT 3 Core dump Ctrl+\
SIGKILL 9 Terminate (forceful) Cannot be caught or ignored
SIGTERM 15 Terminate (graceful) Default signal sent by kill
SIGSTOP 19 Stop process Cannot be caught or ignored
SIGTSTP 20 Stop process Ctrl+Z — suspend to background
SIGUSR1 10 Terminate User-defined signal
SIGUSR2 12 Terminate User-defined signal
kill -15 $PID       # send SIGTERM (graceful shutdown)
kill -9  $PID       # send SIGKILL (force kill)
kill -HUP $PID      # send SIGHUP (reload)
kill -l             # list all signals
pkill -f "myapp"    # kill by process name pattern

Q45: How do job control (fg, bg, jobs, disown) work?

# Run a command in the background
sleep 100 &

# List background jobs
jobs
# [1]+  Running    sleep 100 &

# Bring job 1 to foreground
fg %1

# Suspend foreground job (Ctrl+Z), then send to background
bg %1

# Disown a job — detach it from the shell (survives shell exit)
disown %1

# Run a command immune to hangup signal (survives logout)
nohup ./long-running.sh &

# Check if a specific PID is running
kill -0 $PID && echo "Running" || echo "Not running"

Q46: What is ulimit and how do you use it?

ulimit controls the resource limits available to the shell and its child processes.

ulimit -a             # show all current limits

ulimit -n 4096        # max open file descriptors
ulimit -u 100         # max number of user processes
ulimit -v $((512*1024)) # max virtual memory (KB)
ulimit -s 8192        # max stack size (KB)
ulimit -t 60          # max CPU time (seconds)

# Soft vs hard limits
ulimit -Sn            # show soft limit for open files
ulimit -Hn            # show hard limit for open files

Hard limits can only be raised by root. Soft limits can be raised up to the hard limit by the user.

Q47: How do named pipes (FIFOs) work?

mkfifo mypipe
echo "Hello from producer" > mypipe &   # writer (runs in background)
cat mypipe                               # reader (blocks until writer writes)
rm mypipe

Named pipes allow inter-process communication between unrelated processes.

Q48: What are /dev/fd, /dev/tcp, and /dev/udp?

These are special virtual files provided by the kernel and bash.

# /dev/fd — file descriptor access
ls /dev/fd            # 0=stdin, 1=stdout, 2=stderr
exec 3> output.txt    # open fd 3 for writing
echo "hello" >&3
exec 3>&-             # close fd 3

# /dev/tcp — open a TCP socket (bash built-in, no netcat needed)
exec 3<>/dev/tcp/example.com/80
echo -e "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" >&3
cat <&3
exec 3>&-

# /dev/udp — open a UDP socket
echo "hello" > /dev/udp/127.0.0.1/9999

/dev/tcp and /dev/udp are bash extensions — not available in plain sh.

Q49: How do you implement a mutex/lock in shell scripts?

LOCKFILE="/tmp/myscript.lock"

acquire_lock() {
  if ! mkdir "$LOCKFILE" 2>/dev/null; then
    echo "Script is already running. Exiting."
    exit 1
  fi
}

release_lock() {
  rmdir "$LOCKFILE"
}

trap release_lock EXIT
acquire_lock

echo "Critical section running..."

Using mkdir for locking is atomic on most file systems, making it safer than touch.

Q50: How do you run commands in parallel and wait for them?

#!/bin/bash
pids=()

for i in 1 2 3 4; do
  sleep "$i" &
  pids+=($!)
done

for pid in "${pids[@]}"; do
  wait "$pid" && echo "PID $pid finished OK" || echo "PID $pid failed"
done

& runs a command in the background, wait waits for it, $! captures its PID.

Q51: What is a subshell and how does it affect variable scope?

A subshell is a child process created by the current shell. Variables set in a subshell are not visible in the parent.

x=10
(
  x=20
  echo "Inside subshell: x=$x"   # 20
)
echo "Outside subshell: x=$x"    # 10 — unchanged

Subshells are created by (...), pipelines, command substitution, and background jobs.

Q52: How do you handle long options and flags with getopts / getopt?

#!/bin/bash
usage() { echo "Usage: $0 [-n name] [-v]" >&2; exit 1; }

verbose=0
name=""

while getopts ":n:v" opt; do
  case $opt in
    n) name="$OPTARG" ;;
    v) verbose=1 ;;
    :) echo "Option -$OPTARG requires an argument." >&2; usage ;;
   \?) echo "Invalid option: -$OPTARG" >&2; usage ;;
  esac
done

shift $((OPTIND - 1))
echo "Name: $name, Verbose: $verbose, Remaining args: $*"

getopts is POSIX-compliant and built-in. Use getopt (external) for long options (--name).

Q53: How does coproc work in bash?

coproc creates a coprocess — a background process with bidirectional pipes to the current shell.

coproc my_proc { cat; }

echo "Hello" >&"${my_proc[1]}"   # write to coprocess stdin
read line <&"${my_proc[0]}"      # read from coprocess stdout
echo "Got: $line"

Useful when you need a persistent two-way communication channel with a child process.

Q54: How do you use associative arrays (hash maps)?

Associative arrays require bash 4.0+.

declare -A capitals

capitals["France"]="Paris"
capitals["Germany"]="Berlin"
capitals["Japan"]="Tokyo"

echo "${capitals[France]}"         # Paris
echo "${!capitals[@]}"             # all keys
echo "${capitals[@]}"              # all values
echo "${#capitals[@]}"             # number of entries

# Delete an entry
unset 'capitals[Germany]'

# Iterate over key-value pairs
for country in "${!capitals[@]}"; do
  echo "$country -> ${capitals[$country]}"
done

Q55: How do you profile and debug a shell script?

#!/bin/bash
set -x    # print each command and its expanded form before execution
set -v    # print shell input lines as they are read

# Measure execution time of a section
time {
  for i in $(seq 1 1000); do
    :
  done
}

You can also run a script with bash -x script.sh without modifying it. Use PS4='+(${BASH_SOURCE}:${LINENO}): ' to include file and line number in trace output.

Q56: What are the differences between exec, fork, and source in shell?

Mechanism Description
fork Shell creates a child process; parent waits for it to finish
exec Replaces the current process image — no child is created
source/. Runs commands in the current shell process; no new process created
exec /bin/bash    # replaces current shell — the original process is gone

Q57: What are bash vs sh compatibility gotchas?

#!/bin/sh   # strict POSIX sh — many bash features are unavailable

# These are bash-only — will FAIL under /bin/sh:
# [[ ]]          — use [ ] instead
# arrays         — not supported
# (( ))          — use expr or [ ] for arithmetic
# local          — not POSIX (works in most sh implementations but not guaranteed)
# $'...'         — ANSI-C quoting
# &>             — use > file 2>&1
# {a..z}         — brace expansion
# mapfile        — bash 4+ only
# source         — use . instead
# echo -e        — not portable; use printf

If portability is required, use #!/bin/sh and validate with checkbashisms or shellcheck.

Q58: How do you write a script logging pattern?

#!/bin/bash

# Log levels
LOG_FILE="/var/log/myscript.log"

log() {
  local level="$1"
  shift
  local message="$*"
  local timestamp
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')
  echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

log_info()  { log "INFO " "$@"; }
log_warn()  { log "WARN " "$@" >&2; }
log_error() { log "ERROR" "$@" >&2; }

# Usage
log_info  "Script started"
log_warn  "Configuration file missing — using defaults"
log_error "Failed to connect to database"

Q59: How do you integrate a shell script with cron?

# Edit the crontab for the current user
crontab -e

# Crontab syntax:
# ┌──────── minute        (0–59)
# │ ┌────── hour          (0–23)
# │ │ ┌──── day of month  (1–31)
# │ │ │ ┌── month         (1–12)
# │ │ │ │ ┌ day of week   (0–7, 0 and 7 = Sunday)
# │ │ │ │ │
# * * * * *  command

# Examples
0 * * * *     /home/user/backup.sh         # every hour at :00
30 2 * * *    /scripts/cleanup.sh          # every day at 02:30
0 0 * * 0     /scripts/weekly-report.sh   # every Sunday at midnight
*/5 * * * *   /scripts/check-health.sh    # every 5 minutes

# Cron environment is minimal — always use absolute paths
# and explicitly set needed variables at the top of your script:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/home/user

Cron does not load .bashrc or .profile — always use absolute paths and export PATH explicitly.

Q60: What are eval risks and safer alternatives?

# DANGEROUS — eval executes arbitrary code
user_input="rm -rf /"
eval "$user_input"    # catastrophic!

# Safer alternative: use variable indirection
var_name="greeting"
declare "$var_name=hello"
echo "${!var_name}"   # hello  (indirect expansion)

# Safer alternative: use arrays instead of dynamic variable names
declare -A data
data["key"]="value"
echo "${data[key]}"

# If eval is truly needed, sanitize input strictly
safe_name="${input//[^a-zA-Z0-9_]/}"
eval "echo \"\$$safe_name\""

Q61: How do you write a self-contained script that detects its own location?

#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Script is located in: $SCRIPT_DIR"

This works correctly even when the script is sourced, symlinked, or called from a different directory.

Q62: What are some security best practices for shell scripting?

  1. Always quote variables"$var" prevents word splitting and globbing issues.
  2. Validate input — never trust user-supplied data.
  3. Avoid eval — it executes arbitrary code and is a common injection vector.
  4. Use absolute paths for critical commands in scripts run as root.
  5. Restrict permissions — scripts that contain secrets should not be world-readable.
  6. Use set -euo pipefail — catch errors early.
  7. Sanitize filenames — use -- to separate options from filenames (e.g., rm -- "$file").
  8. Prefer mktemp for temporary files instead of predictable names.
  9. Use ShellCheck — a static analysis tool that catches common bugs and pitfalls.
  10. Avoid storing secrets in scripts — use environment variables or a secrets manager.
# Unsafe
tmpfile="/tmp/myapp-$$"

# Safe
tmpfile=$(mktemp /tmp/myapp-XXXXXX)
trap 'rm -f "$tmpfile"' EXIT