Structures#
Bash has only three data types: scalar strings, indexed arrays, and associative arrays. This page covers both the declaration syntax of each type (string, integer, array, associative array, plus the readonly, exported, case-folded, nameref, and local attributes) and the operational patterns the operator reaches for when those primitives meet real work.
Types#
Variables are untyped strings by default. declare (and its
function-scoped counterpart local) attach attributes that
change how a name behaves: integer arithmetic, array storage,
read-only protection, export to children, case folding, or
indirection. Multiple attributes can stack on the same name.
Type |
Declaration |
When to reach for it |
|---|---|---|
String |
|
Everything that fits in a single line of text. The fallback. |
Integer |
|
A name that should always hold an integer; assignment auto-evaluates arithmetic. |
Indexed array |
|
Ordered list, numeric subscripts. The bash equivalent of a list / vector. |
Associative array |
|
Key / value map with string keys. Requires Bash 4+. |
Readonly |
|
Constants and configuration the operator must not let a later line clobber. |
Exported |
|
Values that need to cross |
Lowercase / Uppercase |
|
Names that auto-fold on assignment; useful for normalizing input. |
Nameref |
|
A pointer to another name; the way to pass arrays or maps to functions by reference. |
Local |
|
Private to the function call; shadows any outer binding and is popped on return. |
String (default)#
The catch-all. Any unqualified assignment lands here. No quoting
restrictions other than the usual word-splitting rules, and string
expansion (${var^^}, ${var/pat/repl}) does the heavy
lifting elsewhere on the page.
$ name="operator"
$ greeting="hello, $name"
$ echo "$greeting"
hello, operator
Integer (declare -i)#
Assignments to an integer-attributed name are evaluated as
arithmetic, no $(( )) wrapper required. Useful for counters,
indices, and any name that should reject a typo of “twelve” instead
of 12. Non-numeric assignments quietly become 0.
$ declare -i count=0
$ count=count+1 # no $(( )) needed
$ count="3 * 4" # parsed as arithmetic
$ echo "$count"
12
Indexed array (declare -a)#
Ordered list, zero-indexed, integer subscripts. arr=( ... ) is
the literal form; arr[2]=x writes one slot; "${arr[@]}"
expands every element as a separate word (quoting matters).
$ declare -a hosts=(web01 web02 db01)
$ hosts+=(cache01) # append
$ echo "${hosts[2]}" # one element
$ echo "${#hosts[@]}" # count
$ for h in "${hosts[@]}"; do echo "$h"; done
db01
4
web01
web02
db01
cache01
Use to collect find results, parse $@ once, or hold a list
of targets the operator iterates over.
Associative array (declare -A)#
String-keyed map. Requires Bash 4 or later (macOS system Bash is 3.x, so install via Homebrew on macOS targets). Ideal for config-style key / value pairs.
$ declare -A region
$ region[web01]=us-east-1
$ region[db01]=eu-west-1
$ region+=([cache01]=us-west-2)
$ echo "${region[web01]}"
$ for k in "${!region[@]}"; do echo "$k -> ${region[$k]}"; done
us-east-1
web01 -> us-east-1
db01 -> eu-west-1
cache01 -> us-west-2
Readonly (declare -r / readonly)#
Marks a name as a constant. Any later assignment errors out;
unset is refused. Use for configuration baked into a script
that the operator must not let a downstream code path overwrite.
$ readonly PI=3.14159
$ PI=3.14 # error: readonly variable
bash: PI: readonly variable
Exported (declare -x / export)#
Promotes a shell variable into the environment block so every
child process the shell forks inherits it. The mechanism behind
$PATH, $EDITOR, secrets passed to a sub-program, and the
-E flag on sudo. See The Terminal for the
full scope picture.
$ export EDITOR=vim
$ bash -c 'echo "$EDITOR"' # child inherited
vim
Case-folded (declare -l / declare -u)#
Auto-lowercase (-l) or auto-uppercase (-u) on every
assignment. Cheap normalization for user input the operator does
not want to handle in two cases everywhere.
$ declare -l env
$ env="PROD"
$ echo "$env"
prod
Nameref (declare -n)#
Stores a reference to another variable’s name. Reads and writes through the nameref hit the underlying variable. The standard way to pass an array or map into a function without copying.
$ declare -A counters=([a]=1 [b]=2)
$ bump() {
$ declare -n ref="$1"
$ ref[$2]=$(( ref[$2] + 1 ))
$ }
$ bump counters a
$ echo "${counters[a]}"
2
Local (function scope)#
local (and declare inside a function) binds a name only for
the lifetime of the function call. Returning pops the binding and
restores any outer value. Scoping is dynamic, so functions
called from this function see the local unless they declare their
own. Always reach for local in script functions to avoid
silently clobbering a caller’s variable.
$ greet() {
$ local who="$1"
$ echo "hello, $who"
$ }
$ who="caller"
$ greet "alice"
$ echo "$who" # outer who untouched
hello, alice
caller
For the full picture of how function, shell, and process scope nest, and how subshells fit, see The Terminal.
Picking the right type#
Type |
Reach for it when… |
Watch out for |
|---|---|---|
Scalar string |
The value fits on one line and nothing splits it. |
Word-splitting on unquoted use; always |
Indexed array |
The values are an ordered list: hosts, targets,
filenames, |
|
Associative array |
The values form a key / value map: hostname → region, env-name → port, role → kubeconfig. |
Bash 4+. macOS system Bash is 3.x; install via Homebrew. |
Hosts#
The single most common indexed-array structure in operator scripts.
Build the list once, iterate with "${arr[@]}" so spaces in
names stay one token.
$ hosts=(web01 web02 db01 cache01)
$ for h in "${hosts[@]}"; do
$ ssh -o ConnectTimeout=3 "$h" 'uptime'
$ done
Drive the list from a file or a command, not a hand-typed glob.
$ mapfile -t hosts < hosts.txt # one host per line
$ mapfile -t hosts < <(awk '/^prod-/{print $1}' inventory.txt)
Host → metadata#
The standard associative-array structure. Look up per-host data by the name the operator already typed.
$ declare -A region
$ region=( [web01]=us-east-1 [db01]=eu-west-1 [cache01]=us-west-2 )
$ h=web01
$ aws --region "${region[$h]}" ec2 describe-instances
Parallel arrays#
When the operator needs more than one value per item but does not
want to leave Bash for jq, keep parallel indexed arrays keyed
by the same index. The discipline is to mutate them as a set, not
one at a time.
$ hosts=(web01 db01 cache01)
$ roles=(web database cache)
$ ports=(443 5432 6379)
$ for i in "${!hosts[@]}"; do
$ printf '%-10s %-8s %5d\n' "${hosts[i]}" "${roles[i]}" "${ports[i]}"
$ done
Record per host#
Bash has no struct. The operator’s two standard moves: an associative array per record (clear, slow if you have many), or JSON via ``jq`` as the source of truth and Bash only iterates the keys.
$ declare -A book=(
$ [title]="The Go Programming Language"
$ [year]=2015
$ [authors]="Donovan, Kernighan"
$ )
$ printf '%s (%d)\n' "${book[title]}" "${book[year]}"
For more than two or three records, push the structure into JSON
and read it through jq. The operator gets nested data, types,
and a real query language; Bash just drives the loop.
$ jq -r '.hosts[] | "\(.name)\t\(.region)"' inventory.json |
$ while IFS=$'\t' read -r name region; do
$ echo "host=$name region=$region"
$ done
Sets via maps#
There is no set type. Use an associative array with a sentinel
value and test for key presence with ${seen[$x]+_}.
$ declare -A seen
$ for ip in $(awk '{print $1}' access.log); do
$ if [[ -z "${seen[$ip]+_}" ]]; then
$ seen[$ip]=1
$ printf '%s\n' "$ip" # first sighting
$ fi
$ done
String as buffer#
When the operator wants to accumulate output and process it
afterwards, treat a scalar like a buffer using +=. Cheaper
than an array for one-off accumulation.
$ buf=""
$ for f in *.log; do
$ buf+="$f: $(wc -l < "$f")"$'\n'
$ done
$ printf '%s' "$buf" | sort -t: -k2 -n
References#
Overview for the grammar, operators, control flow, and the language-wide patterns these structures plug into.
I/O and Pipelines for
mapfile, process substitution, and thewhile readdiscipline these structures feed.Algorithms for the search / sort patterns that operate on indexed arrays.
The Terminal for the function, shell, and process scope rules the
Local/Exportedattributes hook into.man 1 bash(theArraysandParameter Expansionsections of the manual).