Back to Blog

Zsh Variables & Data Types - Shell Scripting Tutorial for Beginners (Lesson 7)

Sandy LaneSandy Lane

Video: Zsh Variables & Data Types - Shell Scripting Tutorial for Beginners (Lesson 7) by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Zsh Lesson 7: Variables, Strings, Arithmetic, Environment

name=value (no spaces around =). Read with $name or ${name}. Double quotes expand variables; single quotes don't. $(( expr )) for math. export makes a variable available to child processes.

This lesson is the foundation of every script you'll write. Get the quoting and expansion right; everything else follows.

Declaring variables

name=Alice
age=25
city=Melbourne

No spaces around =. name = Alice errors — Zsh thinks you're trying to run a command called name with arguments.

Read the variable:

echo "Name: $name"
echo "Age: $age"
# Name: Alice
# Age: 25

Or with braces, useful when the name borders other characters:

file=report
echo "$file_backup"     # variable "file_backup" → empty
echo "${file}_backup"   # report_backup

Always brace when ambiguous.

Strings and quoting

fruit=apple

# Double quotes: expand $vars and $(commands)
echo "I like $fruit"
# I like apple

# Single quotes: literal — no expansion
echo 'I like $fruit'
# I like $fruit

This is the single most important thing to internalize:

  • Double quotes ("...") — expand variables and command substitutions.
  • Single quotes ('...') — completely literal.

To embed a literal $ in double quotes, escape:

price=5
echo "Cost: \$${price}.00"
# Cost: $5.00

Command substitution: $(...)

echo "Today is $(date +%A)"
# Today is Wednesday

today=$(date +%Y-%m-%d)
echo $today
# 2026-05-08

$(command) runs the command and substitutes its output. Use freely:

files=$(ls *.txt | wc -l)
echo "Found $files text files"

The older form `command` (backticks) does the same but doesn't nest cleanly. Use $(...).

Always quote variable references

# Bad
file=$(echo "my file.txt")
ls $file        # fails — sees two args: "my" and "file.txt"

# Good
ls "$file"      # one arg: "my file.txt"

Unquoted variable references undergo word splitting and globbing. Quote unless you specifically want splitting.

Arithmetic: $(( ... ))

result=$((5 + 3))
echo "5 + 3 = $result"
# 5 + 3 = 8

echo "10 - 4 = $((10 - 4))"
echo "6 * 7 = $((6 * 7))"
echo "20 / 4 = $((20 / 4))"
echo "17 % 5 = $((17 % 5))"   # modulo

Inside $(( )), you don't need $ on variable names:

a=10
b=3
echo $((a + b))    # 13
echo "$a + $b = $((a + b))"

Operators: +, -, *, /, %, ** (power), <, >, ==, !=, &&, ||, !, bitwise & | ^ << >>.

Increment and decrement

count=0
((count++))
echo "Count: $count"
# Count: 1

((count += 5))
echo $count
# 6

(( ... )) (without the $) evaluates an arithmetic expression for its side effect. Use for incrementing counters and conditional checks (lesson 9).

Floats?

Bash and Zsh's built-in arithmetic is integer only:

echo $((10 / 3))
# 3 — not 3.333...

For floats, pipe to bc or awk:

echo "scale=2; 10/3" | bc
# 3.33

echo $(awk 'BEGIN { print 10 / 3 }')
# 3.33333

Or in Zsh specifically, (( ... )) with setopt for floats — but it's simpler to call out to bc/awk for serious math.

Environment variables

Some are pre-set:

echo "Home:  $HOME"
echo "User:  $USER"
echo "Shell: $SHELL"
echo "PWD:   $PWD"
echo "PATH:  $PATH"

HOSTNAME, LANG, EDITOR, PAGER, TERM, LINES, COLUMNS — all common.

Set your own:

MY_APP=production
echo $MY_APP

This is shell-local — child processes don't see it.

export: make it global

export DATABASE_URL='postgres://localhost/mydb'
echo $DATABASE_URL

export puts the variable in the environment — every command you run inherits it.

To export an existing variable:

MY_VAR=value
export MY_VAR

Or in one line:

export MY_VAR=value

readonly: lock it

readonly PI=3.14159
echo $PI

PI=3.14    # error: read-only variable: PI

readonly prevents modification. Useful for constants.

unset: remove

temp=delete_me
echo $temp
unset temp
echo "temp is: '$temp'"
# temp is: ''

unset VAR removes the variable. Subsequent $VAR returns empty.

set -u: error on unset

echo $undefined
# (empty — no error)

set -u
echo $undefined
# zsh: undefined: parameter not set

set -u (or set -o nounset) makes Zsh error on referencing an unset variable. Highly recommended in scripts — catches typos.

To safely reference a possibly-unset variable, use ${VAR:-default}:

echo "${MISSING:-not set}"
# not set

Common parameter expansions

filename="report.tar.gz"

# Default if unset/empty
echo "${name:-anonymous}"

# Substring (zero-indexed)
echo "${filename:0:6}"      # report

# Strip suffix (shortest match)
echo "${filename%.gz}"      # report.tar

# Strip suffix (longest match)
echo "${filename%%.*}"      # report

# Strip prefix (shortest match)
echo "${filename#*.}"       # tar.gz

# Strip prefix (longest match)
echo "${filename##*.}"      # gz

# Replace
echo "${filename/tar/zip}"  # report.zip.gz

Pure shell — no sed/awk needed for simple manipulations.

${var:-default} is one of the most useful patterns. There's also ${var:=default} (set if unset), ${var:?message} (error if unset).

A complete script

#!/usr/bin/env zsh
set -euo pipefail

# Configuration with defaults
NAME="${NAME:-World}"
GREETING="${GREETING:-Hello}"
ITERATIONS="${ITERATIONS:-3}"

# Computed values
START=$(date +%s)
COUNT=0

while [[ $COUNT -lt $ITERATIONS ]]; do
  echo "$GREETING, $NAME!"
  ((COUNT++))
done

ELAPSED=$(( $(date +%s) - START ))
echo "Done in ${ELAPSED}s"

Run with defaults:

./greet.sh
# Hello, World! (x3)

Override via env:

NAME=Alice GREETING=Hi ITERATIONS=5 ./greet.sh

This is the standard "configurable script" pattern.

Common stumbles

Spaces around =. name = value errors. Always name=value.

Forgetting quotes. echo $file with file="my file.txt" becomes echo my file.txt — two args. Quote everything.

Single quotes when you wanted double. echo 'Path: $HOME' literally prints $HOME. Use double quotes for interpolation.

$VAR inside $(( )). Works but unnecessary: $((a + b)), not $(( $a + $b )).

Integer-only math. $((10 / 3)) is 3. For floats, bc or awk.

export to a string. export FOO bar doesn't work. Use export FOO=bar.

unset with $. unset $var evaluates $var first. Use unset var (no $).

Variable name typo silently empty. Catch with set -u in scripts.

What's next

Lesson 8: script input. Positional $1, defaults, read, getopts.

Recap

name=value (no spaces). $name or ${name} to read. Double quotes expand, single quotes don't. $(cmd) for command substitution. $(( expr )) for integer arithmetic. export VAR makes it visible to children. readonly VAR locks. unset VAR removes. Quote variable references unless you want word splitting. Use ${var:-default} for safe access. set -euo pipefail in scripts.

Next lesson: script input.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.