Zsh Shell Tutorial 11: Functions — Arguments, Return Values & Scope
Video: Zsh Shell Tutorial 11: Functions — Arguments, Return Values & Scope by Taught by Celeste AI - AI Coding Coach
Zsh Lesson 11: Functions — Arguments, Return Values, Scope
name() { ... }defines a function. Arguments via$1,$2,$@,$#(just like script args).return Nfor exit status (0-255). To return data,echoit and capture with$(...).local varfor function-local variables.
Functions let you split a script into reusable, named pieces. Same syntax for all shells.
Defining a function
Two forms:
# Shorthand (POSIX-compatible)
greet() {
echo "Hello, World!"
}
# function keyword (Zsh/Bash extension)
function farewell {
echo "Goodbye, World!"
}
# Both can also use both:
function greet() { ... }
Use the shorthand name() { ... }. It works in every shell.
Calling functions
greet
# Hello, World!
farewell
# Goodbye, World!
No parentheses at the call site — just the function name. Arguments follow:
greet "Alice"
Arguments work like scripts
Inside a function, $1 $2 ... $@ $# reference the function's arguments, not the script's:
greet_user() {
echo "Hello, $1! Welcome to $2."
}
greet_user "Alice" "Zsh Land"
# Hello, Alice! Welcome to Zsh Land.
This is a key distinction: at the function's top level, the $N are its arguments, not the script's. At the script level (outside functions), they're the script's command-line arguments.
Show all arguments
show_args() {
echo "Total: $#"
echo "All: $@"
for arg in "$@"; do
echo " - $arg"
done
}
show_args apple banana cherry
# Total: 3
# All: apple banana cherry
# - apple
# - banana
# - cherry
"$@" (quoted) preserves argument boundaries — same as in scripts.
Return values: just an exit status
is_even() {
if (( $1 % 2 == 0 )); then
return 0
else
return 1
fi
}
is_even 4
echo $? # 0 — success (true)
is_even 7
echo $? # 1 — failure (false)
return N exits the function with status N (0-255). 0 means success, non-zero means failure. Same convention as scripts.
Without an explicit return, the function returns the exit status of the last command that ran.
Use as conditions
Because functions return exit status, they work directly in if:
is_even() {
(( $1 % 2 == 0 ))
}
if is_even 4; then
echo "even"
fi
The (( )) expression returns 0 if true. The function inherits that. Cleaner than the explicit return 0/1.
"Returning" data via echo
Shell functions can't return strings or arrays directly. Print to stdout, capture with $(...):
get_greeting() {
echo "Hello, $1!"
}
message=$(get_greeting "Bob")
echo "Captured: $message"
# Captured: Hello, Bob!
This is how every shell function passes data back. printf works too:
get_size() {
printf "%dM" $(( ($(stat -f%z "$1") + 1024*1024 - 1) / (1024*1024) ))
}
size=$(get_size /tmp/file.bin)
Scope: variables are global by default
color="blue"
overwrite_color() {
color="green" # modifies global
}
overwrite_color
echo $color # green — global was changed
By default, functions modify their enclosing scope. Watch out — easy to clobber.
local: function-only variables
color="blue"
change_color() {
local color="red" # function-local; doesn't affect outer
echo "Inside: $color"
}
echo "Before: $color" # blue
change_color # Inside: red
echo "After: $color" # blue (unchanged)
local declares a variable that exists only within the function. Always use local for function variables — prevents accidental clobbering.
In Zsh, you can also use typeset (same effect):
my_func() {
typeset count=0
...
}
A library of functions
Group related functions in a script you source:
# lib.sh
log_info() {
echo "[INFO] $*" >&2
}
log_error() {
echo "[ERROR] $*" >&2
}
require_command() {
command -v "$1" >/dev/null || {
log_error "missing required command: $1"
return 1
}
}
# main.sh
source ./lib.sh
require_command git || exit 1
log_info "Starting backup..."
source runs the file in the current shell, so functions become available.
Recursion
Yes, shell functions can recurse:
factorial() {
local n=$1
if (( n <= 1 )); then
echo 1
return
fi
echo $(( n * $(factorial $((n - 1))) ))
}
factorial 5
# 120
But it's slow. Each recursive call forks a subshell. For real recursion, use a real language.
Default values for parameters
Use ${1:-default}:
greet() {
local name="${1:-World}"
echo "Hello, $name!"
}
greet # Hello, World!
greet Alice # Hello, Alice!
Validating arguments
backup() {
if [[ $# -lt 1 ]]; then
echo "Usage: backup <path>" >&2
return 1
fi
local source="$1"
local dest="${2:-/tmp/backup}"
if [[ ! -e "$source" ]]; then
echo "Error: $source doesn't exist" >&2
return 1
fi
cp -a "$source" "$dest"
}
Validate inside the function; return non-zero on failure.
Anonymous functions (Zsh)
Zsh has anonymous functions for one-shot blocks:
() {
local x=10
local y=20
echo $((x + y))
} 30 40 # called immediately with arguments
# 30 (because $1 = 30, $2 = 40 — not x and y)
Rarely useful. Mentioned for completeness.
A worked example
#!/usr/bin/env zsh
set -euo pipefail
# Library functions
log() {
echo "[$(date +%H:%M:%S)] $*" >&2
}
require_command() {
command -v "$1" >/dev/null || {
log "ERROR: missing $1"
return 1
}
}
count_files() {
local dir="$1"
local pattern="${2:-*}"
find "$dir" -type f -name "$pattern" | wc -l
}
# Main
main() {
require_command find || exit 1
local target="${1:-.}"
log "Counting files in $target..."
local total=$(count_files "$target")
local logs=$(count_files "$target" "*.log")
echo "Total files: $total"
echo "Log files: $logs"
}
main "$@"
Note main "$@" at the bottom — forward script args to a main function. Common pattern for organizing larger scripts.
Common stumbles
Forgetting local. Function pollutes globals. Always local for function vars.
Returning strings via return. return "hello" errors — return only takes integers 0-255. Use echo and $(...).
Calling with parens. greet("Alice") is wrong. Functions are called like commands: greet "Alice".
(( count++ )) returning non-zero. When count was 0 (post-increment evaluates to 0). With set -e, exits the script. Use count=$((count + 1)) or ((count++)) || true.
Variable assignment inside if test. if [[ x=$(cmd) ]]; then — confusing. Assign first, test after.
Recursion is slow. Each call forks. For deep recursion, use a real language.
Function and command name collision. Defining cd() overrides the builtin. To call the original: builtin cd .... Avoid naming functions after builtins unless you're intentionally wrapping.
Forward declaration. Functions must be defined before use. If main() calls helper(), define helper first or use main "$@" at the very end of the file.
What's next
Lesson 12: arrays. Indexed and associative arrays.
Recap
name() { ... } defines a function. Arguments via $1 $2 ... $@ $# (shadows script args inside). return N for exit status (0-255). Pass data back via echo, capture with $(func). local var for function-local variables — always use it. Group helpers in a sourced lib. Validate arguments inside functions; return non-zero on failure. Forward script args to a main function with main "$@".
Next lesson: arrays.