Zsh Scripting: Arrays Complete Guide
Arrays are one of the most powerful data structures in Zsh scripting. Unlike Bash, Zsh treats arrays as first-class citizens with rich syntax, flexible indexing, and a wide range of built-in operations. Whether you are automating system tasks, parsing configuration files, or building complex CLI tools, mastering Zsh arrays will make your scripts cleaner, safer, and more expressive.
What Are Arrays in Zsh?
An array in Zsh is an ordered collection of values. Each value is identified by an index, and unlike many other shells, Zsh arrays are 1-indexed by default. This means the first element sits at index 1, not 0. Arrays can hold strings, numbers, or any combination of scalar values, and they grow or shrink dynamically as you add or remove elements.
Zsh also supports associative arrays (key-value maps), which we will cover later in this guide. Together, indexed and associative arrays give you the tools to model almost any data structure you need inside a shell script.
Why Arrays Matter in Zsh Scripting
- Structured data handling: Arrays let you group related values instead of juggling dozens of separate variables.
- Safer iteration: Looping over an array avoids word-splitting bugs that plague space-separated strings.
- Powerful transformations: Zsh provides parameter expansion flags for filtering, sorting, joining, and slicing arrays in a single expression.
- Cleaner scripts: Using arrays reduces the need for external tools like
awkorsedfor simple data manipulation. - Better than Bash: Zsh arrays preserve empty elements and offer more intuitive syntax than POSIX shells.
Creating and Assigning Arrays
The simplest way to create an array is to assign a parenthesized list of words to a variable. Each word becomes an element, and quoting protects values that contain spaces.
#!/usr/bin/env zsh
# Basic indexed array
fruits=(apple banana cherry)
# Array with spaces inside elements
cities=("New York" "Los Angeles" "San Francisco")
# Empty array
empty=()
# Assign individual elements
colors=()
colors[1]=red
colors[2]=green
colors[3]=blue
You can also build arrays from command output using command substitution. Wrapping the substitution in ${(f)...} splits on newlines, which is the safest way to capture lines of output.
# Split ls output into an array, one file per element
files=(${(f)"$(ls -1)"})
# Read lines from a file
lines=(${(f)"$(cat config.txt)"})
Accessing Array Elements
To access the whole array, use the ${name} form with the array sigil. To access a single element, provide its index inside brackets. Remember that Zsh arrays start at index 1.
fruits=(apple banana cherry)
# Whole array
echo ${fruits}
# Output: apple banana cherry
# Single element
echo ${fruits[1]}
# Output: apple
echo ${fruits[2]}
# Output: banana
# Last element using negative index
echo ${fruits[-1]}
# Output: cherry
# Slice: elements 1 through 2
echo ${fruits[1,2]}
# Output: apple banana
Getting the Length and Indices
Use the # operator to get the number of elements. Use the (i) or (k) flags to retrieve indices. These are essential when looping or validating data.
fruits=(apple banana cherry)
# Number of elements
echo ${#fruits}
# Output: 3
# All indices
echo ${fruits[(i)1]}, ${fruits[(i)2]}, ${fruits[(i)3]}
# Iterate using indices
for i in {1..${#fruits}}; do
echo "Element $i: ${fruits[$i]}"
done
Iterating Over Arrays
Zsh offers several idioms for looping through arrays. The most common is a for loop over the array directly.
fruits=(apple banana cherry)
# Simple iteration
for fruit in $fruits; do
echo "I like $fruit"
done
# Iterating with index and value
for index in {1..${#fruits}}; do
echo "$index: ${fruits[$index]}"
done
# Using the (K) and (V) flags for associative arrays (see below)
Adding and Removing Elements
Zsh makes it easy to append, prepend, or remove elements. The += operator appends, while [index]=() removes elements at specific positions.
fruits=(apple banana)
# Append
fruits+=(cherry date)
echo ${fruits}
# Output: apple banana cherry date
# Prepend
fruits=(apricot $fruits)
echo ${fruits}
# Output: apricot apple banana cherry date
# Remove element at index 2
fruits[2]=()
echo ${fruits}
# Output: apricot banana cherry date
# Remove last element
fruits[-1]=()
echo ${fruits}
# Output: apricot banana cherry
Array Parameter Expansion Flags
One of Zsh's standout features is its rich set of expansion flags. These let you transform arrays without external commands.
words=(zebra apple mango banana)
# Join elements with a separator
echo ${(j:, :)words}
# Output: zebra, apple, mango, banana
# Sort alphabetically
echo ${(o)words}
# Output: apple banana mango zebra
# Sort and remove duplicates
mixed=(apple banana apple cherry banana)
echo ${(ou)mixed}
# Output: apple banana cherry
# Reverse the array
echo ${(Oa)words}
# Output: banana mango apple zebra
# Uppercase every element
echo ${(U)words}
# Output: ZEBRA APPLE MANGO BANANA
# Filter elements matching a pattern
echo ${(M)words:#a*}
# Output: apple
Searching and Membership Tests
You can check whether a value exists in an array using the (r) and (i) flags. The (r) flag returns the first matching value, while (i) returns its index.
fruits=(apple banana cherry)
# Find a value
found=${fruits[(r)banana]}
if [[ -n $found ]]; then
echo "banana is in the list"
fi
# Find an index
idx=${fruits[(i)cherry]}
echo "cherry is at index $idx"
# Check if value is NOT present (empty result means not found)
if [[ -z ${fruits[(r)grape]} ]]; then
echo "grape is missing"
fi
Associative Arrays
Associative arrays map keys to values, similar to dictionaries in Python or objects in JavaScript. Declare them with typeset -A and assign key-value pairs.
#!/usr/bin/env zsh
# Declare an associative array
typeset -A config
# Assign values
config[host]="localhost"
config[port]="8080"
config[debug]="true"
# Access by key
echo "Host: ${config[host]}"
echo "Port: ${config[port]}"
# Iterate over keys
for key in ${(k)config}; do
echo "$key = ${config[$key]}"
done
# Iterate over keys and values together
for key val in ${(kv)config}; do
echo "$key -> $val"
done
# Number of entries
echo "Total config entries: ${#config}"
Reading Arrays from User Input
Reading user input into an array is useful for interactive scripts. Use read -A for indexed arrays and read -A with a prompt for a friendlier experience.
# Read multiple words into an array
echo "Enter your favorite colors separated by spaces:"
read -A colors
echo "You entered ${#colors} colors:"
for color in $colors; do
echo " - $color"
done
Passing Arrays to Functions
Zsh functions cannot directly receive arrays as arguments because arguments are split into words. The common workaround is to pass the array name and use ${(P)name} for indirect expansion, or to serialize the array.
print_array() {
local name=$1
local arr=("${(@P)name}")
for element in $arr; do
echo "Element: $element"
done
}
my_array=(one two three)
print_array my_array
Alternatively, you can pass elements directly and reconstruct the array inside the function, which is simpler when you do not need to preserve the original variable name.
sum_numbers() {
local nums=("$@")
local total=0
for n in $nums; do
(( total += n ))
done
echo "Sum: $total"
}
sum_numbers 10 20 30
# Output: Sum: 60
Practical Example: Processing a List of Files
Let's combine everything into a realistic script that collects files, filters them, and processes each one safely.
#!/usr/bin/env zsh
# Collect all .log files in the current directory
logs=(*.log)
if (( ${#logs} == 0 )); then
echo "No log files found."
exit 0
fi
echo "Found ${#logs} log files:"
# Sort and display
for file in ${(o)logs}; do
size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
echo " - $file (${size} bytes)"
done
# Archive files larger than 1KB
large_files=()
for file in $logs; do
size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
if (( size > 1024 )); then
large_files+=("$file")
fi
done
if (( ${#large_files} > 0 )); then
echo "Archiving large files: ${(j:, :)large_files}"
tar -czf large_logs.tar.gz $large_files
fi
Best Practices
- Always quote expansions: Use
"$array[@]"or"${(@)array}"to preserve elements with spaces. - Prefer
${(f)...}for line splitting: Avoid relying on default word splitting when reading command output. - Use
typeset -Afor key-value data: Do not abuse indexed arrays to simulate maps. - Initialize arrays explicitly: Use
arr=()to avoid inheriting values from the environment. - Use
emulate -L zshin functions: This ensures consistent array behavior regardless of the caller's options. - Leverage expansion flags: They are faster and more reliable than spawning external tools for simple transformations.
- Validate indices: Check that an index is within bounds before accessing elements to avoid silent empty results.
- Document 1-based indexing: If your script is shared with Bash users, add comments noting that Zsh arrays start at 1.
Common Pitfalls
Even experienced shell users stumble on a few Zsh-specific quirks. Being aware of them will save you hours of debugging.
# Pitfall 1: Forgetting 1-based indexing
arr=(a b c)
echo $arr[0] # Empty! There is no element 0
echo $arr[1] # Correct: a
# Pitfall 2: Unquoted array loses empty elements
arr=("one" "" "three")
echo ${#arr} # 3
for x in $arr; do echo "[$x]"; done # Empty element may vanish in some contexts
for x in "$arr[@]"; do echo "[$x]"; done # Safe
# Pitfall 3: Confusing scalar and array assignment
var=apple banana # Error: only "apple" assigned, "banana" runs as command
arr=(apple banana) # Correct array assignment
Conclusion
Zsh arrays are a versatile and expressive feature that can dramatically improve the quality of your shell scripts. By understanding indexed and associative arrays, mastering parameter expansion flags, and following best practices around quoting and initialization, you can write scripts that are robust, readable, and maintainable. While the 1-based indexing and rich flag syntax may feel unfamiliar at first, they quickly become second nature and unlock a level of data manipulation that is difficult to achieve in other shells. Whether you are writing a quick automation snippet or a full CLI tool, arrays will be one of the most valuable tools in your Zsh scripting toolkit.