Bash exposes a set of special variables the shell maintains for you — exit
status, process IDs, positional parameters, and more. Misreading them, or
forgetting that $? is overwritten immediately, causes subtle scripting bugs.
This tool is a searchable reference of every special and automatic variable with
its meaning, scope, and a usage example.
How it works
These variables fall into a few families:
- Status & control —
$?(last exit code),$-(current shell flags),PIPESTATUS(per-command status of the last pipeline). - Process IDs —
$$(shell PID),$BASHPID(current process PID),$!(PID of the last background job),$PPID(parent PID). - Positional parameters —
$0(script name),$1…$9,${10},$#(count),$@and$*(all args),$_(last arg of previous command). - Shell internals —
$IFS(field separator),$RANDOM,$SECONDS,$LINENO,$FUNCNAME,$BASH_SOURCE.
Worked example
#!/usr/bin/env bash
process() {
grep -q "$1" "$2" # may succeed or fail
local status=$? # capture immediately
if (( status != 0 )); then
echo "not found (exit $status)" >&2
fi
}
for arg in "$@"; do # quoted "$@" keeps each arg intact
process "$arg" config.txt
done
false | true
echo "${PIPESTATUS[0]}" # 1 — the failing 'false', not the pipeline's 0
Notes
- Always quote
"$@"in loops and when forwarding arguments; unquoted$@re-splits on whitespace and breaks filenames with spaces. $?is clobbered by the next command — including the test in anif. Save it to a local variable the moment you need it.$$stays constant across subshells (it is the main shell’s PID); reach for$BASHPIDwhen you genuinely need the current subshell’s PID.$RANDOMreturns a value 0–32767 and is not cryptographically secure; use/dev/urandomfor security-sensitive randomness.
Commonly misused variables
$@ versus $* in practice
The distinction between "$@" and "$*" only matters when they are double-quoted, but that is exactly when you use them — inside scripts where argument safety is important:
# Set IFS to comma to demonstrate $*
IFS=,
args=("first arg" "second arg")
set -- "${args[@]}"
echo "$@" # prints: first arg second arg (two arguments, each preserved)
echo "$*" # prints: first arg,second arg (joined on IFS character)
Use "$@" in virtually every case. The only time "$*" is useful is when you explicitly want to join all arguments into one string with a custom separator, and even then it is usually clearer to use printf '%s\n' "$@" | paste -sd, instead.
PIPESTATUS timing
PIPESTATUS is only valid immediately after a pipeline. The next command — including a variable assignment — overwrites it. Capture the whole array at once:
cmd1 | cmd2 | cmd3
statuses=("${PIPESTATUS[@]}") # capture before anything else runs
echo "cmd1: ${statuses[0]}, cmd2: ${statuses[1]}, cmd3: ${statuses[2]}"
$0 in scripts versus at the terminal
$0 is the name of the running script when inside a script, but at an interactive prompt it reports the shell itself (bash or -bash for a login shell). If your script relies on $0 to find its own path, use $(realpath "$0") or ${BASH_SOURCE[0]} — the latter works correctly even when the script is sourced rather than executed.
$LINENO in functions
$LINENO gives the current line number, but when used inside a function it reports the line within the function, not the overall script. Combined with $BASH_SOURCE and $FUNCNAME, it can build precise error messages: "Error at ${BASH_SOURCE[0]}:${LINENO} in ${FUNCNAME[0]}".