Shell Variables and Parameters
- Description: Variable assignment, expansion, scope, environment vs shell-local, command substitution, and the special parameters (
$?,$$,$@,$*, etc.) - My Notion Note ID: K2A-E-2
- Created: 2020-06-03
- Updated: 2026-05-18
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Assignment and Expansion
- 2.
unset,readonly,declare - 3. Environment vs Shell-Local (
export) - 4. Command Substitution
- 5. Positional Parameters
- 6.
$*vs$@ - 7. Status and Process Parameters
- 8. References
1. Assignment and Expansion
name=alice # NO spaces around =
greeting="hello $name"
empty= # legal: assigns the empty string
- No spaces around
=.name = aliceruns the commandnamewith args=andalice— a classic beginner trap. - LHS is the bare name (no
$); RHS uses$varto expand other variables. - Use
${var}braces when the boundary is ambiguous:
file=report
echo "$file.txt" # works → report.txt
echo "${file}_v2.txt" # braces needed here
echo "$filename" # WRONG — looks for $filename, not $file + 'name'
- Bash is untyped: every variable is a string.
declare -i ngives it the integer attribute (arithmetic context auto-applies), but it's still stored as text. - Assignment vs export are different operations. A plain assignment
x=1stays inside the current shell — child processes don't see it. See §3.
2. unset, readonly, declare
unset name # remove the variable
readonly PI=3.14159 # value can never change; assignment is an error
declare -r MAX=100 # synonym of readonly within bash
declare -i count=0 # integer attribute
declare -a list # explicit indexed array
declare -A map # associative array (bash 4+)
declare -x PATH # mark as exported (= `export`)
unset -v varremoves a variable;unset -f fnremoves a function.readonlyis one-way — can't unset, can't reassign. Only escape is starting a new shell.declare(synonymtypeset) sets attributes; used together with the above.
3. Environment vs Shell-Local (export)
Two namespaces in one shell:
| Kind | Set with | Visible to children? | Listed by |
|---|---|---|---|
| Shell variable | x=1 |
No | set |
| Environment variable | export x=1 |
Yes | env, printenv, export -p |
LOG=verbose # shell-local
./child.sh # child can't see $LOG
export LOG=verbose # promoted to environment
./child.sh # child inherits LOG=verbose
LOG=verbose ./child.sh # one-shot: exports for THIS command only
exportdoesn't copy — it sets a flag on the variable so the shell hands it toexecve()when launching children.unset LOGremoves both forms.- Common environment variables:
PATH,HOME,USER,SHELL,PWD,OLDPWD,LANG,TERM,EDITOR.
4. Command Substitution
today=$(date +%Y-%m-%d) # preferred form
files=$(ls *.log | wc -l)
arch=`uname -m` # legacy backticks — works, but harder to nest
$(cmd)runscmdin a subshell, captures stdout (with trailing newlines stripped), substitutes the result.- Nests cleanly:
outer=$(echo $(date) and $(whoami)). Backticks need\escapes to nest. - Subshell — runs in a child process; variable assignments inside don't leak out:
x=before
echo $(x=after; echo "$x") # prints "after"
echo "$x" # still "before"
- Inside double quotes:
"$(cmd)"is one word with internal whitespace preserved. Unquoted, the result is word-split and glob-expanded — almost always a bug.
5. Positional Parameters
Inside a script (or function), $1, $2, ... refer to the arguments passed in:
# script.sh
echo "script name: $0"
echo "first arg: $1"
echo "tenth arg: ${10}" # braces REQUIRED for $10+
echo "arg count: $#"
./script.sh alpha beta gamma # $1=alpha, $2=beta, $3=gamma, $#=3, $0=./script.sh
$0— script path (or shell name in interactive bash).$1..$9— single-digit;${10},${11}, ... need braces (otherwise$10parses as$1followed by literal0).$#— number of positional args (does not count$0).shift n— drop the firstnargs, renumber the rest.shiftalone =shift 1.
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose) verbose=1 ;;
-o) output=$2; shift ;;
*) break ;;
esac
shift
done
6. $* vs $@
Both expand to "all positional parameters" — but the difference inside double quotes is load-bearing.
| Form | Result when called with a "b c" d |
|---|---|
$* (unquoted) |
a b c d — split on $IFS |
$@ (unquoted) |
a b c d — same as above |
"$*" |
"a b c d" — single string, joined by first char of $IFS (space by default) |
"$@" |
"a" "b c" "d" — preserves arg boundaries |
- Rule: always use
"$@". It is the only form that forwards arguments without mangling them.
# Correct argument forwarding:
log() { printf '[log] '; echo "$@"; }
log "$@" # passes our args through unchanged
# Bug: "b c" gets split into two args
log $*
"$*"is occasionally useful for joining args into a single string with a custom separator:IFS=, ; echo "$*".
7. Status and Process Parameters
| Var | Meaning |
|---|---|
$? |
Exit status of the most recent foreground command |
$$ |
PID of the current shell |
$! |
PID of the most recent background command (cmd &) |
$- |
Current shell option flags (e.g., himBHs) |
$_ |
Last argument of the previous command, or path of the running script at startup |
$0 |
Name of the script / shell |
$BASH_SOURCE |
Array; ${BASH_SOURCE[0]} is the current source file (works under source, unlike $0) |
$LINENO |
Line number of the currently executing command |
$PPID |
PID of the parent process |
$RANDOM |
Pseudo-random integer 0..32767 each read |
$SECONDS |
Seconds since shell started (or since last assignment) |
sleep 30 &
echo "background PID: $!"
# Idiom: locate the directory of the running script
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
- Pitfall:
$?resets after the next command. Capture immediately if you need it later:rc=$?. - Pitfall:
$0inside a sourced file is the parent shell's$0, not the file. Use${BASH_SOURCE[0]}to get the file path.