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

name=value (default)

Everything that fits in a single line of text. The fallback.

Integer

declare -i

A name that should always hold an integer; assignment auto-evaluates arithmetic.

Indexed array

declare -a or arr=(...)

Ordered list, numeric subscripts. The bash equivalent of a list / vector.

Associative array

declare -A

Key / value map with string keys. Requires Bash 4+.

Readonly

declare -r or readonly

Constants and configuration the operator must not let a later line clobber.

Exported

declare -x or export

Values that need to cross fork / exec into child processes.

Lowercase / Uppercase

declare -l / declare -u

Names that auto-fold on assignment; useful for normalizing input.

Nameref

declare -n

A pointer to another name; the way to pass arrays or maps to functions by reference.

Local

local (inside a function)

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 "$var".

Indexed array

The values are an ordered list: hosts, targets, filenames, "$@" snapshot.

${arr[@]} vs ${arr[*]} quoting; unset 'arr[1]' leaves a hole.

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 the while read discipline 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 / Exported attributes hook into.

  • man 1 bash (the Arrays and Parameter Expansion sections of the manual).