Bash Variables
Master variable assignment, quoting rules, environment variables, export, and special variables like $0, $1, $#, $@, and $$.
Variable Assignment
Variables are the foundation of any script — they let you store values, avoid repetition, and write code that adapts to its inputs. In Bash, the assignment syntax has no spaces around =. This surprises people coming from other languages, but it is strict: a space turns the assignment into a command invocation.
# Correct
name="Alice"
age=30
greeting="Hello, World"
# Wrong — bash treats these as commands, not assignments
name = "Alice" # Error: name: command not found
age = 30 # Error: age: command not found
Variable names are case-sensitive. By convention:
- UPPERCASE for environment variables and constants
- lowercase for local script variables
DATABASE_URL="postgres://localhost/mydb" # environment/config constant
tmp_file="/tmp/work.$$" # local script variable (unique per run)
Reading Variables
Prefix the variable name with $ to read its value. Quoting is almost always the right choice — unquoted variables are split on whitespace and expanded as globs, which causes subtle and hard-to-debug failures.
name="Alice"
echo $name # Alice
echo "$name" # Alice (preferred — always quote)
echo "${name}" # Alice (brace syntax — required in some contexts)
# Concatenation using brace syntax
greeting="Hello, ${name}!"
echo "$greeting" # Hello, Alice!
Always quote your variable expansions. Unquoted variables undergo word splitting and glob expansion, which causes subtle bugs:
filename="my file.txt"
# Wrong — word splitting breaks this into two arguments
rm $filename # tries to remove "my" and "file.txt" separately
# Correct — the quotes preserve the filename as one unit
rm "$filename" # removes "my file.txt" as one argument
Brace Syntax ${}
Braces are required when you need to append text immediately after a variable name. Without them, Bash cannot tell where the variable name ends and the surrounding text begins.
prefix="pre"
echo "$prefixfix" # Empty — bash looks for variable named "prefixfix"
echo "${prefix}fix" # "prefix" — braces delimit the variable name
Braces also unlock parameter expansion features like default values, substring extraction, and find-and-replace (covered in the strings tutorial).
Quoting Rules
Bash has three types of quotes that behave differently. Choosing the wrong one is a common source of bugs, especially when dealing with secrets, paths, or user input.
# Double quotes: expand variables and command substitutions
name="World"
echo "Hello, $name" # Hello, World
echo "Today: $(date)" # Today: Wed Jan 1 10:00:00 UTC 2025
# Single quotes: literal — no expansion whatsoever
echo 'Hello, $name' # Hello, $name (dollar sign is literal)
echo 'Today: $(date)' # Today: $(date)
# Backticks: command substitution (old style — prefer $() for readability)
echo "Today: `date`" # Today: Wed Jan 1...
Use double quotes almost everywhere. Use single quotes when you need literal dollar signs or backslashes, such as when writing regex patterns or awk programs.
Environment Variables
Environment variables are a key mechanism for configuring software without hardcoding values. They are passed to every child process the shell spawns, which is why tools like curl, git, and docker read their configuration from them.
echo "$HOME" # /home/alice
echo "$USER" # alice
echo "$PATH" # /usr/local/bin:/usr/bin:/bin:...
echo "$SHELL" # /bin/bash
echo "$PWD" # /home/alice/projects
echo "$OLDPWD" # previous directory (used by cd -)
echo "$LANG" # en_US.UTF-8
echo "$TERM" # xterm-256color
View all environment variables:
env # all exported variables
printenv HOME # print a specific one
set # all variables (including shell-local ones)
export
export marks a variable so that child processes can see it. Without export, a variable exists only in the current shell — any command you run starts a new process and does not inherit it.
# Set without export — only visible in current shell
DB_PASS="secret"
bash -c 'echo $DB_PASS' # (empty — child process doesn't see it)
# Set with export — visible to all child processes
export DB_PASS="secret"
bash -c 'echo $DB_PASS' # secret
# Or export after assignment
API_KEY="abc123"
export API_KEY
In practice, scripts that need to pass values to subcommands — like database connection strings to your application — should use export.
unset
unset removes a variable entirely from the shell’s memory. This is useful for clearing sensitive values like passwords after they have been used, ensuring they do not linger in the environment.
name="Alice"
echo "$name" # Alice
unset name
echo "$name" # (empty)
# Unset an array
declare -a fruits=("apple" "banana")
unset fruits
readonly
readonly prevents a variable from being changed or unset for the remainder of the script. Use it for configuration constants that should never be overridden — if something tries to change them, the script will error out immediately rather than silently misbehaving.
readonly MAX_RETRIES=3
MAX_RETRIES=5 # Error: MAX_RETRIES: readonly variable
# Or use declare -r
declare -r LOG_DIR="/var/log/myapp"
Special Variables
Bash provides built-in special variables that give you information about the running script and its arguments. These are essential for writing scripts that accept input and report their own status correctly.
#!/usr/bin/env bash
# Run as: ./script.sh arg1 arg2 arg3
echo "$0" # Script name: ./script.sh
echo "$1" # First argument: arg1
echo "$2" # Second argument: arg2
echo "$3" # Third argument: arg3
echo "$#" # Number of arguments: 3
echo "$@" # All arguments as separate words: arg1 arg2 arg3
echo "$*" # All arguments joined as one string: arg1 arg2 arg3
echo "$$" # Current process ID (PID): 12345
echo "$!" # PID of last background process
echo "$?" # Exit code of last command (0 = success)
echo "$-" # Current shell options: himBHs
Practical use of $@
"$@" is the correct way to forward all arguments to another command. It preserves argument boundaries, including arguments that contain spaces.
#!/usr/bin/env bash
# Pass all arguments to another command, preserving spacing
process_files() {
for file in "$@"; do
echo "Processing: $file"
# "file with spaces.txt" is handled correctly as one argument
done
}
process_files "$@"
$? — Exit code checking
The exit code is how commands report success or failure. 0 means success; anything else means failure. Always check it when the result matters.
mkdir /tmp/testdir
if [[ $? -eq 0 ]]; then
echo "Directory created"
else
echo "Failed to create directory"
fi
# Cleaner equivalent — test the command directly in the if condition
if mkdir /tmp/testdir2; then
echo "Directory created"
fi
$$ — PID for unique filenames
The current process ID is guaranteed to be unique on the system, making it ideal for generating temporary filenames that won’t collide with other running instances of the same script.
# Create a temp file unique to this script run
TMP_FILE="/tmp/myscript.$$.tmp"
echo "working..." > "$TMP_FILE"
# ... do work ...
rm -f "$TMP_FILE"
Default Values
Parameter expansion provides a concise way to supply fallbacks for variables that might not be set. This is how well-written scripts handle optional configuration without crashing.
name="${1:-anonymous}" # Use "anonymous" if $1 is unset or empty
logfile="${LOG_FILE:-/tmp/app.log}" # fallback to default path
# Error out immediately if a required variable is missing
db="${DATABASE_URL:?DATABASE_URL must be set}"
# Assign a default if the variable is not already set
: "${CONFIG_DIR:=/etc/myapp}" # sets CONFIG_DIR if not already set
echo "$CONFIG_DIR"
Variable Scope
Bash variables are global within a script by default. This means a variable set inside a function is visible everywhere — which leads to hard-to-trace bugs when function internals accidentally overwrite script-level variables. Use local inside functions to restrict scope.
greeting="global"
say_hello() {
local greeting="local" # shadows the global; does not overwrite it
echo "Inside function: $greeting" # local
}
say_hello
echo "Outside function: $greeting" # global — unchanged
What’s Next
Now that you understand variables, the next tutorial covers Bash’s data types — strings, integers, arrays, and associative arrays.