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 |
|
Everything that fits in a single line of text. |
Integer |
|
A name that should always hold an integer; assignment auto-evaluates arithmetic. |
Float |
|
A name that should hold a decimal value; native to zsh. |
Indexed array |
|
Ordered list, 1-based subscripts. |
Associative array |
|
Key / value map with string keys. |
Readonly |
|
Constants the operator must not let a later line clobber. |
Exported |
|
Values that need to cross |
Lowercase / Uppercase |
|
Names that auto-fold on assignment. |
Tied |
|
A scalar and an array share storage (e.g. |
Unique array |
|
Array that drops duplicates on assignment. |
Local |
|
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, |
1-based by default; |
Associative array |
The values form a key / value map: hostname → region, env-name → port. |
Iteration order is unspecified; use |
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 readdiscipline 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/Exportedattributes 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)).