Bash
Bash as interviewers actually probe it: the expansion rules that decide what a command really receives, control flow and functions, the redirection and process plumbing that makes shell good glue, and the strict-mode and linting habits that separate a throwaway one-liner from a script you would put in cron. Nearly every backend, DevOps, and data role expects you to read and repair shell scripts.
on this pageshowhide
guide
overview
~1 minBash interviews rarely test whether you remember a flag. They test whether you can predict what a line will do: which arguments a command receives after the shell has rewritten it, which process an assignment happens in, and which exit status a condition is really checking. Junior rounds open on quoting and variables, because an unquoted expansion is behind most of the shell bugs people have shipped. Middle rounds move to the plumbing — redirection order, pipelines whose failures disappear, loops that lose their counters — and to the strict-mode line that opens most production scripts. Senior rounds are about scripts other people depend on: cleanup that survives a signal, a container entrypoint that lets the application shut down cleanly, input that a hostile user controls, and knowing when a job has outgrown a shell script. The hub follows those seams. [Variables and Expansion](/topics/lang-bash-variables-expansion) is everything between the text you type and the argument list: quoting, parameter expansion, arrays, splitting and globbing. [Control Flow and Loops](/topics/lang-bash-control-flow) is how a script decides and repeats when every condition is a command. [Functions](/topics/lang-bash-functions) adds arguments, scope and sourcing. [Pipes and Redirection](/topics/lang-bash-pipes-redirection) is the stream and descriptor layer, [Process and Job Control](/topics/lang-bash-process-control) is what forks and what the child inherits, and [Scripting Best Practices](/topics/lang-bash-scripting-practices) covers strict mode, linting, portability and running unattended. Learn expansion first and give it the most time; nearly every later answer turns on it. Control flow and functions come next, then redirection and process control together, since the "why did my variable vanish" questions sit exactly where those two meet. Leave best practices for last: strict mode and linter warnings make sense only once you know the bugs they guard against.
primer
### The shell rewrites the line before anything runs A command never sees what you typed. Bash first passes the line through a fixed sequence of expansions and hands the program whatever is left. Quoting is how you choose which of those stages touch a piece of text. Most bugs an interviewer puts in front of you are an expansion stage you did not mean to run. The habit they want to see is quoting every expansion unless you can say why not. ### Truth is an exit status `if`, `while`, `&&` and `||` do not evaluate expressions. They run commands and branch on whether those commands succeeded: zero is success, anything else is failure. `[`, `[[` and `((` are constructs whose whole job is to produce such a status. It also explains why a status checked one line too late is wrong: every command replaces the status of the one before it. ### Functions hand back a status and print their data A bash function has no return value in the sense other languages mean. `return` sets a small integer; real results travel on standard output and the caller captures them. That makes stdout a channel with a single purpose, and anything else a function prints, such as progress or warnings, belongs on stderr. Variables are global unless declared `local`, and even a local one is visible to every function called while it is in scope. ### Every stream is a descriptor you can move Standard input, output and error are descriptors 0, 1 and 2, and a redirection points one of them somewhere else before the command starts. The shell performs that work itself, in the order the redirections are written. That explains a file emptied before the command could read it, a `sudo` that cannot write where you asked, and error output landing in the wrong place. ### Know which process you are in Pipeline stages, `( … )` groups, command substitutions and background jobs run in child processes. A child starts with a copy of the shell's state, and whatever it changes dies with it; only exported variables reach the programs a shell starts. "My counter is zero after the loop" and "the child script sees an empty variable" are the same idea approached from two directions. ### Failures are quiet by default A script carries on after a command fails, a pipeline reports only its last stage, an unset variable expands to nothing, and a scheduled job's output goes to mail nobody reads. Strict mode, an EXIT `trap`, ShellCheck in CI and deliberate logging exist to make failure loud. Senior interviewers also expect you to know where strict mode leaks — inside conditions, behind `local`, across subshells — instead of treating it as a guarantee. ### Text stays data until something makes it code The result of an expansion is not parsed as shell syntax a second time, which is what makes a quoted variable safe to pass around. `eval`, `bash -c` with an interpolated string, and a filename that begins with a dash landing where a command expects options are the ways data turns back into instructions. That boundary is where shell security questions live.
- Exit status
- The integer from 0 to 255 a command leaves when it finishes; zero means success, and every bash conditional branches on it.
- Parameter expansion
- Replacing a variable reference with its value, optionally transformed on the way by an operator for defaults, length, substrings, prefix or suffix removal, or replacement.
- Word splitting
- The stage that breaks the result of an unquoted expansion into separate arguments wherever it finds a character listed in IFS.
- IFS
- The internal field separator variable: the characters word splitting and read break text on, by default space, tab and newline.
- Globbing
- Pathname expansion: replacing an unquoted word that contains wildcard characters with the sorted list of matching filenames.
- Positional parameters
- The arguments of a script or function, read by number as $1, $2 and so on, counted by $#, and expanded together by $@ or $*.
- Command substitution
- Running a command and inserting its standard output into the command line, written as a dollar sign and parentheses; trailing newlines are removed.
- Subshell
- A forked copy of the running shell that executes a group, a pipeline stage or a substitution; any state it changes is lost when it exits.
- File descriptor
- A small integer naming an open stream inside a process; 0, 1 and 2 are standard input, standard output and standard error.
- Exported variable
- A shell variable marked with export, which copies it into the environment of every program the shell starts from then on.
- trap
- The builtin that registers commands to run when the shell receives a signal or reaches a pseudo-event such as EXIT or ERR.
- Strict mode
- The informal name for setting errexit, nounset and pipefail at the top of a script so that failures stop it instead of passing silently.
- ShellCheck
- A static analyser for shell scripts that reports quoting mistakes, unsafe expansions and non-portable syntax before the script ever runs.
- Bashism
- A feature bash provides but POSIX sh does not, such as double-bracket tests or arrays; it breaks when the script is run by a plain sh.
The first five sections are layers of one pipeline. Expansion decides what a command receives; control flow decides whether it runs, from the statuses earlier commands left behind; functions package both, and because they can only hand back a status, they push real output through the same streams any command uses. [Pipes and Redirection](/topics/lang-bash-pipes-redirection) wires those streams together, and [Process and Job Control](/topics/lang-bash-process-control) says which of the connected pieces run in the shell itself and which in children — which is why a loop at the end of a pipe and a variable set inside parentheses lose their changes for the same reason. [Scripting Best Practices](/topics/lang-bash-scripting-practices) sits on top of all five. Of the strict-mode options, one is a statement about exit statuses, one about expansion and one about pipelines; `trap` belongs to process control; most ShellCheck warnings are expansion mistakes. A question about strict mode is usually a question about a lower layer, asked through its safety net. A short script where several of those ideas meet: ```bash #!/usr/bin/env bash set -euo pipefail log() { printf '%s\n' "$*" >&2; } oldest_backup() { local dir=$1 local -a files=("$dir"/backup-*.tar.gz) [[ -e ${files[0]} ]] || return 1 printf '%s\n' "${files[0]}" } if oldest=$(oldest_backup "${BACKUP_DIR:?}"); then log "pruning $oldest"; rm -- "$oldest" else log "nothing to prune" fi ``` The function reports whether it found anything through its status and says what it found on stdout, while `log` keeps messages on stderr so the capture holds only the path. The glob fills an array, so names with spaces stay whole, and because glob results come back sorted, dated names put the oldest first; the `-e` test catches the case where nothing matched and the pattern came back literally. `${BACKUP_DIR:?}` stops the script with a message when the variable is missing. The call sits in an `if` condition, so `set -e` leaves its failure for the script to handle instead of aborting, and `--` keeps a name that starts with a dash from being read as an option.
- Variables and Expansion →
Quoting, parameter expansion and word splitting decide what every command receives; most bugs shown in interviews begin here.
- Control Flow and Loops →
Conditions are commands and truth is an exit status; the test constructs, arithmetic and loops all build on that.
- Functions →
Arguments, local scope and the split between status and output are what turn a list of commands into a script.
- Pipes and Redirection →
Streams, descriptor order and pipeline status give exact, whiteboard-friendly questions that lean on the sections above.
- Process and Job Control →
Subshells, exports, traps and exec explain the vanished variables and leaked temp files interviewers like to show.
- Scripting Best Practices →
Strict mode, linting, portability and scheduled runs; senior rounds spend most of their time here and assume the other five.
Leaving an expansion unquoted because the test data had no spaces: real filenames and empty values then split, glob or vanish. See Quoting Rules.
Looping over the output of
lsto visit files: a plain glob already yields one item per file, whatever characters the names contain.Treating
set -eas error handling: it is ignored for commands tested byif,while,&&or||, and masked bylocalassignments, so failures still slip through.Piping a build into
teeorgrepin CI withoutpipefail: the pipeline reports its last stage, so a failed build looks green.Using
returnto hand back data: it carries only a status, and anything a function logs to stdout ends up inside the caller's captured value.Expecting a variable changed in a piped
whileloop or a parenthesised group to keep its value: that code ran in a child process.Removing temp files on the script's last line: an early exit, a strict-mode abort or a signal skips it; register the cleanup with a
trapon EXIT.Writing
#!/bin/shabove a script full of arrays and[[: where sh is not bash it fails. See POSIX Portability vs Bashisms.Feeding a string you did not build to
eval: the shell parses it as code, so whoever controls the value runs commands with the script's privileges.
This guide assumes bash 5.x, the version current Linux distributions ship. Most answers read the same on 4.x, but a few milestones still come up because older bash is still around: - **4.0** — associative arrays, `mapfile` (also spelled `readarray`), the `globstar` option for recursive `**` matching, and `coproc`. - **4.3** — namerefs through `declare -n`, and `wait -n` for waiting on whichever background job finishes next. - **4.4** — the `${var@Q}` family of transformations; this release also stopped `set -u` from treating an empty array expanded with `[@]` as an unset variable, a common reason strict-mode scripts failed on older systems. - **5.0** — `EPOCHSECONDS` and `EPOCHREALTIME`, which give a timestamp without starting a `date` process. The version question interviewers still ask is about macOS. Its `/bin/bash` is the 3.2 series, frozen because later releases moved to GPLv3, and the default interactive shell has been zsh since Catalina. A script that uses associative arrays or `mapfile` fails there under the system bash, which is why portable scripts either stay within 3.2 features, target POSIX sh, or say plainly that they need a newer bash on the `PATH`.
Bash is the GNU project's shell and is installed on most Linux servers, so it is the language of install scripts, CI steps, container entrypoints and cron jobs. Interviewers expect you to place it in three ways. **Against other shells.** POSIX sh is the portable subset every Unix-like system provides; on many distributions and in minimal container images, `/bin/sh` is a smaller shell than bash, and bash may not be installed at all. zsh and fish are interactive shells many people use day to day, but scripts are still usually written for bash or sh because those are the ones a server reliably has. **Against the tools it drives.** Bash is glue: the real work in a pipeline is done by coreutils, `grep`, `sed`, `awk`, `find`, `xargs` and, for JSON, `jq`. A strong answer knows when to hand text processing to `awk` rather than write a slow bash loop. **Against a general-purpose language.** Python is the usual next step once a script needs structured data, real error handling or unit tests; shell stays better at starting programs and connecting their streams. Around bash itself, ShellCheck lints, shfmt formats and Bats runs tests — naming them signals that you treat shell as code that deserves review.
explore
- Variables and Expansion39 questions
- Variables, declare and Special Parameters5 questions
- Parameter Expansion Operators5 questions
- Indexed Arrays5 questions
- Associative Arrays and Namerefs5 questions
- Quoting Rules5 questions
- Word Splitting and IFS5 questions
- Globbing and Pattern Matching5 questions
- Expansion Order and Brace Expansion4 questions
- Control Flow and Loops25 questions
- Exit Status and Boolean Operators5 questions
- test, [ ] and [[ ]]5 questions
- Arithmetic Evaluation5 questions
- case Statements4 questions
- Loops and Iteration6 questions
- Functions19 questions
- Defining Functions and Arguments5 questions
- Local Variables, Scope and Recursion5 questions
- Return Status vs Output4 questions
- Sourcing and Shell Libraries5 questions
- Pipes and Redirection23 questions
- Standard Streams and Basic Redirection5 questions
- File Descriptors and Duplication5 questions
- Here-Documents and Here-Strings4 questions
- Process Substitution4 questions
- Process and Job Control21 questions
- Subshells vs the Current Shell4 questions
- Command Substitution4 questions
- Background Jobs and wait4 questions
- Signals and trap4 questions
- exec and Environment Inheritance5 questions
- Scripting Best Practices38 questions
- Strict Mode and Error Handling5 questions
- ShellCheck, shfmt and Style5 questions
- POSIX Portability vs Bashisms4 questions
- Argument Parsing with getopts5 questions
- Debugging and Tracing Scripts5 questions
- Temp Files, Locking and Idempotency5 questions
- Handling Untrusted Input Safely4 questions
- Running Scripts from cron and systemd5 questions
- AI Red Teamingroleanchors this topic
- Cyber Security Expertroleanchors this topic
- DevOps / SRE Engineerroleanchors this topic
- DevSecOps Engineerroleanchors this topic
- MLOps Engineerroleanchors this topic
- Network Engineerroleanchors this topic
- PostgreSQL DBAroleanchors this topic
- Shell & Bashskillanchors this topic
- Linuxskill
questions
165 · 6 sectionsIn bash, how do you create an indexed array, append to it, read a single element and get the number of elements — and why does `echo $arr` print only the first element?
basics
~10 sCreate with arr=(a b c), append with arr+=(d), read one element as "${arr[0]}", count with "${#arr[@]}". A bare $arr is shorthand for ${arr[0]}, so it shows only the first element.
In a bash script, the line `count = 5` fails with `count: command not found`. Why does bash reject spaces around `=` in an assignment, and what are the correct forms when the value itself contains spaces?
basics
~20 sBash parses count = 5 as the command count with the arguments = and 5. An assignment must have no spaces around =: write count=5, and quote the value when it contains spaces, as in msg="hello world".
In bash pathname expansion (globbing), what do the metacharacters `*`, `?`, `[abc]` and `[[:digit:]]` match, and why is the glob `*.txt` not the same pattern as the regular expression `*.txt`?
basics
~20 sIn globbing, * matches any run of characters, ? exactly one character, and [abc] or [[:digit:]] one character from a set; the pattern must match a whole filename. In a regex, * instead repeats the preceding item.
With f=/var/log/app/archive.tar.gz in a bash script, what does each of ${f##*/}, ${f%/*}, ${f%.*} and ${f%%.*} expand to, and what is the rule behind # versus % and single versus double?
basics
~10 sThey give archive.tar.gz, /var/log/app, /var/log/app/archive.tar and /var/log/app/archive. In bash, # strips a matching prefix and % strips a matching suffix; doubling the character strips the longest match instead of the shortest.
In a bash script, what is the difference between wrapping text in single quotes and wrapping it in double quotes, and which shell behaviours does each one turn off?
basics
~20 sDouble quotes still expand variables and command substitutions while suppressing word splitting and filename globbing; single quotes suppress all of that as well, so every character between them is literal, including the dollar sign and the backslash.
In bash, what does `echo $(( 10 / 3 ))` print, and how do you compute a percentage or an average with a decimal point inside a bash script?
basics
~20 sIt prints 3. Bash arithmetic is integer-only, so division truncates toward zero and there is no fractional result. For decimals you call out to another tool — awk or bc — or scale the integers up before dividing and format the result with printf.
In a bash script you write `case "$1" in start|stop) A ;; *.log) B ;; *) C ;; esac`. How does bash compare the word to each pattern, and what happens when more than one pattern could match?
basics
~20 sBash compares the word against each pattern using shell glob rules, not regular expressions, and a pattern must match the whole word. Clauses are tested top to bottom and the first match wins; every later clause is skipped.
In bash, what value does the special parameter `$?` hold, which value means success, and why does inserting an `echo` between a command and its `$?` check break the check?
basics
~20 sIn bash, $? holds the exit status of the most recently completed foreground command: 0 means success and 1-255 mean some kind of failure. Every command resets it, including echo, so an intervening command overwrites the status you meant to test.
In a bash script, what do the test operators -e, -f, -d, -s and -r check about a path, and why can `[ -e "$p" ]` be true while `[ -f "$p" ]` is false for the same path?
basics
~20 sIn bash, -e means the path exists at all, -f means it is a regular file, -d means it is a directory, -s means it exists with a size above zero, and -r means the current user can read it. A directory exists, so -e is true and -f is false.
In bash, what is the difference between `$(( ))` and `(( ))`, and what does each one produce?
basics
~20 s$(( )) is arithmetic expansion: it substitutes the computed integer into the command line. (( )) is an arithmetic command: it evaluates the same expression, prints nothing, and returns exit status 0 when the result is non-zero and 1 when it is zero.
Inside a bash function, what do $1, $# and $0 refer to, and why must the tenth argument be written as ${10}?
basics
~20 sA bash function receives its own positional parameters: $1, $2 … are the function's arguments and $# is their count, shadowing the script's for the duration of the call. $0 is the exception — it still names the script. Write ${10}, because $10 parses as $1 followed by 0.
In bash, a variable assigned inside a function is still set after the function returns and can overwrite a caller's variable of the same name. Why does bash behave this way, and what makes a variable exist only for the duration of the function?
basics
~20 sBash variables are global by default, so an assignment inside a function writes into the one shell-wide namespace and survives the return. Declaring the name with local (or declare) inside the function creates a temporary copy that disappears when the function returns.
In bash, a function ends with `return 42`. What can the caller actually do with that 42, and how would you get a non-numeric result such as a hostname out of a function instead?
basics
~20 sIn bash, return only sets the function's exit status — a number 0-255 the caller reads in $? — never a value. To hand back real data, print it with echo or printf and capture it with result=$(func).
In bash, what is the difference between loading a script with `source setup.sh` (or `. setup.sh`) and running it as `./setup.sh`, and why does only one of them change the variables in your current shell?
basics
~20 ssource (and its POSIX synonym .) reads the file and runs its commands in the shell you are already in, so variables, functions and cd persist. ./setup.sh starts a separate shell that gets a copy of the environment and discards everything when it exits.
In a bash wrapper function that passes its arguments on to another command, what is the difference between forwarding them as "$@", as "$*", and as unquoted $@?
basics
~20 s"$@" expands to one word per argument, preserving spaces and empty arguments — it is the only correct way to forward. "$*" joins everything into a single word separated by IFS's first character. Unquoted $@ re-splits each argument on whitespace and globs the pieces.
In a bash script, what does the redirection in `echo "usage: deploy <env>" >&2` do to the echo command's file descriptors, and why write error messages that way?
basics
~20 sThe >&2 is shorthand for 1>&2: it makes echo's stdout a duplicate of file descriptor 2, so the message is written to stderr instead of stdout. Diagnostics belong on stderr so a caller can capture, pipe or discard the script's real output without losing them.
In bash, the pipeline `false | true` leaves `$?` at 0. Which command's exit status does a pipeline report by default, and why does that let a failing build pass silently in a CI step?
basics
~20 sBy default a bash pipeline reports only the exit status of its last command; every earlier stage's status is discarded. So false | true returns 0, and a failing build piped into tee or grep looks successful to CI.
In bash, running `mytool > out.log` still prints error messages to the terminal instead of putting them in out.log. Which stream does a bare `>` redirect, and how do you redirect the other one?
basics
~20 sA bare > redirects only standard output, file descriptor 1. Error messages travel on standard error, descriptor 2, which stays on the terminal until you redirect it explicitly with 2> file, 2>> file, or 2>/dev/null.
In a bash script, why does `cmd 2>&1 > out.log` behave differently from `cmd > out.log 2>&1`, and where does stderr end up in each case?
basics
~20 sBash applies redirections left to right, and each one copies where the target points at that moment. cmd > out.log 2>&1 puts stdout in the file and then makes stderr a copy of it, so both are captured. cmd 2>&1 > out.log copies stderr to the terminal first, then moves only stdout.
In a bash script, `ssh web01 <<EOF` ... `EOF` sends a block of commands to a remote host as that command's standard input. What changes if you write the opener as `ssh web01 <<'EOF'` instead, and which form do you want when the block references variables that exist only on the remote machine?
basics
~20 sQuoting the here-document delimiter (<<'EOF') sends the body through untouched; leaving it unquoted (<<EOF) makes the local shell expand $variables, $(...) and backticks in the body first. Use the quoted form for anything that must be evaluated on the remote host.
In a bash script, what does appending & to a command do, what does the special parameter $! hold afterwards, and what does a bare wait do?
basics
~20 sAppending & runs the command asynchronously in a background child process and returns immediately with status 0. $! then holds that child's PID. A bare wait blocks until every background child of the script has finished.
In a bash script, what is the difference between writing a command substitution as `$(cmd)` and as the older backtick form `` `cmd` ``, and why does nesting one inside another behave differently?
basics
~20 sBoth run a command and substitute its standard output, but $(...) nests and quotes cleanly because everything up to the matching ) is parsed as a fresh command, while backticks need a backslash escape for every nested level and mangle backslashes and inner quotes.
A bash script sets GREETING=hello and then runs ./child.sh, but child.sh prints an empty value for $GREETING. Why doesn't the child see it, and what makes a variable visible to child processes?
basics
~20 sOnly exported variables become part of a child process's environment. A plain assignment creates a shell variable that lives in the current shell alone; marking it with export copies it into every command the shell runs afterwards, including child.sh.
In a bash script, `trap cleanup EXIT` is the usual way to guarantee cleanup. Which ways of the script ending actually run that EXIT trap, and which ones do not?
basics
~20 sBash runs an EXIT trap whenever the shell itself decides to exit: the end of the script, any explicit exit, an abort under set -e, and after a fatal signal such as SIGTERM. SIGKILL leaves it unrun.
In a bash script, `count=0; cat ids.txt | while read -r id; do count=$((count+1)); done; echo "$count"` prints 0 however many lines the file has. Why does the count vanish, and how do you make it survive the loop?
basics
~20 sEvery stage of a bash pipeline runs in its own forked subshell, so the while loop increments a copy of count that dies when the pipeline finishes. Feed the loop by redirection instead of through a pipe.
A bash script fails only on the CI machine and you cannot reproduce it locally. What does `set -x` add to the script's output, where does that output go, and how do you enable it for one section only — or for a single run without editing the file at all?
basics
~20 sset -x makes bash print every command to stderr just before running it, after expansion, prefixed by PS4 (default "+ "). Wrap a region in set -x and set +x to scope it, or run bash -x script.sh to trace one run without touching the file.
A bash deploy script assembles a command string from an environment variable it did not set and runs it with eval "$cmd". What does eval do to that string, and what can whoever controls the variable achieve?
basics
~20 seval joins its arguments and hands the result back to the shell parser, so metacharacters inside the variable become syntax instead of data. Anyone who controls the value can run arbitrary commands as the script's user.
A bash script parses flags with `while getopts "o:v" opt; do ... done`, then reads its input file from `$1` — but when it is run as `script -v data.csv`, `$1` is `-v` instead of `data.csv`. What is OPTIND, and which line is missing after the loop?
basics
~20 sOPTIND is the index of the next argument getopts will examine, and getopts never alters the positional parameters itself. The missing line is shift $((OPTIND - 1)) after the loop, which drops the parsed options so operands start at $1.
One nightly cron job stopped working weeks ago and nobody noticed; another crontab line was "fixed" with `> /dev/null 2>&1` after it flooded an operator's mailbox. What does cron do with whatever a scheduled command prints, and how should a scheduled script's output be handled instead?
basics
~20 scron mails any output a job produces to the crontab owner, or to MAILTO if set, and mails nothing when the job prints nothing. Appending > /dev/null 2>&1 discards errors too, which is why failures go unnoticed.
A colleague asks why the CI pipeline runs ShellCheck over every shell script when the scripts already work in production. What class of bugs does ShellCheck actually find, how does it decide which shell dialect to apply, and what does its severity scale mean?
basics
~20 sShellCheck statically analyses shell scripts and flags quoting, expansion and exit-status mistakes that run without any error message yet behave wrongly. It grades findings as error, warning, info or style, and picks the dialect from the script's shebang.