Variables in Bash

A variable in Bash is a named slot of memory that holds a string of text — a filename, a number, a whole command’s output, or a flag that controls how your script behaves. Variables let you avoid repeating the same value everywhere, capture the result of a command so you can reuse it, and make scripts configurable without editing their code. Nearly every script beyond a single command uses variables, so understanding exactly how Bash stores, expands, and scopes them is essential before you write anything more advanced.

Overview: How Variables Work

You create a variable simply by assigning it: name="value". There is no type declaration and no keyword required — Bash creates the variable on first assignment. The one strict rule is that there can be no spaces around the equals sign. name = "value" is not an assignment at all; Bash parses it as a command named name being run with two arguments, = and value.

Bash variables are untyped: internally, every ordinary variable is stored as a string, even if it looks like a number. When you write count=5, Bash stores the two characters 5, not a binary integer. Arithmetic only happens when you explicitly ask for it, using an arithmetic context like $((count + 1)), or by declaring the variable with the integer attribute (declare -i). Outside of an arithmetic context, + is just a character, so count + 1 would be treated as text, not math.

Variable names may contain letters, digits, and underscores, and cannot start with a digit. Names are case-sensitive. By convention, environment variables and constants are written in UPPER_CASE (like PATH or HOME), while ordinary script-local variables are written in lower_case — this is a style convention only, not a rule the shell enforces.

To read a variable’s value, prefix its name with a dollar sign: $name or ${name}. The brace form ${name} exists to remove ambiguity — if you wrote $name_backup intending “the value of name, followed by the text _backup“, Bash would instead look for a variable literally called name_backup. Writing ${name}_backup makes the boundary explicit.

Where variables actually live

Under the hood, your interactive shell or running script keeps an internal table mapping variable names to their string values, held in the shell process’s own memory. That table is private to the shell — a child process (another program, or a script you run from this one) does not automatically see it. When you mark a variable with export, Bash flags it to be copied into the process’s environment block, a separate list of KEY=value strings. Every time the shell creates a new process via fork() and exec(), the kernel hands that environment block to the new process, which is why exported variables are visible to child programs and non-exported shell variables are not.

Command substitution, written $(command), actually forks a subshell, runs command in it, waits for it to finish, captures everything it wrote to standard output, strips any trailing newline, and substitutes that text into the surrounding command line before the outer command runs. This is why $(...) is relatively expensive compared to a plain variable read — it involves creating an entire new process.

Function scope in Bash is global by default: a variable set anywhere is visible everywhere, including inside functions, unless you explicitly declare it with local inside a function. A local variable exists only for the lifetime of that function call (and is visible to functions it calls), and disappears when the function returns, leaving any outer variable of the same name untouched.

Syntax

The general forms you’ll use constantly:

name="value"
export name="value"
readonly name="value"
unset name
local name="value"
declare -i name=5
declare -r name="value"
declare -x name="value"
"${name}"
"${name:-default}"
result=$(command)
Form Meaning
name=value Create or overwrite a variable. No spaces around =.
$name / ${name} Expand the variable’s value. Braces disambiguate the name’s boundary.
"$name" Quoted expansion — prevents word splitting and glob expansion of the value.
$(command) Command substitution — runs command and substitutes its stdout.
${name:-default} Use default if name is unset or empty, without changing name.
${name:=default} Same, but also assigns default to name if it was unset or empty.
export name=value Assign and mark the variable to be inherited by child processes.
readonly name Make an existing variable immutable for the rest of the shell’s life.
unset name Delete the variable entirely.
local name=value Inside a function only: create a variable scoped to that function call.
declare -i / -r / -x Attach the integer, readonly, or export attribute when declaring a variable.

Examples

Example 1: Basic assignment and expansion

#!/usr/bin/env bash
name="Ada"
greeting="Hello, $name!"
echo "$greeting"

Output:

Hello, Ada!

The first line assigns the string Ada to name. On the second line, Bash expands $name while building the string that gets assigned to greeting, so greeting ends up holding the literal text Hello, Ada! — expansion inside double quotes still happens, only word splitting and globbing are suppressed.

Example 2: Command substitution and arithmetic

#!/usr/bin/env bash
backup_dir="/var/backups/app_$(date +%Y%m%d)"
file_count=$(find /var/log -maxdepth 1 -type f | wc -l)

echo "Backup target: $backup_dir"
echo "Log files found: $file_count"

count=5
count=$((count + 1))
echo "New count: $count"

Output:

Backup target: /var/backups/app_20260804
Log files found: 12
New count: 6

$(date +%Y%m%d) runs in a subshell and its output (today’s date) is spliced directly into the string being assigned to backup_dir. file_count captures the output of a whole pipeline the same way. The last three lines show the difference between string context and arithmetic context: count=5 stores the string 5, but $((count + 1)) evaluates count numerically and produces 6.

Example 3: Export, function scope, and default values

#!/usr/bin/env bash
export APP_ENV="production"
port="${PORT:-8080}"

show_env() {
  local local_var="only visible inside this function"
  echo "APP_ENV inside function: $APP_ENV"
  echo "Port: $port"
  echo "$local_var"
}

show_env
echo "local_var outside function: '$local_var'"
bash -c 'echo "Child process sees APP_ENV=$APP_ENV"'

Output:

APP_ENV inside function: production
Port: 8080
only visible inside this function
local_var outside function: ''
Child process sees APP_ENV=production

This script ties several concepts together: an exported variable, a default-value expansion, and a function-local variable.

How It Works Step by Step

Walking through Example 3:

Line 2export APP_ENV="production" both assigns APP_ENV and flags it for inclusion in this shell’s environment block, so any process this script later launches will inherit it.

Line 3port="${PORT:-8080}" checks whether a variable named PORT is already set (for example, exported by whatever launched this script). Since no PORT variable exists here, the expansion falls back to the literal 8080, and that’s what gets assigned to port. PORT itself is never created or modified by this expansion.

Lines 5–9 define the function show_env but do not run it yet — a function definition just registers the function’s body under that name.

Line 11 — calling show_env runs the function body. Inside it, local local_var=... creates a variable that only exists for this function call; $APP_ENV and $port are visible here too, because ordinary variables are global unless declared local.

Line 12 — once show_env returns, its local variable is destroyed. Referencing $local_var here expands to an empty string, which is why the output shows empty quotes.

Line 13bash -c '...' launches an entirely new Bash process. That new process cannot see this shell’s internal variable table at all — it can only see the environment block the kernel handed it at exec() time. It prints APP_ENV=production only because APP_ENV was exported; had it been a plain assignment without export, the child would have printed an empty value.

Common Mistakes

Mistake 1: Spaces around the equals sign

VAR = "value"
echo $VAR

Output:

bash: VAR: command not found

Bash parses VAR = "value" as a request to run a command literally called VAR with the arguments = and value — not an assignment. Remove the spaces:

VAR="value"
echo "$VAR"

Mistake 2: Leaving a variable unquoted

file="my report.txt"
touch $file
ls

Output:

my  report.txt

Because $file is unquoted, Bash performs word splitting on its value before running touch, so touch receives two separate arguments, my and report.txt, and creates two files instead of the one intended. Quoting the expansion keeps the value intact:

file="my report.txt"
touch "$file"
ls

Output:

my report.txt

Mistake 3: Forgetting arithmetic context

count=5
count=$count+1
echo "$count"

Output:

5+1

Without $(( )), $count+1 is plain string concatenation: the value of count followed by the literal characters +1. Wrap the expression in an arithmetic context to actually add:

count=5
count=$((count + 1))
echo "$count"

Output:

6

Best Practices

  • Always quote variable expansions — "$file", not $file — unless you specifically want word splitting or globbing to happen.
  • Use UPPER_CASE for exported/environment variables and constants, lower_case for ordinary script-local variables, so a reader can tell scope at a glance.
  • Declare function-only variables with local to avoid accidentally overwriting a global variable of the same name.
  • Use ${VAR:-default} to give scripts sane fallback behavior when a caller doesn’t set an expected variable.
  • Use readonly for values that should never change during the script’s run, such as a fixed configuration path.
  • Only export variables that a child process genuinely needs; keep everything else as a plain shell variable to avoid leaking values into programs you call.
  • Prefer $(command) over the older backtick syntax — it nests and reads more clearly.

Practice Exercises

1. Write a script called disk_report.sh that stores the current hostname in a variable using command substitution, stores today’s free disk space on / in another variable, and prints a one-line summary combining both. Hint: hostname and df -h / are the commands you need to capture.

2. Write a function greet that takes no arguments, uses a local variable for a greeting message, and prints it. Call the function, then try printing that same variable name outside the function and confirm it’s empty.

3. Write a script that reads an optional environment variable LOG_LEVEL with ${LOG_LEVEL:-info}, prints the resolved value, then run the script twice: once normally, and once as LOG_LEVEL=debug ./yourscript.sh, to see the default get overridden.

Summary

  • Assign with name="value" — no spaces around = — and read with $name or ${name}.
  • Bash variables are untyped strings by default; arithmetic only happens inside $(( )) or with declare -i.
  • Shell variables live in the shell’s own memory; only exported variables are copied into a child process’s environment at fork()/exec() time.
  • $(command) forks a subshell and substitutes its captured stdout into the command line.
  • Variables are global by default; use local inside functions to scope them to that call.
  • Always quote expansions to avoid word splitting and globbing on the value.
  • ${VAR:-default} and ${VAR:=default} supply fallback values without failing when a variable is unset.