What Actually Comes Up When You Walk Into a Senior Shell Scripting Interview
I've sat on both sides of these interviews for over a decade. The questions that separate someone who has actually written production scripts from someone who has watched a few tutorials are surprisingly specific. Below is a breakdown of the topics that consistently come up, along with answers that reflect real experience rather than textbook definitions. These are not just trivia. Interviewers ask this because how you handle arguments and status codes directly affects whether your script breaks in production or stays robust. $* expands to all positional parameters as a single word when quoted. $@ expands each parameter as a separate quoted word. This matters enormously when you pass arguments to another command. Use "$@" in almost every case where you need to preserve spacing inside individual arguments.
$# gives the number of positional parameters. $$ is the process ID of the current shell. $! holds the PID of the most recently executed background pipeline. $_ (sometimes asked instead of $!) stores the last argument of the previous command. $? returns the exit status of the last executed command. This is critical for error handling. I once had a monitoring script fail silently for three days because someone used $* instead of "$@" when passing file paths with spaces to rsync. The command failed, but the script checked the wrong variable for status. It kept logging "success" while backups were corrupting.
2. How do you handle errors in a shell script?
Most junior candidates say "use set -e." That is only partially correct and misses the practical complications. set -e exits on any nonzero return, but it has well-known gotchas. It does not trigger on commands within if conditions, pipelines (unless you also use set -o pipefail), or commands in subshells unless explicitly enabled. Relying solely on set -e is how scripts silently miss failures. A more complete approach combines these flags:
set -e for immediate exit on failures
set -o pipefail so pipeline failures are caught
set -u to treat unset variables as errors For logging, wrap your main logic in a trap: trap 'echo "Script failed at line $LINENO with exit code $?"' ERR
This gives you a concrete failure point instead of the script dying without explanation. I added this pattern to a deployment script after spending four hours debugging a mid-night outage where the previous version had no visibility into which command failed. The trap caught it immediately on the next run.
Get the Full Details
3. What is the difference between single quotes, double quotes, and no quotes?
This question seems basic but reveals whether someone understands when expansion happens and when it does not. Single quotes preserve everything literally. No variable expansion, no command substitution, no special character interpretation. Double quotes allow variable expansion and command substitution but protect most special characters. No quotes leave everything vulnerable to word splitting and globbing, which is the most common source of bugs. The practical rule: always quote your variables unless you explicitly need unquoted behavior. Unquoted variables with spaces in them are the #1 cause of shell script failures in production environments I have seen.
4. How do you compare strings and numbers in bash?
String comparison uses == or != inside [[ ]] or [ ]. The test command with -eq or -ne is for integers only. Mixing these up causes runtime errors that are hard to trace in older scripts. For strings: [[ "$var1" == "$var2" ]]
[ "$var1" != "$var2" ]
For numbers: [ "$a" -eq "$b" ]
[ "$a" -ne "$b" ]
[ "$a" -gt "$b" ]
[ "$a" -lt "$b" ] Bash also supports (( )) for arithmetic comparison, which is cleaner for numeric operations. I prefer the (( )) syntax for anything involving math because it reads more like standard programming languages and avoids the string-to-number conversion edge cases that trip up beginners.
5. Explain how process substitution works and when to use it.
Process substitution uses <(...) or >( ...) to feed the output or input of a command as a file argument to another command. This is useful when a command requires a filename but you only have a stream of data. For example, comparing two sorted file streams without creating temporary files: diff <(sort file1)
(sort file2)
This replaces what used to require mktemp and careful cleanup. I use this pattern regularly in log analysis scripts where I need to merge or compare output from multiple commands. Without process substitution, those scripts become significantly more verbose and harder to maintain. One thing beginners often miss: process substitution creates /dev/fd entries, not actual temporary files. This makes them faster and avoids disk I/O overhead, but they have limited lifetime and disappear when the subshell completes.

6. How do you read a file line by line in a shell script?
The traditional approach uses a while loop with read: while IFS= read -r line; do
echo "$line"
done < file.txt The IFS= prevents leading and trailing whitespace from being stripped. The -r flag prevents backslash interpretation. Both are essential for arbitrary file content including paths with backslashes on Windows-formatted files.
An alternative with awk or grep is often faster for simple transformations, but the while loop gives you more control when you need conditional logic per line. I found that using awk for bulk transformations cut my processing time from about 45 minutes to roughly 3 minutes on a 200MB log file with complex conditional parsing.
7. What are the different types of loops in bash?
Bash supports for, while, and until loops. The for loop iterates over a list of values. The while loop continues while a condition is true. The until loop continues while a condition is false. For iterating over files, a for loop with globbing is concise: for file in /path/to/dir/*; do
[ -f "$file" ] || continue
& process "$file"
done
The [ -f "$file" ] check filters out directories. Without it, your script tries to process directory entries as files and generates confusing errors. This is another common mistake I see in code reviews of people new to shell scripting.
8. How do you create and use functions in shell scripts?
Functions are defined with the function name followed by parentheses or the function keyword. Arguments are passed positionally just like script arguments. Example: log_error() {
echo "ERROR: $1" >&2
}
log_error "Disk space critically low"
Functions can return values through the return statement (0-255 range for exit codes) or through global variables for returning strings or arrays. Using return for error codes and variables for data is the standard pattern. One advanced nuance: functions in bash run in the same shell process, unlike subshells created by piping. This means functions can modify variables in the parent scope. This is powerful but also a source of subtle bugs when someone assumes a function runs in isolation.
9. How do you pass arrays in shell scripts?
Bash supports indexed and associative arrays. Indexed arrays are declared with parentheses and accessed with ${array[index]}. Associative arrays use declare -A and support string keys. Passing arrays between functions requires care because arrays are not passed by value. You typically either export them, use namerefs (bash 4.3+), or restructure your code to avoid the need for array passing. I encountered a situation where a complex configuration parser needed to pass multiple arrays between functions. The nameref approach with local -n solved the problem cleanly and avoided the copy overhead of working with flattened string representations.
10. What is the shebang and why does it matter?
The shebang (#!/bin/bash or #!/usr/bin/env bash) tells the operating system which interpreter to use when executing the script. It only matters when the script is executed directly, not when sourced or piped. Using /usr/bin/env bash is generally preferred over /bin/bash because it respects the user PATH and works across different systems where bash might be installed in different locations. Scripts without a shebang default to the user's current shell, which introduces unpredictable behavior in automated environments. I once inherited a script collection where half the scripts lacked shebangs. In a CI/CD environment switching between dash and bash, this caused inconsistent behavior that was nearly impossible to debug without adding explicit shebangs to every file.
11. How do you schedule and manage background jobs?
Bash provides job control with &, bg, fg, and jobs commands. For persistent scheduling, cron is the standard tool. For more complex workflows, systemd timers or at provide additional options. When running background processes, always redirect stdout and stderr. Unredirected background processes fill up log directories or get interrupted by terminal hangups. Using nohup or disown prevents SIGHUP from killing the process when the terminal closes. A practical pattern I use for long-running background tasks:
nohup bash script.sh > script.log 2>&1 & disown The 2>&1 redirects stderr to stdout before the & backgrounds the process. This keeps all output in one stream for easier monitoring.

12. Explain the difference between source and execute when running a shell script.
Executing a script creates a new subprocess. Variables, functions, and environment changes do not affect the calling shell. Sourcing runs the script in the current shell context, modifying the current environment directly. Use source (or the . command) for configuration files and environment setup. Use execution for standalone scripts that should not leak their variables into your current session. This distinction matters especially in interview scenarios involving .bashrc, .profile, and similar dotfiles. Candidates who confuse these two concepts tend to write scripts that break when moved between environments because they make incorrect assumptions about where variables are defined.
13. How do you handle large outputs and pipes efficiently?
Pipelines create a new process for each command, which has overhead. For simple transformations, built-in bash features or awk are faster than chaining multiple grep and cut commands. Additionally, pipe buffering can cause unexpected behavior. Commands may not flush output in real-time, which appears as lag or deadlock in monitoring scripts. Setting the buffer size with stdbuf or using unbuffered I/O modes resolves this. In one production migration script, switching from a chain of five pipes to a single awk program reduced execution time from 12 minutes to under 90 seconds. The overhead of spawning eight processes per line was the hidden cost that nobody had measured.
14. What are common security considerations for shell scripts?
Input validation is the first concern. Never trust arguments from external sources without sanitization. Use strict mode flags, validate file paths, and avoid eval whenever possible. File permissions matter too. Scripts that write to shared directories or handle sensitive data need careful permission management. Running scripts with elevated privileges without proper validation is a frequent attack vector. I once found a backup script that constructed file paths from user input without any path traversal checks. The script blindly trusted input and could overwrite arbitrary files on the system. Adding a simple realpath check and validating against an allowlist fixed the vulnerability, but the initial code review should have caught it.
15. How do you debug a shell script that is behaving unexpectedly?
Bash provides several debugging tools. Running with bash -x prints each command as it executes, showing variable expansion. The -v flag shows raw input, which is useful for spotting syntax issues. The -n flag performs a syntax check without execution. For interactive debugging, set -x within the script at specific points rather than running the entire script in debug mode. This reduces output noise and helps isolate the problematic section. Beyond built-in debugging, structured logging with timestamps and severity levels makes post-mortem analysis significantly faster. I switched our team's scripts from echo statements to a dedicated logging function that writes to structured log files, and average debugging time for reported issues dropped from several hours to under thirty minutes.
These questions cover the topics that separate casual script writers from engineers who maintain complex shell infrastructure. The answers are straightforward if you have actually written and maintained production scripts. Theory alone does not prepare you for the edge cases that come up in real interviews.
