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

name=alice           # NO spaces around =
greeting="hello $name"
empty=               # legal: assigns the empty string
  • No spaces around =. name = alice runs the command name with args = and alice — a classic beginner trap.
  • LHS is the bare name (no $); RHS uses $var to 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 n gives 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=1 stays 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 var removes a variable; unset -f fn removes a function.
  • readonly is one-way — can't unset, can't reassign. Only escape is starting a new shell.
  • declare (synonym typeset) 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
  • export doesn't copy — it sets a flag on the variable so the shell hands it to execve() when launching children.
  • unset LOG removes 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) runs cmd in 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 $10 parses as $1 followed by literal 0).
  • $# — number of positional args (does not count $0).
  • shift n — drop the first n args, renumber the rest. shift alone = 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: $0 inside a sourced file is the parent shell's $0, not the file. Use ${BASH_SOURCE[0]} to get the file path.

8. References