Shell Scripting Interview Questions for 2026

Shell scripting interview questions are a fixture of DevOps, SRE, platform, and backend hiring loops, because the shell is where operational automation lives and where candidates reveal whether they understand the machine under the commands. This page is a complete, tiered bank: 104 questions with worked answers, split into beginner, intermediate, and scenario sections, plus hands-on labs, a command cheat sheet, and a five-question quiz at the end. Work through it in order, or jump straight to the tier that matches your next round.

Shell scripting interview questions tiered bank with beginner, intermediate, and scenario sections
The three tiers in this bank: daily-driver fluency, Bash mechanics, and debugging under pressure.

TL;DR

  • 104 questions, three tiers. Beginner daily-driver fluency, intermediate Bash mechanics such as expansion order, traps, and arrays, and realistic debugging scenarios, each with a worked answer written the way interviewers want to hear it.
  • Answers explain mechanisms. Every answer names the component that owns the behavior, the parser, the kernel, cron’s environment, then gives the command, then the caveat that separates a pass from a strong hire.
  • Debugging is the graded skill. Scenario rounds hand you a broken script in one sentence and score your diagnostic order, so the labs and cheat sheet rehearse exactly that: quoting failures, swallowed pipeline errors, stale locks, and cron surprises.

How the shell scripting interview works in 2026

Most shell scripting interview questions arrive in one of three rounds. The first is the screening call, often with a hiring manager or senior engineer, that mixes definitions with quick precision checks: what single quotes and double quotes actually do differently, what $? holds after a pipeline, why chmod +x matters. These are confidence filters. Nobody expects a lecture; they expect a crisp 30-second answer with one level of detail behind it, because a candidate who fumbles quoting in minute five will not survive the debugging round in minute forty.

The second round is the deep technical, and this is where shell scripting interview questions get mechanical: the order bash expands a command line, what IFS controls, when set -e refuses to fire, how trap and flock and mktemp combine into a script that survives production. Interviewers at this stage are listening for whether your knowledge has edges. Saying “quotes preserve spaces” passes a beginner bar; explaining that quoting removes an expansion from word splitting and pathname expansion passes a working-engineer bar. Shell scripting interview questions are graded on that second sentence, not the first.

The third round is the scenario round, sometimes live on a terminal, sometimes a take-home against a container with a deliberately broken script. You get one symptom sentence: “the backup works by hand and fails in cron,” “the pipeline exits green and ships corrupt data,” “the job left a lock file and now refuses to start.” What is scored is your diagnostic order and the reasoning between steps, not luck or typing speed. Engineers who narrate “first I check the environment, because cron strips it” outperform engineers who guess, and most scenario shell scripting interview questions are won or lost in that narration.

Role shape moves the emphasis. DevOps and platform roles weight integration topics: scripts as cron jobs and systemd units, CI runners, deployment glue, and the environment gaps that break them. SRE loops add reliability mechanics: locking, idempotency, safe temp files, signal handling. Backend roles still ask, usually fewer questions aimed at build tooling and text processing. The base bank on this page is the same for all of them; only the expected depth moves.

Preparation that works is boringly mechanical. Write small scripts, break them on purpose, and run shellcheck on everything until its warnings become reflexes. Then rehearse the explanations out loud, because a spoken answer with one mechanism detail beats a silent perfect answer every time. Pair this page with a scratch VM or container: create a file named report final.csv, point an unquoted rm at it in a safe directory, and watch the disaster happen in miniature. Shell scripting interview questions reward people who have broken these things deliberately, and the labs at the end exist so you can.

Core concepts interviewers expect you to know

Three mental models unlock most intermediate and scenario shell scripting interview questions: how the parser turns a line of text into a running process, what contract a script’s exit status makes with its caller, and what a production-safe script skeleton looks like. Interviewers love these because everything else in the bank is a special case of one of them.

Quoting and the expansion pipeline

When you press Enter, bash does not just run your words. It applies expansions in a fixed order: brace and tilde expansion, then parameter, arithmetic, and command substitution, then word splitting on the characters in IFS, then pathname expansion of any unquoted glob characters, then quote removal. The dangerous stages are the last two before execution. An unquoted $var whose value contains spaces becomes several words at stage three, and any of those words containing * or ? becomes a list of files at stage four. Quoting an expansion, "$var", removes it from both stages, which is the entire logic behind the quoting discipline scripts follow.

This one diagram explains a whole family of bugs, which is why it sits under so many shell scripting interview questions: the space-in-filename deletion, the empty variable that globbed the working directory, the for f in $(ls) loop that shredded a directory of odd names. If you can narrate the stages and point at where a given bug happens, you can answer the entire quoting branch of an interview loop from one model.

From command line to process: the expansion order 1. Brace + tilde a{b,c} ~user 2. Substitutions $var $(cmd) $((a+b)) 3. Word splitting unquoted text split on IFS 4. Glob expansion * ? [abc] match files 5. Quote removal quotes vanish here fork + exec the command finally runs split words are matched against filenames here Quoting a variable removes it from stages 3 and 4 entirely.
Bash expands a command in a fixed order; unquoted variables face word splitting and globbing, quoted ones skip both stages.

Exit codes and the failure contract

Every command ends with a number, 0 for success and nonzero for failure, and everything above the shell is built on that contract: if tests it, && and || branch on it, CI systems judge jobs by it, and a script’s caller can only trust it if the script manages it deliberately. The mechanics have sharp edges that interviewers probe. A pipeline’s status is its last command’s unless set -o pipefail says otherwise, so a failed producer can hide behind a successful consumer forever. set -e exits on failure but skips its check inside if conditions, on the left of &&, and for every pipeline stage except the last. The status a script reports is its last command’s unless exit N says otherwise, which is how a script that printed an error still leaves CI green: cleanup ran last, cleanup succeeded, contract violated.

The professional framing interviewers want: strict mode, set -euo pipefail, plus an ERR trap that logs the failing line, converts silent corruption into loud, attributed failure. Expected failures get explicit guards like if grep -q or || true at exactly the spots where failure is normal, never globally. That pairing of strictness with deliberate exceptions is the design taste scenario shell scripting interview questions are circling.

The robust script skeleton: traps, locks, and temp files

Production scripts share a skeleton. They set strict mode at the top. They acquire a lock with flock on an open file descriptor, so the kernel holds the lock and a killed script cannot leave one behind. They create scratch space with mktemp -d instead of predictable names in /tmp. And they declare one cleanup path, trap cleanup EXIT, that removes temp trees and releases resources no matter how the script leaves: normal exit, set -e abort, or a fatal INT or TERM, which bash converts into an exit with status 128 plus the signal number while still running the trap. Only SIGKILL escapes, because the kernel never asks the process.

That skeleton answers an entire block of shell scripting interview questions in advance: why the job stopped leaving lock files behind, why the race between two nightly instances disappeared, why the audit no longer flags fixed /tmp paths. When you can draw it and explain each piece’s job, the follow-up questions become extensions rather than new topics.

Lifecycle of a robust script strict mode set -euo pipefail lock + temp dir flock, mktemp -d do the work sync, deploy, import exit with code exit 0 or exit 1 EXIT trap: one cleanup path rm -rf tmpdir, close lock fd, log the result runs on success, on failure, and on Ctrl+C Fail anywhere and the trap still runs; only SIGKILL gets past it.
Every exit path, success, error, or signal, converges on the single EXIT trap, so cleanup code exists exactly once and cannot be skipped.

Everything else in the bank hangs off these models. Arrays and parameter expansion are parser tools that keep data whole through the expansion pipeline. getopts and argument loops are just case statements with bookkeeping. Cron failures are the skeleton meeting a stripped environment. When you rehearse, keep asking which stage of the pipeline, or which side of the contract, owns this behavior, because that is the question underneath most shell scripting interview questions, and the interviewers know it.

Question bank at a glance

The bank below is organized the way shell scripting interview questions are actually asked: broadly increasing difficulty within each tier, then the scenario round that finishes most loops. Counts are exact so you can pace a week of prep: roughly 15 beginner questions a day gets you through the bank in a working week, with the labs on the weekend.

SectionQuestionsWhat it proves
Beginner38You use the shell daily and know quoting, pipes, loops, and exit codes cold.
Intermediate40You understand mechanisms: expansion order, arrays, traps, strict mode, text tools.
Scenario and troubleshooting26You can take a broken script symptom and diagnose it in a defensible order.

Difficulty inside each tier still rises as you go, so if a section starts feeling easy near its end, move on. Every answer names the mechanism and the command, because shell scripting interview questions grade both halves: the right tool, and the right explanation of why that tool applies.

Beginner shell scripting interview questions

These 38 open the bank of shell scripting interview questions, and they look easy, which is the trap. Interviewers use them as precision checks: answer each in one or two sentences, then add exactly one supporting detail. Say more and you burn round time; say less and you sound like a list. All 38 are answerable from a bash prompt, so type them as you read.

What is a shell script?

Answer: A text file of shell commands that an interpreter such as bash executes in sequence. The shell reads each line, expands it, and runs it exactly as if you had typed it interactively, which is why every habit from the prompt becomes automatable. That bridge from typing to scheduling and versioning is the foundation of operations work, and it is the opening idea the rest of an interview loop builds on.

What does the shebang line do?

Answer: The first line, #!/usr/bin/env bash, tells the kernel which interpreter to launch when the script runs directly. Without it, the file gets handed to the caller’s current shell, so behavior silently varies between machines. Using env bash resolves bash from PATH instead of hardcoding /bin/bash, which helps on BSDs and Nix-style systems. Shell scripting interview questions open on this line because it proves you know a script never guesses its own shell.

How do chmod +x, bash script.sh, and source script.sh differ?

Answer: chmod +x plus ./script.sh runs it as a new process using the shebang’s interpreter. bash script.sh skips the execute bit and names the interpreter explicitly, handy when permissions are wrong. source script.sh executes the commands inside your current shell, so variables and cd changes persist afterward, which is also how source leaks side effects. The persistence difference is the part interviewers want stated out loud.

What is the difference between sh and bash?

Answer: sh is a program name, not a language: on modern Debian and Ubuntu it is dash, a small POSIX shell without bash’s extensions. Scripts using arrays, [[ ]], or mapfile fail under it with confusing errors. The rule: declare #!/bin/sh only when you deliberately write portable POSIX code, otherwise declare bash. So many broken scheduled jobs trace back to this mismatch that it anchors the portability branch of shell scripting interview questions.

How do variable assignments work in bash?

Answer: name=value, with no spaces around the equals sign: name = value instead runs a command named name with two arguments. Names are letters, digits, and underscores, never starting with a digit, and values containing spaces need quotes: name="two words". Assignments without export stay local to the shell and never reach child processes. The no-spaces rule catches everyone once, which is why it appears early in every scripting round.

What is the difference between single quotes and double quotes?

Answer: Single quotes are literal: nothing inside expands, so '$HOME' is five characters of text. Double quotes expand variables and command substitutions but suppress word splitting and globbing, so "$var" stays one argument even when it contains spaces. The working rule: double-quote every expansion in a script, and use single quotes when data must pass through untouched. This distinction causes more real damage than any other, so it sits near the front of shell scripting interview questions.

One variable, two destinies file=”report final.csv” rm $file rm “$file” two arguments report, final.csv: wrong files one argument exactly the file you meant unquoted: split, then glob quoted: stays one word
An unquoted expansion is split on whitespace and re-globbed at execution time; quoting keeps the value as a single word.
What are the positional parameters $0, $1, $#, and $@?

Answer: $0 is the script’s name, $1 through $9 are the first nine arguments, ${10} needs braces beyond that, $# counts them, and $@ holds them all. shift discards $1 and slides the rest down, which is how parsing loops walk an argument list. Each parameter has a job: count checks, usage messages, parse loops. Mapping parameters to their jobs is the practical depth this question wants.

Why does “$@” matter so much, and how is it different from $*?

Answer: "$@" expands to each argument as its own correctly bounded word; $* and unquoted $@ join everything into one string that gets re-split, so arguments containing spaces merge or shred. "$*" deliberately joins all arguments with the first character of IFS, which is useful when you truly want one string. The daily habit is simple: forward arguments only as "$@". This asymmetry is among the most quoted examples in shell scripting interview questions because it is testable in one line.

How do you read input into a script?

Answer: Two channels: arguments, which arrive as positional parameters, and stdin, which read var consumes one line at a time. read -p "prompt: " var adds a prompt and read -r stops backslash mangling, so -r is the default in any serious script. For whole files, the canonical loop is while IFS= read -r line; do ... done < file. Knowing both channels and their guard flags is the fluency this question checks.

echo or printf: which belongs in scripts, and why?

Answer: printf '%s\n' "$value" is the predictable choice: the format string controls interpretation, the arguments are pure data, and every POSIX system agrees on the behavior. echo varies: some shells expand backslash escapes in the data, -e and -n disagree across systems, and echo "$value" prints nothing when the value happens to be -n. One printf line retires the entire class of surprises, which is exactly the kind of portability judgment interviewers grade.

What are exit codes, and how do you read one?

Answer: Every command finishes with a numeric status for its caller: 0 means success, nonzero means failure, and $? holds the last code. A script ends with its last command’s status unless exit N says otherwise, so a script can print an error, run successful cleanup last, and still exit 0 into your CI pipeline. Always end with an explicit exit status. That contract is the subject of many follow-up shell scripting interview questions.

How do if statements and test operators work?

Answer: if [ -f "$file" ]; then runs the test command and branches on its exit status, because if checks a command, nothing more magical. File tests: -f regular file, -d directory, -e exists, -r, -w, -x permissions. Strings: -z empty, -n nonempty, = equal. Numbers use -eq, -lt, -ge, not = or <. Quote both sides of every comparison. Operator fluency at this level is table stakes in any scripting round.

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

Answer: [ is the classic test command: POSIX, portable, and unforgiving, since unquoted variables break its parsing and < must be escaped. [[ ]] is a bash keyword: no word splitting inside, safe < and > string comparison, == pattern matching, =~ regex, and && plus || combining tests in place. Pick [ when POSIX portability is the goal, [[ ]] inside bash scripts. Explaining why the safer form exists is what this entry in shell scripting interview questions listens for.

Why does [ $var = “yes” ] fail when var is empty?

Answer: The shell expands $var before the test runs, so an empty value leaves [ = "yes" ], and the test command sees = as its first argument and errors out. Quoting both sides, [ "$var" = "yes" ], preserves the empty argument, and [[ $var = yes ]] is safe by design because no splitting happens inside. Every engineer hits this once, which is why it remains one of the classic shell scripting interview questions for beginners.

What do && and || do between commands?

Answer: They are shortcut conditionals on exit status: cmd1 && cmd2 runs cmd2 only if cmd1 succeeded, cmd1 || cmd2 runs cmd2 only if cmd1 failed. Idioms everywhere: mkdir -p dir && cd dir, [ -f config ] || exit 1. Chains evaluate left to right and short-circuit at the first decision that settles them. Worth adding: these are list operators, not booleans, so the whole chain’s outcome lands in $? and interacts with set -e.

What are the three standard streams?

Answer: stdin is descriptor 0, the input; stdout is 1, the normal output; stderr is 2, the diagnostics. Programs compose because stdout carries data and stderr carries complaints: grep miss input | wc -l pipes data while errors print untouched to your terminal. Mixing them floods pipelines with log noise. Keeping data and diagnostics separate by design is the point behind many shell scripting interview questions about output handling.

How do you redirect stdout and stderr, and what does 2>&1 mean?

Answer: cmd > out.txt sends stdout to a file, 2> err.txt captures stderr, and 2>&1 points stderr wherever stdout currently goes, so cmd > all.log 2>&1 captures both in one place. Order matters because redirections apply left to right: 2>&1 > file points stderr at the terminal, stdout’s original target, not the file. Bash’s &> file does both at once but is not portable. Explaining why the order matters is the depth layer in shell scripting interview questions here.

What is the difference between > and >>?

Answer: > creates the file or truncates it to zero before the command writes; >> appends after existing content. Truncation is instant and independent of the command: > big.log empties a live log in place without deleting it, the standard ops move for shrinking a runaway file. Scripts that write per-iteration output inside a loop almost always want >>, and an accidental > inside a loop is how files end up holding only the last line.

What is a pipe, and when do you choose it over redirection?

Answer: A pipe feeds one command’s stdout into the next command’s stdin so tools compose: ps aux | grep nginx | wc -l. Redirection connects a command to a file instead. Choose a pipe when each stage transforms data in memory and streaming matters; choose redirection when the output is an artifact worth keeping. Both forms run stages as separate processes with separate exit statuses, which is exactly the nuance the intermediate tier builds on.

How does a for loop work in bash?

Answer: for f in *.log; do echo "$f"; done iterates over words: the shell expands the list first, then assigns each word to f in turn. The list can be a glob, an array, or "$@". The beginner trap: unquoting $f inside the body re-splits any filename containing spaces, the loop’s own expansion happens safely before the body runs. Quote the variable in the body, always. Loop fluency over globs and arrays is a fixture of shell scripting interview questions at every level.

How do you process a file line by line?

Answer: while IFS= read -r line; do printf '%s\n' "$line"; done < file. Every piece earns its place: IFS= stops trimming of leading and trailing whitespace, -r preserves backslashes, and redirecting the file into done instead of piping into the loop keeps it in the main shell so counters survive. A while read fed by a pipe runs in a subshell, the classic reason totals end at zero. That subshell surprise is a favorite follow-up in shell scripting interview questions.

What do case statements do, and when do they beat if/elif?

Answer: case $arg in -h) usage ;; start|stop) act ;; *) usage ;; esac matches one value against glob patterns top to bottom and runs the first branch that fits, with ;; closing each. They beat if/elif chains whenever a single variable is matched against many patterns: argument dispatch, file extensions, service names. Patterns are globs, so *.conf works without regex. Argument dispatch is exactly where a large share of practical interview questions land.

What is command substitution, and how does $(…) work?

Answer: $(cmd) runs cmd in a subshell and replaces the construct with its stdout, trailing newlines stripped: today=$(date +%F). Backticks do the same but nest poorly and collide visually with quotes, so modern scripts use $(...) exclusively. The substitution completes before the outer command runs. Because it runs in a subshell, assignments inside never leak out, which is the most common follow-up to this entry in shell scripting interview questions.

How does arithmetic work in bash?

Answer: $(( )) evaluates integer expressions: count=$((count + 1)), or (( total += 5 )) when you also want success as an exit status. Bash has no floating point; decimals need awk or bc. One gotcha earns follow-ups: leading zeros switch bash into octal, so $((08)) errors with a base complaint, and $((10#08)) forces base 10. Integer-only arithmetic plus that octal trap are both standard entries in this tier.

How do you define and call a function?

Answer: greet() { echo "hello, $1"; } defines it, and greet world calls it. Inside, $1 is the function’s own first argument, completely separate from the script’s. Definitions must appear before use because scripts execute top to bottom. Functions report status with return N, and any variable they assign is global unless declared local, which is the single most common function bug and a reliable follow-up in shell scripting interview questions.

What is the difference between return and exit?

Answer: return N ends a function with status N and the script continues; exit N ends the whole process. Calling exit inside a helper function kills the caller, which is deliberate in a main "$@" guard and a disaster in shared helpers. A function without return passes through the status of its last command, which can quietly mislead callers. Stating exactly which process ends is the sentence this question is fishing for.

What is a subshell, and why do variables set inside one disappear?

Answer: A subshell is a forked copy of the shell. Parentheses, command substitutions, every stage of a pipeline, and background jobs all create one. The child inherits a snapshot of variables, but its assignments die with it, so cat file | while read l; do n=$((n+1)); done leaves n at zero afterward. Keep the work in the main shell with redirection, done < file, or mapfile. Lost-assignment debugging is one of the most practical shell scripting interview questions for working engineers.

What does export do?

Answer: export VAR=value marks a variable for copying into the environment of every child process the shell starts; unexported variables are invisible to children, including scripts and tools you invoke. Inheritance is one-way, parent to child: a child that changes PATH or unsets things never changes its parent. That is why no script can cd or export its way into your interactive session. Inheritance direction is the mechanism behind several shell scripting interview questions about cron and subprocesses.

What do basename and dirname do?

Answer: basename /var/log/syslog.1 .1 prints the filename with the suffix stripped, and dirname /var/log/syslog prints the directory part. Both are external commands, so pure-bash equivalents run faster in loops: ${path##*/} for the name and ${path%/*} for the directory. Knowing both spellings, command and expansion, signals you have read real scripts, which is the quiet credibility that fluency awards.

How is grep used inside scripts?

Answer: grep -q pattern file prints nothing and communicates by exit status, which is exactly what if grep -q "$token" conf; then wants. Add -c to count, -v to invert, -F to treat the pattern as a fixed string so user data never meets the regex engine, and -r to recurse. The critical script behavior: grep exits 1 on no match, which under set -e kills the script unless it sits inside an if. That interplay is why grep anchors several shell scripting interview questions about failure handling.

What does sed ‘s/old/new/’ do?

Answer: It streams text and applies substitutions line by line: sed 's/foo/bar/' file replaces the first foo on every line, adding g replaces each occurrence, and -i edits files in place instead of printing to stdout. Anchor patterns with ^ and $, and quote the expression so the shell does not reinterpret it. Sed is where regex and shell quoting meet, which makes it a reliable precision checkpoint in shell scripting interview questions.

What does awk do at its core?

Answer: Awk runs a tiny program once per line: awk '{print $1}' prints the first whitespace-separated field of every line, and awk -F: '{print $NF}' prints the last field under a custom separator. Patterns before braces filter lines, NR is the line number, and $NF is always the last field. It replaces fragile cut-and-grep chains the moment you need conditions, arithmetic, or several fields at once. Field-model fluency here is what the intermediate tier probes next.

What is cut, and how does it differ from awk?

Answer: cut -d, -f2 file slices one delimiter-delimited column per line, cheap and fast, with -c for character ranges. Unlike awk it has no conditions, no arithmetic, and no field rearranging, and it keeps empty fields in their positions rather than collapsing them. Choose cut for a single clean column; reach for awk the moment logic appears. Knowing both and the boundary between them is the everyday-tool half of text-processing rounds.

What does sort | uniq -c | sort -rn actually do?

Answer: It is the counting idiom. sort groups identical lines together because uniq only compares neighbors, uniq -c collapses each run into one line prefixed by its count, and sort -rn orders numerically, descending, so the most frequent line lands on top. Every stage exists for a reason, and dropping the first sort breaks the count silently. Piping idioms like this recur in shell scripting interview questions because they test pipeline thinking rather than memorized flags.

What do head, tail, and wc give a script?

Answer: head -n 20 and tail -n 20 slice from either end, tail -f follows a growing file, and wc -l counts lines, the standard how-many primitive that closes a pipeline. Refinements worth naming: wc -l < file prints just the number without a filename, and tail -c +N skips leading bytes. Small tools, but counting and slicing are the verbs scripts use most, so they show up throughout shell scripting interview questions.

What is /dev/null for?

Answer: It is a device that discards everything written to it and returns end-of-file when read. cmd > /dev/null 2>&1 throws away all output, 2> /dev/null discards only errors, which is how scripts silence noisy commands under cron. The reverse trick, read var < /dev/null, produces an instant empty read to flush a prompt. Knowing it is a device rather than a file rounds out the streams picture that shell scripting interview questions build around redirection.

Why might a script not find a command that you can run interactively?

Answer: PATH differs between environments, and the shell searches it in order for every command word. Your interactive shell may have ~/.local/bin or a version manager’s directory prepended, while a script under cron or systemd inherits the bare default and calls the wrong tool or none. Fix scripts to use absolute paths for anything critical, or set PATH explicitly at the top. Environment inheritance is the root cause behind a whole family of practical shell scripting interview questions.

What do .bashrc and .bash_profile have to do with scripts?

Answer: Very little, and that is the lesson: startup files configure interactive shells, while scripts run non-interactive shells that never read them. That is why aliases do not exist in scripts, why PATH customizations vanish under cron, and why the honest fix is declaring what you need inside the script itself. A script should be self-contained, depending on nothing a login environment loaded. Self-containment is the principle this entry in shell scripting interview questions exists to confirm.

Intermediate shell scripting interview questions

This tier is where shell scripting interview questions stop testing vocabulary and start testing mechanism. Forty questions follow, and the pattern to internalize is component, then command, then caveat: name the thing that owns the behavior, the parser, the kernel, the cron daemon, then the command that exercises it, then the operational caveat. That three-beat answer is what moves you from operator to engineer in an interviewer’s notes.

What is the full order of expansions bash applies to a command line?

Answer: Brace expansion, tilde expansion, then parameter, arithmetic, and command substitution left to right, then word splitting, then pathname expansion, then quote removal. The two late stages are where scripts die: word splitting tears unquoted expansions apart on IFS characters, and globbing replaces what remains with file lists. Quoting an expansion removes it from both. Reciting that order unprompted, and pointing at which stage ate the filename, is one of the highest-signal moments in intermediate shell scripting interview questions.

What is word splitting, and what does IFS actually control?

Answer: After substitution, bash splits every unquoted expansion’s result on the characters in IFS, by default space, tab, and newline. IFS is just a shell variable, so code that changes it changes every later unquoted expansion, which is why careful scripts scope it per command: IFS= read -r line trims nothing for that one read. Filenames with spaces are the canonical casualty. Explaining IFS as configuration of the splitter rather than a property of the variable is the insight this question rewards.

What is pathname expansion, and how do you control it?

Answer: After word splitting, each word containing unquoted *, ?, or [ is treated as a pattern and replaced by matching filenames, or left literal when nothing matches, unless shopt -s nullglob makes empty matches vanish or failglob makes them error. set -f disables globbing for the remainder of the script, the guard for loops that must treat data literally. Globs run inside the shell with no fork, so they beat find for simple matches, a performance note interviewers like hearing.

Why is eval dangerous, and what replaces it?

Answer: Eval re-runs the parser on an already-expanded string, so any metacharacter in the data, semicolons, quotes, command substitutions, executes as syntax. eval "rm $list" with untrusted content is arbitrary code execution by construction. The replacements cover nearly everything: arrays hold argument lists, printf '%q' quotes data for later reuse, and bash -c 'script' name arg1 passes values as arguments instead of splicing them into code. Naming those alternatives is what the eval branch of shell scripting interview questions is actually scoring.

What do ${var:-x}, ${var:=x}, ${var:?msg}, and ${var:+x} do?

Answer: ${var:-x} expands to x when var is unset or empty; ${var:=x} also assigns x; ${var:?msg} prints msg and exits, the guard you want on required inputs like ${1:?usage: target required}; ${var:+x} expands only when var is set. All four distinguish set from unset without an if statement. A one-line guard at the top of a script replaces a page of validation, which is the practical punchline intermediate shell scripting interview questions look for here.

Which string operations does parameter expansion give you without spawning tools?

Answer: ${#s} length, ${s:2:5} substring, ${s#prefix} and ${s##*/} strip shortest or longest from the front, ${s%.ext} and ${s%%/*} from the back, ${s//a/b} replaces all occurrences, ${s,} and ${s^^} shift case. All parser work: no fork, no quoting hazards, no locale surprises for path surgery. Rewriting a basename-and-extension script using only these forms is the exercise hiding inside this question.

How do indexed arrays work in bash?

Answer: declare -a files, then files=(one two "three four"). Read with ${files[0]}, expand all with ${files[@]}, count with ${#files[@]}, append with files+=(more), list indices with ${!files[@]}. The one non-negotiable habit: double-quote on use, "${files[@]}", exactly like "$@", so elements with spaces stay whole. Storing file lists in arrays instead of strings is the single habit that prevents the space-in-filename disaster class, which is why arrays headline every collections discussion.

How do associative arrays work, and when do you reach for them?

Answer: declare -A seen creates a map keyed by strings: seen["$key"]=1 marks, ${seen[$key]:-} reads safely, ${!seen[@]} lists keys. Typical uses: deduplication, occurrence counting, marking processed items, and tiny lookup tables that replace grep-against-file patterns. They need bash 4 or newer, so macOS’s system bash 3.2 is a real portability constraint worth naming. Knowing both the uses and the version floor is the completeness this entry in shell scripting interview questions expects.

What do mapfile and readarray do?

Answer: They slurp stdin into an indexed array, one line per element: mapfile -t lines < file, with -t stripping trailing newlines. It replaces the while read loop when the whole file fits in memory, stays in the current shell so counters survive, and preserves blank lines that plain loops can mishandle. readarray is a synonym. For gigantic files a streaming loop still wins on memory, and offering that judgment is what this entry in shell scripting interview questions is after.

Beyond quoting, what separates $* from $@ at the mechanism level?

Answer: Unquoted, both are identical: one string re-split by IFS. Quoted, "$*" joins all parameters into a single string glued with the first IFS character, while "$@" expands to N separate, individually quoted parameters. The classic use is an error helper, die() { printf '%s\n' "$*" >&2; exit 1; }, where one message is the goal, versus forwarding "$@" untouched. IFS even changes what "$*" glues with. That asymmetry is a reliably deep cut in technical screens.

What does set -e actually do, and where does it refuse to fire?

Answer: It exits when a simple command returns nonzero, with documented exceptions: commands inside if, while, or until conditions; anything left of && or ||; commands prefixed with !; and every pipeline stage except the last. So if grep -q x f never triggers it and false | true passes unless pipefail is on. Strict mode is a safety net with holes, not a guarantee, and enumerating the holes precisely is exactly what this entry in shell scripting interview questions tests.

What does set -o pipefail change about a pipeline?

Answer: Without it, a pipeline’s status is its last command’s, so curl url | gzip > f.gz exits 0 even when curl died midstream and gzip compressed a truncated body. With pipefail, the pipeline returns the rightmost nonzero status, surfacing the real failure. The cost: expected-failure stages, grep finding nothing, now fail the pipeline, so guards like if grep -q or a PIPESTATUS check belong exactly where failure is normal. Stating both halves of that trade is what shell scripting interview questions on pipelines want.

Pipeline exit status: default vs pipefail default fetch-data exits 1 (failed) gzip exits 0 (ok) $? = 0 failure hidden pipefail fetch-data exits 1 (failed) gzip exits 0 (ok) $? = 1 failure surfaces With pipefail the pipeline returns the rightmost nonzero status; PIPESTATUS holds every stage’s code.
Without pipefail the last command decides the pipeline’s status; with it, any failed stage fails the whole pipeline.
What does set -u catch, and how do you live with it?

Answer: It turns unset-variable expansion from a silent empty string into an error, catching typos like $DSTINATION before they delete the wrong tree. Defaults stay available: ${var:-}, ${1:-} for optional arguments, and "$@" is safe even when empty on modern bash. The friction, genuinely optional variables needing explicit defaults, is a feature: every default becomes visible in the code. Eliminating the empty-versus-unset ambiguity is the safety story this entry in shell scripting interview questions wants told.

What do set -x and bash -x give you, and what does PS4 control?

Answer: They print each command after expansion, before execution, to stderr, so you see exactly what the parser produced: the fastest way to watch a quoting or splitting bug happen in real time. PS4='+ L$LINENO ' enriches the trace with line numbers and nesting depth. For targeted work, wrap one suspect region in set -x and set +x rather than tracing everything, since full traces on hot loops bury the signal. Trace-driven debugging recurs through interviews about scripts that misbehave only sometimes.

What is shellcheck, and what classes of bugs does it catch?

Answer: A static analyzer for shell scripts that flags unquoted expansions (SC2086), word splitting in command substitutions, useless cat pipes, wrong test operators, subshell assignment losses, and dozens more, each with an explanation and a fix. It catches the exact mistakes this page drills, before runtime, for free. Running shellcheck script.sh locally and in CI is the cheapest quality gate shell work has, and recommending it unprompted is one of the strongest signals you can send in shell scripting interview questions.

What is trap, and which conditions can a script trap?

Answer: trap 'commands' CONDITION registers code to run when the shell hits a signal or pseudo-signal: real signals INT, TERM, and HUP, plus the pseudo-signals EXIT on any exit, ERR after a failed command, and DEBUG before each command. Traps are how scripts get last words: cleanup, lock release, error lines with LINENO. SIGKILL cannot be trapped because the kernel kills the process without asking it, which is why lock design must never depend on traps alone. That impossibility is a favorite twist in shell scripting interview questions about cleanup.

How does a trap on EXIT make cleanup reliable?

Answer: trap cleanup EXIT fires when the script leaves, however it leaves: normal completion, explicit exit, a set -e abort, or a fatal INT or TERM, which bash converts into an exit with status 128 plus the signal number while still running the trap. One handler means cleanup logic exists once and cannot be skipped by a new early return added next quarter. For clean status codes, pair it with translations like trap 'exit 143' TERM. SIGKILL remains the one exit nothing can catch.

What are the DEBUG and ERR traps for?

Answer: trap 'cmd' DEBUG runs before every command, the hook behind poor-man’s tracers and timing overlays. trap 'printf "failed at line %s\n" "$LINENO" >&2' ERR runs after any simple command fails, even where set -e would not exit, which makes it a better error logger than -e is an exiter. Add set -E so the ERR trap also fires inside functions and subshells. ERR plus LINENO is the standard poor man’s stack trace and a well-loved deep cut in shell scripting interview questions.

How do you resolve a script’s own directory reliably?

Answer: script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd). $0 breaks when the script is sourced or invoked through PATH oddities, while ${BASH_SOURCE[0]} names the file the shell is actually reading. The cd plus pwd combination normalizes relative paths, after which the script can reference sibling config and library files regardless of the caller’s working directory. This one-liner is a compact quiz in parsing, sourcing, and subshells, which is why it recurs in platform-team interviews.

What is a here-document, and what do its variants do?

Answer: cmd <<EOF followed by lines and a terminating EOF feeds that text to the command’s stdin. An unquoted delimiter expands variables inside the body; <<'EOF' passes everything literally; <<-EOF strips leading tabs so indented code stays readable, tabs only, never spaces. Typical uses: multi-line config, SQL batches, and remote scripts piped over ssh. The classic bug is an unquoted delimiter swallowing dollar signs in content that was meant to be literal, a delimiter-quoting choice that shows up as a small but reliable probe in shell scripting interview questions.

What is process substitution, and when is it better than a temp file?

Answer: diff <(sort a.txt) <(sort b.txt) runs each command and hands it a virtual file under /dev/fd, cleaning up automatically: no named files, no trap needed, and the two sides even run concurrently. The >(cmd) form feeds output into a command, letting one stream fan out to several consumers. It requires bash rather than sh plus /dev/fd support, so it is not portable to minimal containers. Comparing two command outputs in one pass is the exact gap this entry in shell scripting interview questions is testing.

How do extra file descriptors work in bash?

Answer: exec 3< data.txt opens descriptor 3 for reading for the whole script, read -u 3 line consumes it one line at a time, and exec 3<&- closes it. Unlike a redirection on a single command, an exec-opened descriptor persists, which is how scripts interleave reads from two files or hold a lock file open for the process lifetime. Numbers above 2 are yours, though conventions like 200 dodge tools that borrow low descriptors. Persistent descriptors are the mechanism behind both multi-file readers and flock-based locking.

What is the safe pattern for temp files and directories?

Answer: tmpdir=$(mktemp -d) creates a directory whose name did not exist a moment before, with 0600 permissions, honoring TMPDIR, and trap 'rm -rf "$tmpdir"' EXIT guarantees removal on every exit path. Predictable names like /tmp/myapp.tmp are collision and symlink-attack liabilities on shared hosts. The pairing is atomic in the ways that matter: creation is race-free, cleanup is unconditional. Mktemp plus an EXIT trap is the canonical answer wherever temp files come up.

How does flock work for script locking?

Answer: exec 200>>job.lock; flock -n 200 || exit 0 takes an advisory lock on the open file description, held for the process lifetime; a second instance fails to take it and leaves quietly. Unlike touch-file locks, the kernel releases it when the process dies for any reason, so there is no stale-lock recovery code and no dependence on traps. flock -w 30 waits with a timeout instead of exiting. Kernel-managed locking is the entire answer, and explaining why stale locks become impossible is what earns the follow-up nod in shell scripting interview questions.

What does xargs add over a shell loop?

Answer: It batches stdin words into as few command invocations as argv allows, so find . -name '*.log' -print0 | xargs -0 -n 500 rm runs far fewer processes than a per-item loop and never hits the argument-list ceiling. -0 pairs with NUL delimiters to survive any filename, -P 8 parallelizes across cores, -I{} places input mid-command. The portability caveat: without GNU’s -r, xargs may run the command once even on empty input. Batch size, delimiter, and that empty-input quirk are the three facts any xargs question checks.

What are sed’s key mechanics beyond s/old/new/?

Answer: -n with a p command prints only matches, turning sed into a fast filter; addresses select lines by number, range, or pattern: 10,20d, /^BEGIN/,/^END/p, $d. -i edits in place, where GNU sed allows it bare but BSD sed demands a suffix argument, so the portable spelling is sed -i.bak 's/a/b/' f. Sed streams rather than loads files, which is why it stays the king of huge-log edits. Those three mechanics, filtering, addressing, and the portability split, are precisely what shell scripting interview questions probe on sed.

What is awk’s execution model?

Answer: Programs are pattern { action } pairs. Awk reads line by line, tests each pattern against the current line, runs matching actions, executes BEGIN before input and END after. Built-ins carry the load: $1 through $NF fields, NR line number, FS field separator, and associative arrays for grouping. awk '{c[$7]++} END {for (k in c) print c[k], k}' is an entire log analyzer in one line. Framing answers as pattern-action, not as a grep substitute, is what separates awk responses in shell scripting interview questions.

How does getopts work?

Answer: while getopts ":vo:" opt; do case $opt in v) verbose=1 ;; o) out=$OPTARG ;; \?) usage ;; esac; done walks flags, setting opt and OPTARG, with the o: in the spec declaring that -o takes a value. It handles combined flags like -vo file, stops at the first non-flag argument, and afterwards shift $((OPTIND-1)) leaves "$@" holding the positional operands. It is POSIX-portable and limited to short options. Knowing where OPTIND lives and what OPTERR suppresses are the follow-up depths worth knowing here.

When do you parse arguments with a while-case-shift loop instead of getopts?

Answer: Manual parsing handles long options, --output file, which getopts cannot, plus --opt=value forms: while [[ $# -gt 0 ]]; do case $1 in --output) out=$2; shift 2 ;; *) shift ;; esac; done. The obligations you take on: shift in every branch or the loop never ends, honor -- as the end-of-options marker, and implement your own help flag. Most modern scripts need this loop for long options, so both parsing styles are staples of shell scripting interview questions rather than rivals.

How does variable scoping work inside bash functions?

Answer: Bash uses dynamic scoping: a function sees all its callers’ variables, and any assignment it makes is global unless declared local. local i keeps a loop counter from leaking into the caller’s loops, and local works on anything declare accepts, including arrays. Recursion works, with locals allocated per call frame. Forgetting local is the classic state-corruption bug, a helper that silently overwrites the caller’s variable, and it is among the most common real-world findings in shell scripting interview questions about functions.

Which constructs spawn subshells, and what breaks as a result?

Answer: Parentheses, command substitution, every pipeline stage, background jobs, and process substitution each fork a shell copy. Consequences: assignments inside vanish, cd inside does not move the caller, traps declared inside fire at that subshell’s exit rather than the script’s, and $! plus wait become the coordination tools for background work. Recognizing which side of a pipe your code runs on explains a whole family of otherwise mysterious bugs, which is why this mechanism underlies so many intermediate questions.

What do shopt’s globstar, extglob, nullglob, and nocaseglob change?

Answer: globstar makes ** recurse directory trees, so for f in **/*.log replaces a find pipeline. extglob enables pattern lists like +(a|b) and !(pattern) for match-except logic. nullglob makes unmatched globs expand to nothing instead of a literal star that downstream commands then treat as a filename. nocaseglob matches case-insensitively. All are bash-isms enabled with shopt -s and invisible to sh, which is exactly the portability boundary this entry in shell scripting interview questions draws.

What is the safe way to build and run a command from stored parts?

Answer: Store the pieces in an array, cmd=(rsync -a --delete "$src/" "$dst/"), and run "${cmd[@]}": every element stays exactly one argument regardless of what characters it contains. Building a string and evaluating it forces word-splitting and quoting games that break on the first path with a space, and eval turns that breakage into execution. For dry-run logging, printf '%q ' "${cmd[@]}" prints a safely re-runnable form. Arrays over strings is the design habit these questions exist to install.

Why do careful scripts write IFS= read -r line, and what does each part defend against?

Answer: Bare read mangles input twice: it strips backslashes and treats them as continuation characters, and it trims leading and trailing IFS whitespace from the result. -r preserves backslashes; the per-command assignment IFS= preserves surrounding whitespace for that read only; -d '' switches the delimiter to NUL for filenames that contain newlines. One more guard belongs after the loop: a final line without a trailing newline is read but returns nonzero, so while read -r line || [[ -n $line ]] keeps it. Each piece kills one silent corruption, which is the whole lesson.

How does regex matching work inside [[ ]]?

Answer: [[ $value =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] tests the value against an extended regular expression and reports by exit status, with captured groups landing in ${BASH_REMATCH[1]} and onward. The regex must stay unquoted so it remains a pattern rather than a literal string, and complex expressions are best stored in a variable first, re='...'; [[ $v =~ $re ]], which sidesteps parser edge cases. It is bash-only, so portable scripts fall back to grep -E. That quoting rule is the part most candidates miss in shell scripting interview questions on regex.

How do you stop a command from hanging forever?

Answer: timeout 30s cmd runs it under a watchdog, sending TERM at the deadline and exiting 124 if it fired; -k 5s escalates to KILL when TERM is ignored, and --foreground matters under CI runners that reshuffle process groups. Inside scripts, read -t 5 var bounds interactive prompts, with a default assigned when the read times out. Unbounded curl or ssh calls inside scheduled jobs are the classic overnight hang, and naming watchdogs as the standard fix is what this entry in shell scripting interview questions expects.

What does the locale change in a script, and why do careful scripts set LC_ALL=C?

Answer: Sort order, the meaning of character ranges like [a-z], decimal points, and case conversion all consult the locale, so the same script sorts differently on two servers whose LANG differs. export LC_ALL=C pins byte-wise, stable, and faster behavior for machine-facing output and comparisons; human-facing formatting can deliberately keep the locale. Checking locale on both hosts is the first diagnostic when output differs between identical servers. Determinism across hosts is exactly what this entry in shell scripting interview questions is probing.

What does logger do for scripts?

Answer: logger -t mybackup -p user.notice "finished in 42s" writes to syslog or the journal under your tag, so scheduled scripts inherit timestamps, hostname, rotation, and central shipping instead of hand-rolling log files. Read it back with journalctl -t mybackup or grep against /var/log/syslog. Stdout from cron already becomes mail, but mail is not searchable history and grows unbounded. Logger gives scripts the same observability daemons enjoy, which is the upgrade this entry in shell scripting interview questions wants you to articulate.

What makes a script cron-safe?

Answer: Assume nothing from your login: set PATH explicitly, use absolute paths or cd to a known directory, redirect stdout and stderr somewhere real since cron mails leftovers, quote every expansion, avoid anything needing a TTY, and remember cron treats % specially in the command field, so escape it as \%. Add flock against overlap and logger for history. Minimal environment is the root cause in most failed-job stories, and it is the standard opener for the cron branch of shell scripting interview questions.

How do you find where a script spends its time?

Answer: Coarse first: time ./script.sh for the total, and the SECONDS builtin bracketed around phases for a section breakdown. Fine-grained: wrap suspect sections in time ( ... ) subshells, or run a timestamped trace with PS4='+ $(date +%s.%N) L$LINENO ' and read the gaps. Remember bash -x adds overhead, so confirm hot paths with tracing off. Delivering numbers per phase, and optimizing only what measurement convicts, is the maturity this entry in shell scripting interview questions rewards.

Scenario and troubleshooting shell scripting interview questions

This tier is how scenario rounds actually feel: one symptom sentence, then the interviewer goes quiet while you think out loud. For every item below, the graded skill is identical: state your order of checks and the reasoning between them, then act. Silence plus random typing loses to a spoken diagnostic plan almost every time, and that plan is what these shell scripting interview questions are really measuring.

Your backup script runs perfectly when you execute it by hand, but fails every night under cron. Walk me through your diagnosis.

Answer: Assume the environments differ and prove where, in order. First, see the real error: cron already mails output, and adding >> /var/log/backup.log 2>&1 captures it permanently. Second, diff the environments: cron’s PATH is usually /usr/bin:/bin, cwd is the user’s home, TERM is unset, and none of your profile ran. Third, reproduce interactively under that same stripped environment with env -i PATH=/usr/bin:/bin /bin/bash script.sh, which turns a nightly mystery into an instant one. Fourth, fix by making the script self-contained: absolute paths or an explicit PATH line, flock for overlap, logger for history. This environment-diff sequence is the single most common scenario in shell scripting interview questions, and naming the repro command is what makes the answer senior.

Same script, two environments backup.sh your shell PATH: full and personalized cwd: wherever you stand stdout: your terminal aliases and profile vars loaded cron PATH: /usr/bin:/bin cwd: the user’s home output: mailed to the user no TTY, no aliases, % is special Failures start where the two columns differ; env -i reproduces the right-hand side.
A script inherits everything from its caller; cron provides almost nothing, which is why jobs pass by hand and fail on schedule.
A colleague ran rm -rf $dir where dir held “My Documents” with a space, and two unrelated directories vanished. Reconstruct the mechanism and the prevention.

Answer: Unquoted expansion happened before rm ever ran: $dir split on the space into two words, so the shell executed rm -rf My Documents, two arguments, two targets. The quoting decision belongs to the shell, not to rm. Prevention, in layers: double-quote every expansion, rm -rf "$dir"; enable set -u so a similar unset variable fails loudly instead of expanding empty; store file lists in arrays, never strings; and let shellcheck flag SC2086 before review does. Reconstructing the two-word argv is the graded half of this entry in shell scripting interview questions, because it proves you know the shell expands before the command sees anything.

A deploy script executed rm -rf “$TARGET/”* with TARGET empty, and it tried to delete from the filesystem root. What single guard class prevents this?

Answer: With TARGET empty, the quoted part contributed nothing and the unquoted /* globbed every top-level directory, so rm ran against the root with whatever permissions the job had. The guard class is fail-fast input validation at entry: set -u plus : "${TARGET:?TARGET must be set}" aborts before any command runs, and explicit checks like [[ -d $TARGET ]] or refusing empty-and-root paths for destructive operations close the rest. Also never let a variable own a glob; expand globs literally in code so an empty variable cannot become a wildcard. Validation before destruction is the entire lesson here.

A nightly pipeline, fetch-data | gzip > archive.gz, has been shipping truncated archives while monitoring stays green. Diagnose it.

Answer: Default pipeline semantics hid the failure: a pipeline’s exit status is its last command’s, so gzip’s 0 masked the fetcher dying midstream from a reset connection or expired credential, and gzip faithfully compressed whatever it received. Fix with set -o pipefail so the pipeline carries the failed stage’s status, and read ${PIPESTATUS[@]} when you need per-stage forensics after the fact. Guard expected-noise stages individually with an if rather than relaxing strictness globally, and add an independent integrity check, a size floor or checksum, because exit codes and data quality are different contracts. Both contracts failing at once is exactly what scenario shell scripting interview questions stage.

A queue-processing script is spinning at 100% CPU, producing no output and never finishing. Where do you look first?

Answer: Look at the loop condition before anything else, because the classic cause is a read whose EOF status is ignored: while true; do read line; do_work; done spins forever once stdin closes, since read returns nonzero at EOF but this shape never checks it. The correct form puts read in the condition, while IFS= read -r line; do ... done, which ends the loop the moment input exhausts. Confirm with bash -x or strace to see the read spam, fix the condition, then add a seatbelt: timeout around the job and a progress counter logged every N items. Unbounded input meeting an unchecked read is the mechanism, and it is a named favorite in shell scripting interview questions about runaway jobs.

After a crash last night, every rerun of the job exits immediately claiming another instance is running, and a .lock file from 2am sits there. Fix it properly.

Answer: The lock is a touch-file tested with if [ -f job.lock ], which survives crashes by design, so the state is stale and the code has no way to know. Migrate to kernel locking: exec 200>>job.lock; flock -n 200 || exit 0. The lock lives on the open file description, so the kernel releases it the instant the process dies, and the stale-lock class disappears rather than getting patched. If a file lock must stay, store the PID inside, verify liveness with kill -0, and treat a dead PID as stale. Removing the class instead of handling instances is the answer this lock scenario is fishing for.

Two instances of a report generator ran concurrently and the output file contains interleaved fragments of both. What failed, and what does the robust fix look like?

Answer: Two things: no mutual exclusion, and a shared, predictable output path written in place. Fix in layers. First, exclusion: flock on a lock descriptor so the second instance waits or exits. Second, atomic publication: write to a mktemp name in the same directory, then mv it over the final name, so readers only ever see a complete file, since rename is atomic on the same filesystem. Third, unique temp names per run, which mktemp provides, so even bypassed locking cannot collide. Optionally set -o noclobber as a tripwire. Lock, then publish atomically, in that order, and say the order out loud.

The same reporting script produces differently sorted output on two servers. Both run bash. Where do you look?

Answer: Locale first, because sort order is locale-dependent: servers with different LANG or LC_COLLATE order case, punctuation, and accents differently, so identical input yields different sequences. Confirm with locale on both hosts and a one-line printf 'a\nB\n' | sort comparison. Fix by pinning machine-facing scripts: export LC_ALL=C at the top gives byte-wise, stable, and faster sorting everywhere. Note the same variables affect [a-z] ranges in grep and case conversion, so pin early, not per command. Human-facing tools can keep their locale deliberately. Two-host divergence with identical code is a textbook locale story and a common twist in shell scripting interview questions.

A find | while read loop mangles every file whose name contains a newline. What is the correct construction?

Answer: Text streams cannot carry newline-bearing names unambiguously: find’s default print and read’s default delimiter both split on the newline character, so one file becomes two records. Switch the entire chain to NUL delimiters, the one byte filenames cannot contain: find . -print0 | while IFS= read -r -d '' f; do ...; done, or pipe into xargs -0. Never parse ls output in scripts; it is formatted for humans and breaks on exactly the names that hurt. Naming NUL as the only safe delimiter, and why, is the entire graded content of this question.

You added set -e to a deploy script and now it stops mid-run with zero explanation. Make failures loud.

Answer: set -e exits silently by design, so pair it with observability: set -E plus trap 'printf "FAILED at line %s: %s\n" "$LINENO" "$BASH_COMMAND" >&2' ERR, which prints the failing line and command wherever the failure lands, including inside functions and subshells that -E propagates into. Add a phase log, one line per stage, so the trace reads as a narrative in the cron mail. And remember -e’s holes: commands inside if conditions and every pipeline stage but the last never trigger it, which is exactly why the ERR trap, not -e, is your error telemetry. Silent exits becoming narrated failures is the upgrade this question wants described.

ssh host “deploy.sh $VERSION” worked for one engineer and no-op’d for another. What happened?

Answer: Quoting decided which side expanded the variable. Double quotes expand $VERSION locally before ssh ships the command, so the engineer without that variable in their shell sent the literal string deploy.sh with an empty argument. Single quotes ship the text untouched and the remote shell expands against its own environment. Diagnose by testing both, ssh host 'echo "$VERSION"' versus the double-quoted form, and inspect what the remote actually received in its logs. The durable rule: single-quote remote commands unless deliberate interpolation is intended, or pass values as arguments, ssh host 'deploy.sh' "$VERSION", so each side expands only what it owns.

A script parses its -v and -o flags fine on the first call, then a second call to the same parse function misparses. What broke?

Answer: OPTIND is a global that persists across function calls, so the second getopts loop resumes where the first stopped and sees nothing left to parse. Fix by making the state per-call: local OPTIND (and optionally local opt arg) at the top of the function, so every call restarts at argument one. Same bug family: forgetting shift $((OPTIND-1)) after the loop, so the caller’s positional arguments still contain flags, and reusing the same variables for stored options. Reproducing with two calls in one script, failing only on the second, is the tell, and knowing that getopts keeps state in a global is the fix’s justification.

The script works on your laptop and fails on the server with command not found for tools you know are installed. Give the diagnostic order.

Answer: First, print PATH from inside the failing environment, not yours: add echo "$PATH" >&2 early or run bash -x there. Second, compare against the tool’s real location with type -a or which; version-manager tools and anything in ~/.local/bin live in login-initialized PATH entries that scripts never see. Third, reproduce the minimal environment with env -i /bin/bash script.sh to confirm. Fourth, fix at the source: absolute paths for machine-critical binaries or an explicit PATH line at the top of the script, then run shellcheck and wire the script into CI so the stripped-PATH case is tested rather than remembered. Environment-first diagnosis is the order interviewers score.

Your sed -i cleanup works on Linux and breaks on macOS, leaving extra files or errors. Explain and fix.

Answer: BSD sed requires an explicit backup suffix after -i: sed -i '' 's/a/b/' f on macOS, where GNU sed interprets that empty argument as the script and the real expression as a filename, breaking both ways at once. The portable spelling is sed -i.bak 's/a/b/' f, which works on both and leaves .bak files you clean up deliberately, or sidestep entirely with perl -pi -e 's/a/b/' f. In mixed teams, detect the platform, case $(uname) in Darwin) and branch, or standardize on GNU coreutils via the package manager. Knowing both argument grammars, and that the difference is in how -i consumes its next argument, is the full answer.

A script’s log lines arrive minutes late and out of order when piped into the aggregator, though the terminal looks fine. What changed?

Answer: Buffering switched. When stdout is a terminal, libc line-buffers, flushing per line; into a pipe it block-buffers, holding kilobytes before flushing, so lines sit in memory and arrive late, and parallel producers make the arrival order look scrambled. Fix at the producers: stdbuf -oL cmd, grep --line-buffered, awk with an explicit fflush() after each print, or python -u. Ordering is actually preserved per stream; the delay just moved into the buffers, which is why the log looks reordered when producers interleave. Naming the tty-versus-pipe buffering switch is the mechanism this scenario grades, and stdbuf is the one-command fix interviewers want to hear.

A cleanup script deleted live data because the path it was given was a symlink pointing at the real dataset. What went wrong and how do you harden it?

Answer: The script trusted its input path, and operations along the way, globbing through it, cd into it, or an explicit realpath, resolved the symlink to the target, so rm -rf operated on real data. Harden in layers: resolve and inspect before acting, readlink -f "$path", and refuse if the resolved path is not the expected literal; check [[ ! -L $path ]] when symlinks are never legitimate; use find -xdev so destruction cannot cross into other filesystems; and require an explicit marker file inside anything a destructive script may touch. Destructive scripts verify identity, not just existence, and saying that sentence is the point of the question.

A counter that works for values 1 through 7 fails the moment input contains 08, with a value too great for base error. What is happening?

Answer: Arithmetic expansion treats leading-zero numbers as octal: $((08)) has no octal digit 8, so bash errors out and the whole expression fails. Fix with an explicit base, $((10#$n)), which forces decimal regardless of leading zeros, or strip the zeros before arithmetic. The same family bites elsewhere: percentages and money need decimals that $(( )) cannot represent, so route those through awk or bc rather than forcing integers. Data-driven numbers meeting a parser that guesses bases is the mechanism, and this exact error message is a favorite detail in shell scripting interview questions about arithmetic.

A helper function increments its own counter, but the caller’s loop counter mysteriously resets. Diagnose.

Answer: Missing local. Bash uses dynamic scoping: a function sees its caller’s variables, and any assignment it makes is global unless declared local, so a helper that reuses the name i or count silently overwrites the caller’s state. Confirm by printing the variable before and after the call, then grep the function for assignments without local. Fix by declaring every function-private variable, including loop counters and arrays, with local, and adopt a convention that generic names never appear undeclared inside functions. The bug is invisible until data loops get weird, which is precisely why scoping discipline is drilled in shell scripting interview questions about functions.

During an audit you find several scripts writing to /tmp/build.log, a fixed name. What are the risks and the fix?

Answer: Fixed names in a world-writable directory carry two risks: collisions, where two runs or two users interleave or clobber each other’s data, and symlink attacks, where a malicious local user pre-creates /tmp/build.log as a link to a victim’s file so the script writes through it. The fix is mktemp: tmpdir=$(mktemp -d) for working trees and tmp=$(mktemp) for single files, which allocates a name that did not exist a moment earlier, with 0600 permissions, then an EXIT trap removes it. Predictability is the vulnerability; random names plus restrictive permissions close both risks, and naming both risks is what full-credit answers to this scenario contain.

An import pipeline runs its loader exactly once even when the spool directory is empty, creating a bogus empty batch. Why?

Answer: Classic xargs semantics: without GNU’s -r flag, xargs invokes its command once even with zero input items, so the loader runs with an empty argument list and manufactures an empty batch. Fix with xargs -r on GNU systems, or portably guard the pipeline yourself: count first with find ... | wc -l or test a list file with -s before invoking the loader. Note the behavior differs across platforms, BSD xargs does not run on empty input, so CI on a Mac can diverge from a Linux server. Empty-input behavior is the whole question, and -r versus a count guard is the expected fix pair.

A script that draws progress bars with tput works for humans and fills the cron log with escape garbage. What is the principled fix?

Answer: Output should adapt to its destination. [[ -t 1 ]] tests whether stdout is a terminal: emit tput and ANSI sequences only when it is, and print plain single-line progress otherwise. The same rule drives --color=auto conventions in modern tools. Guard TERM explicitly too, since cron runs jobs without it and tput fails or misbehaves there. And route progress to stderr rather than stdout, so stdout stays pure data that pipelines and cron mail can consume. Destination-aware output is the principle, the -t test is the mechanism, and moving decoration to stderr is the design that ends this class of log pollution.

Your cleanup trap deleted files that a subshell still needed, because the trap fired earlier than you expected. Why?

Answer: Traps belong to one shell. If the main work ran inside a subshell, a pipeline stage, command substitution, or parentheses, any EXIT trap declared in that scope fires when the subshell exits, not when the script ends, and a trap declared inside a function fires at the function’s return in some arrangements. Restructure so the top-level script owns all trap declarations and subshells only produce output: collect results with stdout, command substitution, or temp files rather than sharing live state inward. Remember set -E propagates only the ERR trap, not EXIT, so an EXIT trap inherited-looking inside a pipe stage is really a local one. Trap scope equals shell scope, and stating that identity is the answer.

The shebang says #!/bin/sh and the script uses arrays and [[ ]]. It works on the dev box and explodes on the Alpine server. Diagnose and state the rule.

Answer: sh is a contract, not a nickname for bash. On the dev box /bin/sh is probably a symlink to bash, which tolerates the bash-isms; Alpine’s /bin/sh is busybox ash, which rejects arrays and [[ ]] with syntax errors at those lines. Diagnose with ls -l /bin/sh on each host and sh -n script.sh to catch the first non-POSIX construct without running anything. The rule: match interpreter to syntax. If you write bash, declare #!/usr/bin/env bash and install bash; if you declare sh, write POSIX, replacing arrays with positional parameters or case statements. Portability failures that only appear on one host are a staple of scenario shell scripting interview questions precisely because the shebang hid the truth.

A failed migration script shows green in CI. It printed an error, cleaned up, and exited 0. What design flaw is this, and what is the fix?

Answer: The script’s exit status is its last command’s, and cleanup ran last, succeeded, and erased the failure from the contract. Fix the plumbing: track the outcome explicitly, rc=0 at the top, rc=1 at each failure site, exit $rc as the final statement after cleanup, so the last command reports truth instead of destroying it. Reinforce with set -e plus an ERR trap so mid-script failures are impossible to forget, and have CI assert on artifacts, the new version string, row counts, not merely the exit code. An exit status is a promise to every caller, and treating it as a managed value rather than an accident is the entire lesson of this entry in shell scripting interview questions.

In CI the deploy script hangs forever at a prompt that never appears; locally it is interactive and fine. Diagnose.

Answer: In CI, stdin is not a terminal: read with no timeout blocks forever on an empty or closed stream, and commands that prompt, ssh host-key confirmation, sudo password, git credentials, wait for input nobody will give. Find the prompts with bash -x to see the last command before the hang. Fix each one: read -t 10 -r var with a default on timeout, input fed explicitly via redirection or here-strings, ssh -o BatchMode=yes and -o StrictHostKeyChecking=accept-new, DEBIAN_FRONTEND=noninteractive for package managers, and a guard at the top that fails fast when [[ -t 0 ]] is false in an unattended run. Assuming no human is watching is the posture this question rewards.

A backup manifest built as a string, files=”$files $f”, breaks when a filename contains *, ? or spaces. What is the failure and the correct structure?

Answer: A string list gets re-parsed every time it is used: the next expansion splits it on whitespace and re-globs it, so a file literally named *.log re-expands at use time into every log file present, and names with spaces become several entries. Strings cannot hold lists; structures can. Fix end to end with an array: files+=("$f") to append and "${files[@]}" to use, where every element stays exactly one item. For lists that must cross into other processes, write them NUL-delimited with printf '%s\0' or find -print0 and read back with read -d ''. The general law, lists that pass through strings get re-parsed, is what this question exists to teach.

Hands-on scenario labs

Reading answers builds vocabulary; building reflexes takes repetition, and the scenario tier of shell scripting interview questions is graded on reflexes. Each lab below is a safe, self-inflicted incident you can run in a throwaway VM or container. Do each three times: once with the checklist open, once from memory, once narrating out loud as if an interviewer were listening. That narration is exactly the rehearsal scenario rounds reward.

Lab 1: watch an unquoted variable destroy a directory

  • Build a safe sandbox: mkdir -p /tmp/lab1 && cd /tmp/lab1, then touch "report final.csv" notes.txt and mkdir "My Docs".
  • Commit the classic bug: f="report final.csv" followed by rm $f, and read the two errors carefully: the shell sent rm two arguments.
  • Run bash -x on a two-line script doing the same thing and watch the trace show the split happening before rm executes.
  • Fix it three ways: rm "$f", then [[ -f $f ]] guards, then shellcheck on the broken script to see SC2086 name the bug for you.
  • Finish by explaining out loud which two expansion stages an unquoted variable passes through and why quoting skips both.

Lab 2: build a cron-safe, locked, self-cleaning job

  • Start from the skeleton: #!/usr/bin/env bash, then set -euo pipefail, then an explicit PATH line as the first executable statement.
  • Add locking with exec 200>>/tmp/lab2.lock and flock -n 200 || exit 0, then launch two copies at once and confirm the second exits instantly.
  • Create scratch space with tmpdir=$(mktemp -d) and register trap 'rm -rf "$tmpdir"' EXIT, then force a failure mid-script and verify the directory is gone anyway.
  • Add trap 'exit 143' TERM, kill the running job, and observe both traps fire and the script still clean up after itself.
  • Schedule it under cron with output redirected to a log, then reproduce any failure with env -i PATH=/usr/bin:/bin /bin/bash script.sh before debugging live.

Lab 3: catch a swallowed pipeline failure and lint it away

  • Write false | true in a script and confirm it exits 0, then read ${PIPESTATUS[@]} immediately after to expose the per-stage truth.
  • Enable set -o pipefail and re-run: the script now fails loudly, and the failure points at the real stage.
  • Break it again the subtle way: grep missing /etc/hostname | wc -l under pipefail, and fix it with an explicit if grep -q so expected no-matches stay expected.
  • Run shellcheck over all three versions and read every SC code it reports before silencing anything.
  • Close the loop by adding bash -n and shellcheck as CI steps, so the next swallowed failure is caught before a human ever sees it.

Cheat sheet: the commands behind the answers

Nearly every item in this bank of shell scripting interview questions resolves to one of the constructs below, in the same order you would reach for them live. Learn the table, then learn one sentence per row about what can go wrong without it, and any scenario round becomes a checklist instead of an improvisation.

Command or constructWhat it tells you or buys you
set -euo pipefailStrict mode: exit on errors, reject unset variables, fail failed pipelines
bash -n script.shSyntax check without executing a single command
shellcheck script.shStatic analysis; the cheapest bug filter shell work has
bash -x script.shTrace every expanded command; watch the parser’s output, not your intent
trap 'rm -rf "$tmp"' EXITCleanup guaranteed on every exit path, including errors and signals
trap 'exit 143' TERMTranslate signals into explicit exit codes a supervisor can read
tmp=$(mktemp -d)Collision-free, permission-safe scratch space; never fixed /tmp names
exec 200>j.lock; flock -n 200Kernel-held lock that dies with the process, no stale files possible
: "${1:?missing target}"Fail fast with a message when a required argument is absent
arr=(a b c); "${arr[@]}"Argument lists that survive spaces, globs, and every weird filename
mapfile -t lines < fileFile into an array in the current shell, no subshell, no lost counters
IFS= read -r lineFaithful line reads: no trimming, no backslash damage
printf '%s\n' "$var"Output that never mangles its own data, portable everywhere
grep -q pattern fileMatch testing by exit code, silence by design, safe under set -e
${PIPESTATUS[@]}Per-stage exit codes of the last pipeline, for forensic reads
sed -i.bak 's/a/b/' fIn-place edit that works the same on GNU and BSD sed
awk -F: '{print $NF}'Last field under a custom separator, with logic available when needed
sort | uniq -c | sort -rnThe counting idiom: group, collapse, rank
find ... -print0 | xargs -0 -n 500Safe batching for any filename, no argument-list ceiling
timeout 30s cmdBounded runtime, exit 124 on deadline, -k escalation for ignorers
stdbuf -oL cmdLine-buffered output into pipes; fixes lagging and interleaved logs
LC_ALL=C sortDeterministic byte-order sorting, identical on every host
[[ -t 1 ]]Is stdout a terminal? The gate for progress bars and color
logger -t myjob "done in 42s"Script logs into syslog/journal with timestamps, host, and rotation

Authoritative details for every construct live in the Bash Reference Manual, and shellcheck.net explains every warning class the tool can raise, including the SC codes named throughout this page.

Key takeaways and FAQ

The through-line of every tier above: shell scripting interview questions reward people who can pair a command with the component that owns its behavior. The command alone reads as memorization; the mechanism alone reads as theory; together they read as someone whose scripts have run unattended at 3am and come home clean. Keep that pairing in every answer you rehearse, and this bank becomes a script for the whole loop.

How long should I prepare for shell scripting interview questions?

With daily shell use, a focused week: two days speaking the beginner tier aloud, two on the intermediate mechanisms, one on scenarios, the weekend on the labs. Without that daily use, plan three to four weeks and type every construct yourself once, because fingers remember what eyes skim, and quoting rules in particular only become reflexes after you have watched them fail.

Are shell scripting interview questions different for DevOps and SRE roles?

The beginner and intermediate tiers are identical; the scenario emphasis moves. DevOps loops lean toward cron, CI integration, and deployment glue, while SRE loops add locking, idempotency, signal handling, and observability. Prepare the full bank either way: the panel decides which half it owns only after you are in the room.

Do I need to memorize awk and sed for shell scripting interview questions?

You need working fluency, not encyclopedia recall: field splitting and pattern-action in awk, substitution and addressing in sed, and the honesty to say that is a job for a real parser when data is structured. Interviews probe whether you reach for the right tool and know its failure modes, not whether you can write a one-liner golf champion.

What is the single most common failure point in shell scripting interview questions?

Quoting and word splitting, without contest. The space-in-filename deletion, the empty variable that globbed a directory, and the argument that became three are all one mechanism wearing different symptoms. Master the expansion order and you convert the most common scenario questions into free points.

Should I learn bash only, or POSIX sh too?

Learn bash deeply, then learn which of your habits are bash-isms: arrays, [[ ]], mapfile, read -d ''. Interviewers ask for POSIX awareness because real fleets still run alpine and busybox images where sh is ash. Being able to say exactly which constructs are non-portable, and what replaces them, is worth more than writing everything twice.

What should I read after this page?

Continue with the Linux interview questions bank, since expansion, processes, and signals sit on the OS layer this page assumes, and the Git interview questions bank, since hooks and automation are where these scripts meet version control. Then browse the DevOps Prep hub for the rest of the series as it ships.

Shell Scripting Prep Quiz: Quoting, Pipes, and Traps

1 / 5

With default settings (no pipefail), what is the exit status of false | true?

2 / 5

trap 'rm -rf "$tmpdir"' EXIT gives guaranteed cleanup on every exit path, with one exception. Which one?

3 / 5

Which locking mechanism leaves no stale lock behind when the script is killed?

4 / 5

A cron job works when you run it by hand and fails every night on schedule. What do you compare first?

5 / 5

dir="My Reports"; rm -rf $dir executes. What actually happens?

0%