BASH | Bash Cookbook |
Using Variables in Bash
Here is the truth about Bash variables: they are all strings, and everything interesting is about when those strings get split, expanded or left alone. Master expansion and the rest of the shell starts to behave.
What it is, in plain words
A variable in Bash is a name bound to a string value (arrays are the structured exception). You assign with =, no spaces:
name=vikix
count=3
os="Void Linux"
You read with $name or, safer, ${name}. Environment variables (PATH, HOME, EDITOR) are just variables marked for export to child processes.
echo "$name"
echo "${name}-box"
export EDITOR=nvim
Why it matters
Almost every Bash bug is an expansion bug: a missing quote that splits a filename with spaces, an unquoted $@ that mangles arguments, a $var that was empty and deleted a flag. Knowing the rules means writing scripts that survive real-world input.
An everyday analogy: mail merge
A variable is a blank in a form letter. Expansion is printing the letter with the blank filled in. Quoting is putting the filled-in value in an envelope so the post office (the shell) doesn’t tear it into separate words at every space.
flowchart LR
A["\$name"] --> B[expand to value]
B --> C{quoted?}
C -- '"..."' --> D[one word]
C -- unquoted --> E[split on IFS]A hands-on example
# Basics
greeting="hello"
echo "$greeting world"
# Default values
echo "${unset_var:-fallback}" # use fallback if unset/empty
echo "${unset_var:=fallback}" # also assign it
echo "${greeting:+set}" # expand if set
# Substrings and lengths
file="archive.tar.gz"
echo "${#file}" # length: 14
echo "${file%.gz}" # archive.tar
echo "${file%%.*}" # archive
echo "${file#*.}" # tar.gz
echo "${file##*.}" # gz
# Arrays (Bash)
hosts=(vikix pi.local "nas box")
echo "${hosts[0]}"
echo "${hosts[@]}" # all elements, correctly quoted
echo "${#hosts[@]}" # count
# Looping safely
for h in "${hosts[@]}"; do
echo "host: $h"
done
# Environment vs shell
FOO=bar # shell only
export FOO # also children
env | grep '^FOO='
The quoting rules that matter:
| Form | Meaning |
|---|---|
"$var" | expand, keep as one word |
'$var' | literal $var, no expand |
$var | expand and split on IFS (usually space/tab/newline) |
"$@" | all arguments, each separately quoted – always use this |
"$*" | all arguments as one word |
# Dangerous
rm $file # if file="a b.txt", removes "a" and "b.txt"
# Safe
rm -- "$file"
Special variables: $0 script name, $1…$9 arguments, $# count, $? last exit status, $$ PID, $! last background PID, $_ last argument of previous command.
Integer and readonly attributes
declare -i n=5 # integer attribute
n=n+2 # arithmetic, still stored as string "7"
declare -r pi=3.14159 # readonly
# pi=2 # error
declare -a nums=(1 2 3) # indexed array
declare -A map=([os]=void [wm]=stump) # associative (Bash 4+)
echo "${map[os]}"
local inside a function makes a variable scoped to that call – use it by default so functions don’t leak names into the global namespace.
greet() {
local name=$1
echo "hi $name"
}
Export, env and child processes
A child process (another script, python, git) only sees variables that were exported. A common pattern for one-shot environment:
EDITOR=nvim crontab -e
env -i HOME="$HOME" PATH="/usr/bin:/bin" bash --noprofile --norc
The second starts a clean shell – useful for testing install scripts.
Common misconceptions
- “Spaces around
=are fine.”name = valueruns a command calledname. Usename=value. - “All variables need
export.” Only those children must see. - "
$@and$*are the same." Only when unquoted. Always"$@". - “Arrays are portable.” They’re Bash/ksh; POSIX
shdoesn’t have them. - “Integers are a type.”
declare -ihelps; arithmetic is still$(( … )). Everything remains a string underneath.
Indirection and namerefs
Sometimes you need a variable whose name is in another variable. Old scripts used eval, which is dangerous with untrusted input. Bash has two safer tools:
colour=red
name=colour
echo "${!name}" # indirect expansion: prints red
# A nameref: 'ref' becomes another name for whatever variable it is given
add_item() {
local -n list=$1 # list is now an alias for the caller's array
list+=("$2")
}
tools=(vim emacs)
add_item tools pil
echo "${tools[@]}" # vim emacs pil
Namerefs (declare -n, Bash 4.3+) are how you pass arrays to functions “by reference” without copying them.
Associative arrays: a dictionary in the shell
declare -A port=([ssh]=22 [http]=80 [https]=443)
port[gemini]=1965
for svc in "${!port[@]}"; do
printf '%-7s %s\n' "$svc" "${port[$svc]}"
done | sort
${!port[@]} gives the keys, ${port[@]} the values. Order is not guaranteed, which is why the output is piped through sort. Declaring with -A is required; without it Bash silently treats the subscripts as arithmetic and everything lands in index 0.
Seeing what is set
declare -p tools port # print variables in a re-usable form
set -o nounset # make unset variables an error (same as set -u)
printenv | sort | less # just the exported environment
declare -p is the best debugging tool for variables: it shows the type (-a, -A, -i, -r, -x) and the exact value with quoting, so invisible whitespace and empty elements become obvious. In PicoLisp the equivalent habit is (show 'Sym) or (pp 'Fun): ask the system what a name holds instead of guessing.
Connections
#!/bin/bash
echo "hello"
WORD='script'
echo "This is a $WORD"
echo "This ${WORD}ing a $WORD"
ENDING='ed'
echo "${WORD}$ENDING"
ENDING='s'
echo "${WORD}$ENDING"
ENDING='alicious'
echo "${WORD}$ENDING"
# Echo the exit status - notice 0 generally means success
echo ${?}
# User UID
echo ${UID}
# User name - notice the use of $() construct for a command substitution
# you could use `id -un` also
echo $(id -un)
