Structures#

Zsh has more data types than Bash: scalar strings, integers, floats, indexed arrays (1-based by default), associative arrays, plus the tied attribute that mirrors a scalar and an array (the way $PATH and $path stay in sync). This page covers the declaration syntax of each type and the operational patterns the operator reaches for when those primitives meet real work.

Types#

Variables are untyped strings by default. typeset (and its synonyms declare, integer, float, local) attaches attributes: integer arithmetic, float arithmetic, array or hash storage, read-only protection, export, case folding, indirection, tying. 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.

Integer

typeset -i (or integer)

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

Float

typeset -F / -E (or float)

A name that should hold a decimal value; native to zsh.

Indexed array

typeset -a or arr=(...)

Ordered list, 1-based subscripts.

Associative array

typeset -A

Key / value map with string keys.

Readonly

typeset -r or readonly

Constants the operator must not let a later line clobber.

Exported

typeset -x or export

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

Lowercase / Uppercase

typeset -l / typeset -u

Names that auto-fold on assignment.

Tied

typeset -T

A scalar and an array share storage (e.g. PATH / path).

Unique array

typeset -aU

Array that drops duplicates on assignment.

Local

local (inside a function)

Private to the function call; popped on return.

String (default)#

The catch-all. Any unqualified assignment lands here.

$ name="operator"
$ greeting="hello, $name"
$ print -- "$greeting"
hello, operator

Integer (typeset -i / integer)#

Assignments to an integer-attributed name are evaluated as arithmetic. integer is a synonym for typeset -i.

$ integer count=0
$ count=count+1            # no $(( )) needed
$ count="3 * 4"            # parsed as arithmetic
$ print -- "$count"
12

Float (typeset -F / float)#

Zsh has real floating-point, unlike bash. -F formats with fixed precision (-F 4 keeps four decimals), -E uses exponential notation.

$ float -F 4 ratio
$ ratio="22.0 / 7.0"
$ print -- "$ratio"
3.1429

Indexed array (typeset -a)#

Ordered list, 1-based by default. arr=( ... ) is the literal form; arr[2]=x writes one slot; $arr or ${arr[@]} expand every element. Quoting is less of a foot-gun than bash because zsh does not word-split unquoted expansions.

$ typeset -a hosts=(web01 web02 db01)
$ hosts+=(cache01)               # append
$ print -- "$hosts[2]"           # one element (1-based!)
$ print -- "${#hosts[@]}"        # count
$ for h in $hosts; do print -- "$h"; done
web02
4
web01
web02
db01
cache01

Slicing. Zsh subscripts accept ranges: $arr[2,4] is elements 2 through 4 inclusive. Negative indices count from the end ($arr[-1] is the last).

$ print -- $hosts[2,3]          # web02 db01
$ print -- $hosts[-1]           # cache01

KSH_ARRAYS. setopt KSH_ARRAYS flips to 0-based indexing and the bash-style ${arr[0]} form. Many ported bash scripts expect this; turn it on at the top of the script or run the script under bash if it relies on the behavior heavily.

$ setopt ksh_arrays
$ print -- ${arr[0]}             # first element with KSH_ARRAYS

Use to collect find results, parse $@ once, or hold a list of targets the operator iterates over.

Associative array (typeset -A)#

String-keyed map. Available in every zsh; no version check required.

$ typeset -A region
$ region[web01]=us-east-1
$ region[db01]=eu-west-1
$ region+=(cache01 us-west-2)            # zsh syntax for append
$ print -- "$region[web01]"
$ for k v in ${(kv)region}; do
  $ print -- "$k -> $v"
$ done
us-east-1
web01 -> us-east-1
db01 -> eu-west-1
cache01 -> us-west-2

The (kv) flag flattens the hash into key1 value1 key2 value2, which the operator can then iterate two-at-a-time with for k v in ....

Readonly (typeset -r / readonly)#

Marks a name as a constant. Any later assignment errors out.

$ readonly PI=3.14159
$ PI=3.14                # error: read-only variable
zsh: read-only variable: PI

Exported (typeset -x / export)#

Promotes a shell variable into the environment block so every child process the shell forks inherits it. The mechanism behind $PATH, $EDITOR, and the -E flag on sudo.

$ export EDITOR=vim
$ zsh -c 'print -- "$EDITOR"'      # child inherited
vim

Case-folded (typeset -l / typeset -u)#

Auto-lowercase (-l) or auto-uppercase (-u) on every assignment.

$ typeset -l env
$ env="PROD"
$ print -- "$env"
prod

Tied (typeset -T)#

Zsh-only attribute that ties a scalar and an array so they share storage. PATH (colon-joined string) and path (array view) are tied this way out of the box. The trick lets the operator manipulate $path with array operations and read $PATH for any tool that wants the joined form.

$ typeset -T LD_LIBRARY_PATH ld_library_path ':'
$ ld_library_path+=(/opt/lib)
$ print -- "$LD_LIBRARY_PATH"

The third argument is the separator; : is the default if omitted.

Unique array (typeset -aU)#

Drops duplicates on assignment. The standard reach for $path-style variables where doubling an entry would be a correctness bug.

$ typeset -aU path
$ path+=(/usr/local/bin /usr/local/bin /opt/bin)
$ print -l -- $path
/usr/local/bin
/opt/bin

Local (function scope)#

local (and typeset inside a function) binds a name only for 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.

$ greet() {
  $ local who="$1"
  $ print -- "hello, $who"
$ }
$ who="caller"
$ greet "alice"
$ print -- "$who"             # outer who untouched
hello, alice
caller

For the function, shell, and process scope picture, 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.

Less of a quoting foot-gun than bash, but still quote on the right of ==.

Indexed array

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

1-based by default; KSH_ARRAYS flips it.

Associative array

The values form a key / value map: hostname → region, env-name → port.

Iteration order is unspecified; use ${(k)} and ${(v)} flags to be explicit.

Tied scalar+array

Colon-joined path-style variables.

Mostly a system-built convenience; rarely worth introducing your own.

Hosts#

The single most common indexed-array structure in operator scripts.

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

$ hosts=("${(@f)$(<hosts.txt)}")                  # one host per line
$ hosts=("${(@f)$(awk '/^prod-/{print $1}' inventory.txt)}")

The (f) flag splits on newlines; the @ makes each line its own array element.

Host → metadata#

The standard associative-array structure.

$ typeset -A 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 zsh for jq, keep parallel indexed arrays keyed by the same index.

$ hosts=(web01      db01        cache01)
$ roles=(web        database    cache)
$ ports=(443        5432        6379)

$ for i in {1..$#hosts}; do
  $ printf '%-10s %-8s %5d\n' "$hosts[i]" "$roles[i]" "$ports[i]"
$ done

Record per host#

Zsh 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 zsh only iterates the keys.

$ typeset -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.

$ jq -r '.hosts[] | "\(.name)\t\(.region)"' inventory.json |
  $ while IFS=$'\t' read -r name region; do
  $ print -- "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 ${+map[$key]}.

$ typeset -A seen
$ for ip in $(awk '{print $1}' access.log); do
  $ if (( ! ${+seen[$ip]} )); then
    $ seen[$ip]=1
    $ print -- "$ip"             # first sighting
  $ fi
$ done

Or use typeset -aU to keep an array unique on assignment, trading the explicit “first sighting” branch for the simpler form.

$ typeset -aU seen
$ seen+=("${(f)$(awk '{print $1}' access.log)}")
$ print -l -- $seen

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. Even cheaper in zsh than bash, because the (F) flag joins an accumulated array with newlines on the fly.

$ buf=()
$ for f in *.log; do
  $ buf+=("$f: $(wc -l < $f)")
$ done
$ print -- "${(F)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 the while read discipline these structures feed, plus =( ) process substitution.

  • 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 zshparam (parameters and arrays).

  • man 1 zshexpn (parameter-expansion flags (@), (k), (v), (F), (f), (s::), (j::), (o), (O), (u), (n), (P)).