PONY λ M2 Modula-2

Ruby.CodeCompared.To/Zsh

An interactive executable cheatsheet for Rubyists learning Zsh

Ruby 4.0 Zsh 5.9
Variables & Quoting
Variable Assignment
greeting = "Hello" number = 42 puts greeting puts number
greeting="Hello" number=42 echo $greeting echo $number
Zsh requires no spaces around the = sign — a space on either side causes a syntax error. Variable names are case-sensitive. Unlike Ruby, Zsh has no separate integer or string types at the variable level — everything is a string unless you use typeset -i.
Curly Brace Expansion
language = "Ruby" puts "I love #{language}!" puts "#{language}ist"
language="Ruby" echo "I love ${language}!" echo "${language}ist"
In Zsh, curly braces around a variable name (${variable}) are optional for simple access but required when the variable name is immediately followed by letters, digits, or underscores that should not be part of the name. Ruby's #{} syntax is analogous but always required for interpolation.
Single vs Double Quotes
name = "World" puts 'Hello, #{name}' # No interpolation — literal #{} puts "Hello, #{name}" # Interpolation
name="World" echo 'Hello, $name' # No interpolation — literal $name echo "Hello, $name" # Interpolation
Single quotes in Zsh prevent all interpretation — no variable expansion, no escape sequences. Double quotes allow variable and command substitution but prevent word splitting and glob expansion. Ruby's single-quote strings prevent interpolation similarly, but still allow \\ and \' escapes.
Default Value
port = nil port ||= 8080 puts port
echo "${port:-8080}" # port is unset, so prints: 8080 port=3000 echo "${port:-8080}" # port is set, so prints: 3000
The ${var:-default} expansion returns the default if the variable is unset or empty, without modifying it. This is Zsh's idiomatic equivalent of Ruby's ||= for providing fallbacks. The :- variant treats both unset and empty string as "missing"; - (without colon) only triggers on unset.
Assign If Unset
count ||= 0 count += 1 puts count
: ${count:=0} (( count += 1 )) echo $count
The ${var:=value} expansion assigns the value to the variable if it is unset or empty, then expands to that value. The leading : (the null command) discards the expansion result while still triggering the assignment. This is Zsh's equivalent of Ruby's ||= when you want the assignment to persist.
Alternate Value
debug = true prefix = debug ? "[DEBUG] " : "" puts "#{prefix}Server started"
debug=true echo "${debug:+[DEBUG] }Server started"
The ${var:+alt} expansion returns the alternate value if the variable is set and non-empty, otherwise returns nothing. It is the inverse of ${var:-default}. Useful for optionally inserting a prefix or flag based on whether a variable is set.
Readonly Variables
PI = 3.14159 puts PI # PI = 3.0 # => NameError: already initialized constant
typeset -r PI=3.14159 echo $PI # PI=3.0 # would produce: zsh: read-only variable: PI
The typeset -r flag marks a variable as read-only. Any attempt to reassign it causes a runtime error. typeset is the idiomatic Zsh form; declare also works as an alias. In Ruby, constants (capitalized names) warn on reassignment but still allow it — Zsh is stricter.
Integer Variables
count = 0 begin count += "5" # TypeError — cannot coerce String to Integer rescue TypeError => error puts error.message end count += 5 puts count
typeset -i count=0 count+=5 # Arithmetic context — adds 5 count+="hello" # Non-numeric string treated as 0 echo $count
The typeset -i flag forces a variable into integer context. Assignments to it are evaluated as arithmetic expressions — string values that cannot be parsed as integers are treated as zero. This is Zsh's idiomatic form; typeset is preferred over declare in Zsh scripts.
Automatic Case Transformation
tag = "HELLO" puts tag.downcase # hello puts tag.upcase # HELLO
typeset -l lowered="HELLO WORLD" typeset -u uppered="hello world" echo $lowered # hello world echo $uppered # HELLO WORLD
The typeset -l flag auto-lowercases every value assigned to the variable; typeset -u auto-uppercases. The transformation is applied at assignment time, not at read time. This is Zsh's idiomatic form — typeset is preferred over declare -l/-u in Zsh scripts.
Environment Variables
home = ENV["HOME"] puts "Home: #{home}" puts "Shell: #{ENV["SHELL"]}"
echo "Home: $HOME" echo "Shell: $SHELL"
In Zsh, environment variables inherited from the process are immediately accessible as plain variables. Ruby accesses them through the ENV hash. Zsh convention uses uppercase names for environment variables and lowercase for local script variables, though Zsh itself enforces no such distinction.
String Operations
String Length
message = "Hello, World!" puts message.length # 13
message="Hello, World!" echo ${#message} # 13
The ${#variable} expansion returns the number of characters in the string. In Ruby, .length and .size are equivalent methods. For arrays, the same Zsh syntax (${#array}) returns the element count.
Substring Extraction
text = "Hello, World!" puts text[7, 5] # World puts text[7..] # World!
text="Hello, World!" echo ${text:7:5} # World echo ${text:7} # World!
The ${var:offset:length} expansion extracts a substring starting at offset (0-indexed). Omitting :length returns everything from the offset to the end. Negative offsets count from the end: ${var: -3} (space required before the minus). Ruby's slice syntax [start, length] maps directly to this.
Uppercase & Lowercase
greeting = "Hello, World!" puts greeting.upcase # HELLO, WORLD! puts greeting.downcase # hello, world! puts greeting.capitalize # Hello, world!
greeting="Hello, World!" echo ${(U)greeting} # HELLO, WORLD! echo ${(L)greeting} # hello, world! echo ${(C)greeting} # Hello, World! (capitalize each word)
Zsh uses parameter flags inside ${(flags)var} for case transformation: (U) uppercases, (L) lowercases, (C) capitalizes the first letter of each word. This is distinct from Bash's ${var^^} / ${var,,} syntax. The flags approach is more powerful and consistent across Zsh expansions.
Replace First Occurrence
text = "the cat sat on the mat" puts text.sub("at", "og") # the cog sat on the mat
text="the cat sat on the mat" echo ${text/at/og} # the cog sat on the mat
The ${var/pattern/replacement} expansion replaces the first match of the glob pattern. Ruby's String#sub is equivalent but uses a Regexp by default. Note that Zsh patterns use glob syntax (*, ?, [...]), not regular expressions.
Replace All Occurrences
text = "the cat sat on the mat" puts text.gsub("at", "og") # the cog sog on the mog
text="the cat sat on the mat" echo ${text//at/og} # the cog sog on the mog
The ${var//pattern/replacement} expansion (double slash) replaces all occurrences. The single-slash version only replaces the first. Ruby's String#gsub is the equivalent. Unlike gsub, the Zsh pattern is a glob, not a regexp.
Strip Prefix
path = "/usr/local/bin/ruby" # Remove leading slash and up to next slash: puts path.delete_prefix("/usr/") # local/bin/ruby puts path.sub(%r{^/[^/]*/}, "") # local/bin/ruby
path="/usr/local/bin/ruby" echo ${path#/usr/} # local/bin/ruby (shortest match) echo ${path##/*/} # local/bin/ruby (longest prefix up to /)
The ${var#pattern} expansion removes the shortest prefix matching the glob; ${var##pattern} removes the longest matching prefix. This is frequently used to strip directory components from paths. Ruby uses delete_prefix, sub, or regexp operations for the same task.
Strip Suffix
filename = "archive.tar.gz" puts filename.delete_suffix(".gz") # archive.tar puts filename.sub(/.[^.]*$/, "") # archive.tar (last ext) puts filename.sub(/..*$/, "") # archive (all exts)
filename="archive.tar.gz" echo ${filename%.gz} # archive.tar (shortest suffix) echo ${filename%.*} # archive.tar (last extension) echo ${filename%%.*} # archive (all extensions)
The ${var%pattern} expansion removes the shortest matching suffix; ${var%%pattern} removes the longest. Together with the prefix operators, these four expansions cover most path and filename manipulation tasks without calling external tools.
Test String Contains
sentence = "The quick brown fox" if sentence.include?("quick") puts "Found it" end
sentence="The quick brown fox" if [[ $sentence == *"quick"* ]]; then echo "Found it" fi
In Zsh, the == operator inside [[ ]] performs glob matching when the right side is unquoted. Wrapping the pattern in *...* checks for a substring anywhere in the string. Quoting the entire right side would do a literal string comparison instead.
String Concatenation
first = "Hello" second = "World" puts first + ", " + second + "!" puts "#{first}, #{second}!"
first="Hello" second="World" echo "$first, $second!" combined="$first, $second!" echo $combined
Zsh has no string concatenation operator. Strings are simply placed adjacent to each other inside double quotes. There is no + operator for strings — using it would attempt arithmetic and likely produce an error or zero. Ruby's + and #{} interpolation are both cleaner for this.
Join an Array into a String
words = ["one", "two", "three"] puts words.join(", ") # one, two, three puts words.join("-") # one-two-three
words=("one" "two" "three") echo ${(j:, :)words} # one, two, three echo ${(j:-:)words} # one-two-three
The Zsh parameter flag (j:sep:) joins array elements with the given separator — this is Zsh's clean equivalent of Ruby's Array#join. The colon is the flag delimiter; any character can be the separator. This is one of the clearest examples of Zsh's parameter flags being more ergonomic than pure Bash.
Split a String
csv_line = "alice,bob,charlie" parts = csv_line.split(",") puts parts.inspect puts parts[1]
csv_line="alice,bob,charlie" parts=(${(s:,:)csv_line}) echo "${parts[@]}" echo ${parts[2]} # bob (Zsh arrays are 1-indexed)
The Zsh parameter flag (s:sep:) splits a string on the given separator into an array — this is Zsh's built-in equivalent of Ruby's String#split. Note that the resulting array is 1-indexed, so the first element is ${parts[1]}, not ${parts[0]}.
Trim Whitespace (Manual)
text = " hello world " puts text.strip puts text.lstrip puts text.rstrip
text=" hello world " # Strip leading whitespace trimmed_left="${text#"${text%%[! ]*}"}" echo "'$trimmed_left'" # Strip trailing whitespace trimmed_right="${text%"${text##*[! ]}"}" echo "'$trimmed_right'"
Zsh has no built-in trim function. The idiomatic approach nests parameter expansions: ${var%%[! ]*} extracts the leading spaces, then ${var#...} strips that prefix. This works but is deliberately obscure — it highlights why Ruby's .strip is a real quality-of-life advantage.
Arithmetic
Arithmetic Expansion
puts 10 + 3 # 13 puts 10 - 3 # 7 puts 10 * 3 # 30 puts 10 / 3 # 3 (integer division) puts 10 % 3 # 1 puts 2 ** 8 # 256
echo $(( 10 + 3 )) echo $(( 10 - 3 )) echo $(( 10 * 3 )) echo $(( 10 / 3 )) # Integer division: 3 echo $(( 10 % 3 )) echo $(( 2 ** 8 ))
The $(( )) construct is arithmetic expansion — it evaluates integer arithmetic and expands to the result as a string. This is entirely different from $() command substitution (which requires fork()). Zsh only supports integer arithmetic natively; for floating-point, use the zsh/mathfunc module or an external tool.
Compound Arithmetic Tests
x = 10 x += 5 x -= 2 puts x # 13
x=10 (( x += 5 )) (( x -= 2 )) echo $x # 13
The (( )) construct (without the leading $) evaluates arithmetic and is used as a command — it returns exit code 0 if the result is non-zero, 1 if zero. This makes it usable directly in if and while conditions. All C-style compound operators work: +=, -=, *=, /=, %=, **=.
Increment & Decrement
counter = 0 counter += 1 puts counter # 1 counter += 1 puts counter # 2
counter=0 (( counter++ )) echo $counter # 1 (( ++counter )) echo $counter # 2 (( counter-- )) echo $counter # 1
Zsh supports C-style post-increment (counter++) and pre-increment (++counter) inside (( )). The difference: post-increment returns the value before incrementing; pre-increment returns the value after. Ruby has no ++ or -- operators — use += 1 instead.
Arithmetic Comparisons
score = 85 puts score >= 90 ? "A" : (score >= 80 ? "B" : "C")
score=85 if (( score >= 90 )); then echo "A" elif (( score >= 80 )); then echo "B" else echo "C" fi
Inside (( )), comparison operators use C syntax: ==, !=, <, >, <=, >=. This is much cleaner than the legacy -eq, -lt, -gt flags used with [ ] or [[ ]] for numeric comparisons. The (( )) style is unambiguous and preferred in modern Zsh.
Ternary in Arithmetic
x = 7 result = x.even? ? "even" : "odd" puts result
x=7 echo $(( x % 2 == 0 ? 0 : 1 )) # 1 (x is odd) (( x % 2 == 0 )) && echo "even" || echo "odd"
Arithmetic expansion supports the C ternary operator cond ? yes : no, but it only works for integer results, not strings. The && ... || ... idiom chains shell commands — it works like a ternary for string output but can misbehave if the "then" command fails. Use an explicit if for clarity.
Floating-Point Formatting
pi = 3.14159265 puts pi.round(2) # 3.14 printf("%.4f ", pi) # 3.1416
pi=3.14159265 printf "%.2f " $pi # 3.14 printf "%.4f " $pi # 3.1416
Zsh's arithmetic is integer-only by default — $(( 1/3 )) produces 0, not 0.333. The printf builtin can format floating-point literals (strings that look like floats) using %f. For actual float arithmetic, load zmodload zsh/mathfunc and use $(( sin(x) )) etc.
The let Builtin
product = 6 * 7 puts product # 42
let "product = 6 * 7" echo $product # 42 let "x = 2 ** 10" echo $x # 1024
The let builtin evaluates arithmetic expressions and assigns results to variables. It is older than (( )) and less commonly used in modern Zsh scripts. Both forms are equivalent — (( product = 6 * 7 )) is the contemporary idiom. let is supported in both Zsh and Bash.
Indexed Arrays
Create & Access
fruits = ["apple", "banana", "cherry"] puts fruits[0] # apple puts fruits[1] # banana puts fruits[-1] # cherry
fruits=("apple" "banana" "cherry") echo ${fruits[1]} # apple (Zsh arrays start at 1) echo ${fruits[2]} # banana echo ${fruits[-1]} # cherry (negative index counts from end)
Zsh arrays are 1-indexed by default — the first element is at index 1, unlike Ruby and Bash which both start at 0. This is the most important difference from Bash. Negative indices count from the end: ${fruits[-1]} is the last element. Set setopt KSH_ARRAYS for 0-based indexing if needed.
Array Length
fruits = ["apple", "banana", "cherry"] puts fruits.length # 3
fruits=("apple" "banana" "cherry") echo ${#fruits} # 3
In Zsh, ${#array} returns the number of elements (without the [@] subscript). The Bash form ${#array[@]} also works in Zsh, but ${#array} is the idiomatic Zsh shorthand. Both are equivalent for non-sparse arrays.
All Elements
fruits = ["apple", "banana", "cherry"] puts fruits.join(" ") puts fruits.inspect
fruits=("apple" "banana" "cherry") echo "${fruits[@]}" # apple banana cherry printf "%s " "${fruits[@]}" # one per line
Inside double quotes, "${array[@]}" expands to one separate word per element, preserving spaces within elements. "${array[*]}" joins all elements into a single word separated by the first character of IFS. Always prefer [@] inside double quotes when iterating.
Append Elements
fruits = ["apple", "banana"] fruits << "cherry" fruits.push("date") puts fruits.inspect
fruits=("apple" "banana") fruits+=("cherry") fruits+=("date" "elderberry") echo "${fruits[@]}"
The += operator appends elements to an array when the right-hand side is a parenthesized list. This is analogous to Ruby's << or push. You can append a single element or multiple at once. Zsh also supports fruits[6]="fig" for sparse assignment.
Array Slice
fruits = ["apple", "banana", "cherry", "date"] puts fruits[1, 2].inspect # ["banana", "cherry"]
fruits=("apple" "banana" "cherry" "date") echo "${fruits[@]:1:2}" # apple banana (Zsh: offset is 0-based within [@]:)
The ${array[@]:offset:length} slice uses a 0-based offset even in Zsh's otherwise 1-indexed world — :1:2 starts after the first element. To slice starting from the second element (index 2 in Zsh's 1-based indexing), use ${fruits[2,3]} — Zsh's native slice syntax, which is 1-based.
Iterate Over Array
fruits = ["apple", "banana", "cherry"] fruits.each do |fruit| puts fruit end
fruits=("apple" "banana" "cherry") for fruit in "${fruits[@]}"; do echo "$fruit" done
Always quote "${fruits[@]}" in the for loop. Without quotes, elements containing spaces would be split into multiple words. The double-quoted [@] expansion ensures each array element is treated as a single word, matching Ruby's block-based iteration.
Array Indices
fruits = ["apple", "banana", "cherry"] fruits.each_with_index do |fruit, index| puts "#{index}: #{fruit}" end
fruits=("apple" "banana" "cherry") for index in "${(@k)fruits}"; do echo "$index: ${fruits[$index]}" done
The Zsh parameter flag (@k) (or (k)) expands to the list of keys/indices of the array — for an indexed array this is 1 2 3 ... (1-based). This differs from Bash's ${!fruits[@]} which yields 0 1 2 .... The loop prints 1: apple, 2: banana, etc.
Delete an Element
fruits = ["apple", "banana", "cherry"] fruits.delete_at(1) puts fruits.inspect # ["apple", "cherry"]
fruits=("apple" "banana" "cherry") unset "fruits[2]" # Deletes "banana" (index 2 = second element) echo "${fruits[@]}" # apple cherry fruits=("${fruits[@]}") # Compact to remove gap
In Zsh, unset fruits[2] removes the second element (1-based indexing). After deletion, reassign the array to itself to compact the indices: fruits=("${fruits[@]}"). This contrasts with Bash where unset fruits[1] removes what Bash calls index 1 (its second element). Ruby's delete_at compacts automatically.
Brace Expansion Range
(1..5).each { |i| puts i } (0..10).step(2) { |i| puts i }
for i in {1..5}; do echo $i done for i in {0..10..2}; do echo $i done
Brace expansion ({start..end} and {start..end..step}) generates a sequence of words at parse time — before any variables are resolved. This means {1..n} where n is a variable does not work — use a C-style for (( i=1; i<=n; i++ )) loop instead. Ruby's Range with .step is more flexible.
Associative Arrays
Create & Access
person = { name: "Alice", age: 30, language: "Ruby" } puts person[:name] # Alice puts person[:age] # 30
typeset -A person person[name]="Alice" person[age]=30 person[language]="Ruby" echo ${person[name]} # Alice echo ${person[age]} # 30
Associative arrays require typeset -A before use — typeset is the idiomatic Zsh form (equivalent to Bash's declare -A). Without it, Zsh treats the variable as a scalar and string keys are ignored. Unlike Ruby hashes, Zsh associative arrays only support string keys and values, and iteration order is undefined.
Initialize with Values
colors = { red: "#FF0000", green: "#00FF00", blue: "#0000FF" } puts colors[:red]
typeset -A colors=([red]="#FF0000" [green]="#00FF00" [blue]="#0000FF") echo ${colors[red]}
Associative arrays can be initialized inline using the ([key]=value ...) syntax inside parentheses, combined with typeset -A. The key must be in brackets. This mirrors Ruby's hash literal syntax but is less readable due to the required brackets and parentheses.
All Keys & Values
scores = { alice: 95, bob: 87, carol: 92 } puts scores.keys.inspect puts scores.values.inspect
typeset -A scores=([alice]=95 [bob]=87 [carol]=92) echo "${(k)scores}" # alice bob carol (order varies) echo "${(v)scores}" # 95 87 92 (order varies)
The Zsh parameter flags (k) and (v) expand an associative array to its keys or values respectively. Alternatively, ${!scores[@]} (Bash-compatible) gives the keys. Iteration order is undefined — unlike Ruby where hashes preserve insertion order since Ruby 1.9.
Check If Key Exists
colors = { red: "#FF0000", green: "#00FF00" } puts colors.key?(:red) # true puts colors.key?(:purple) # false
typeset -A colors=([red]="#FF0000" [green]="#00FF00") if (( ${+colors[red]} )); then echo "red exists" fi if (( ! ${+colors[purple]} )); then echo "purple does not exist" fi
The ${+assoc[key]} expansion is Zsh's idiomatic key-existence test — it returns 1 if the key exists, 0 if not. The (( )) arithmetic context interprets this as a boolean. The Bash-compatible [[ -v assoc[key] ]] also works in Zsh 5.3+. Do not use [[ -n ${assoc[key]} ]] — that conflates an empty-string value with a missing key.
Iterate Key-Value Pairs
inventory = { apples: 5, bananas: 3, cherries: 12 } inventory.each do |item, count| puts "#{item}: #{count}" end
typeset -A inventory=([apples]=5 [bananas]=3 [cherries]=12) for item in "${(@k)inventory}"; do echo "$item: ${inventory[$item]}" done
Iterate over keys with "${(@k)inventory}" (Zsh-idiomatic) or "${!inventory[@]}" (Bash-compatible). The (@k) flag forces word-splitting on each key so spaces in keys are handled correctly. Iteration order is undefined — sort with print -l "${(@ok)inventory}" for deterministic output.
Delete a Key
settings = { debug: true, verbose: false, timeout: 30 } settings.delete(:verbose) puts settings.keys.inspect
typeset -A settings=([debug]=true [verbose]=false [timeout]=30) unset "settings[verbose]" echo "${(k)settings}"
The unset builtin removes a key from an associative array. Quote the expression to prevent glob expansion of the key. After deletion, the key no longer appears in ${(k)settings} and (( ${+settings[verbose]} )) returns false.
Number of Keys
inventory = { apples: 5, bananas: 3, cherries: 12 } puts inventory.size # 3
typeset -A inventory=([apples]=5 [bananas]=3 [cherries]=12) echo ${#inventory} # 3
${#assoc} counts the number of keys in an associative array — the same ${#} prefix works for string length, indexed array length, and associative array length in Zsh.
Control Flow
if / elif / else
temperature = 72 if temperature > 85 puts "Hot" elsif temperature > 65 puts "Comfortable" else puts "Cold" end
temperature=72 if (( temperature > 85 )); then echo "Hot" elif (( temperature > 65 )); then echo "Comfortable" else echo "Cold" fi
Zsh uses then, elif, and fi (if backwards) to delimit conditional blocks. The then must be on a new line or preceded by a semicolon. Using (( )) for numeric tests is the modern idiom — it supports all C-style comparison operators without the awkward -gt/-lt flags.
String Comparisons
language = "ruby" puts language == "ruby" # true puts language != "python" # true puts language.empty? # false puts language.length > 0 # true
language="ruby" [[ $language == "ruby" ]] && echo "match" [[ $language != "python" ]] && echo "not python" [[ -z $language ]] && echo "empty" || echo "not empty" [[ -n $language ]] && echo "has content"
The [[ ]] construct (double brackets) is the modern conditional expression. It supports == and != for string comparison, -z for "zero length", and -n for "non-zero length". The [[ ]] is safer than the older [ ] because it handles empty variables without quoting issues.
Numeric Comparisons
x = 5 puts x == 5 # true puts x < 10 # true puts x >= 5 # true
x=5 [[ $x -eq 5 ]] && echo "equals 5" # Legacy flags [[ $x -lt 10 ]] && echo "less than 10" (( x == 5 )) && echo "equals 5" # Modern (( )) (( x < 10 )) && echo "less than 10"
Numeric comparisons in Zsh can use either legacy flags (-eq, -ne, -lt, -le, -gt, -ge) inside [[ ]], or C-style operators (==, <, etc.) inside (( )). The (( )) style is more natural for arithmetic comparisons.
Logical Operators
age = 25 is_member = true if age >= 18 && is_member puts "Access granted" end
age=25 is_member=true if (( age >= 18 )) && [[ $is_member == "true" ]]; then echo "Access granted" fi
Zsh uses && and || for logical AND and OR both between separate commands and inside [[ ]]. You can combine tests from different constructs ((( )) and [[ ]]) with &&. Avoid the older -a and -o flags inside [ ] — they are deprecated.
case / esac
day = "Monday" case day when "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" puts "Weekday" when "Saturday", "Sunday" puts "Weekend" else puts "Unknown" end
day="Monday" case $day in Monday|Tuesday|Wednesday|Thursday|Friday) echo "Weekday" ;; Saturday|Sunday) echo "Weekend" ;; *) echo "Unknown" ;; esac
Zsh's case/esac is the inverse spelling of case (like fi for if). Patterns use | for alternatives and ;; to end each branch. Patterns are glob expressions, not regexes. The * default case is the equivalent of Ruby's else.
case with Glob Patterns
filename = "report.pdf" case filename when /.txt$/ then puts "Text file" when /.pdf$/ then puts "PDF document" when /.(jpg|png)$/ then puts "Image" else puts "Unknown type" end
filename="report.pdf" case $filename in *.txt) echo "Text file" ;; *.pdf) echo "PDF document" ;; *.jpg|*.png) echo "Image" ;; *) echo "Unknown type" ;; esac
The case statement is especially powerful for matching file extensions and other glob patterns. The *. prefix matches any string followed by a dot and extension. This replaces the need for many if/elif chains. Ruby's equivalent uses case/when with regexps, which are more powerful but heavier.
Short-Circuit Evaluation
dir = "/tmp" File.exist?(dir) && puts("#{dir} exists") # Or: puts "Dir exists" if File.exist?(dir)
dir="/tmp" # && runs right side only if left side succeeds (exit code 0) [[ -d $dir ]] && echo "$dir exists" # || runs right side only if left side fails [[ -d /nonexistent ]] || echo "Does not exist"
Zsh's && and || short-circuit at the command level based on exit codes, not boolean values. Exit code 0 is "success" (truthy); any non-zero is "failure" (falsy). This is the opposite of most languages where 0 is falsy. The pattern command && on-success || on-failure mimics a ternary but can misbehave if the success branch fails.
Loops
for-in Loop
["red", "green", "blue"].each do |color| puts color end
for color in red green blue; do echo $color done
The for item in list; do ... done loop iterates over whitespace-separated words. The list is subject to word splitting and glob expansion — be careful with spaces. Always quote array expansions: for item in "${array[@]}". Ruby's .each block is semantically identical.
C-Style for Loop
5.times { |i| puts i } 0.upto(4) { |i| puts i }
for (( i=0; i<5; i++ )); do echo $i done
The C-style for (( init; condition; increment )) loop uses arithmetic context throughout — no $ needed inside. This is the idiomatic Zsh replacement for Ruby's .times or .upto when you need an index. The loop variable is available after the loop ends.
while Loop
count = 0 while count < 5 puts count count += 1 end
count=0 while (( count < 5 )); do echo $count (( count++ )) done
The while condition; do ... done loop runs as long as the condition command returns exit code 0. Using (( )) as the condition gives C-style arithmetic tests. Unlike Ruby's while, the Zsh loop body must end with a separator (; or newline) before done.
until Loop
count = 0 until count >= 5 puts count count += 1 end
count=0 until (( count >= 5 )); do echo $count (( count++ )) done
The until loop runs as long as the condition is false (exit code non-zero). It is the logical inverse of while. Ruby has both while and until with the same inversion semantics. In practice, until is less common in both languages — a negated while is often clearer.
break & continue
(1..10).each do |i| next if i.even? break if i > 7 puts i end
for (( i=1; i<=10; i++ )); do (( i % 2 == 0 )) && continue (( i > 7 )) && break echo $i done
Zsh's break and continue work exactly like Ruby's break and next for loops. Both accept an optional numeric argument to break or continue an outer loop: break 2 exits the two innermost loops. This is more explicit than Ruby's break in nested loops.
Brace Expansion in Loops
('a'..'e').each { |letter| puts letter } (1..10).step(3) { |n| puts n }
for letter in {a..e}; do echo $letter done for n in {1..10..3}; do echo $n done
Brace expansion generates a sequence of words at parse time, before any variables are resolved. This means {1..n} where n is a variable does not work — use a C-style for (( i=1; i<=n; i++ )) loop instead. Ruby's Range with .step is more flexible.
Read Lines of Input
lines = ["one", "two", "three"] lines.each do |line| puts "Line: #{line}" end
# Reading from a pipe: not available in browser (requires fork) # Standard pattern for reference: # while IFS= read -r line; do # echo "Line: $line" # done < file.txt
The canonical Zsh (and POSIX shell) pattern for reading file content line by line uses while IFS= read -r line; do ... done < file. The IFS= prevents leading/trailing whitespace stripping; -r disables backslash processing. This pattern is not runnable in the browser (no real filesystem or pipes).
Functions
Define & Call a Function
def greet(name) puts "Hello, #{name}!" end greet("Alice") greet("Bob")
greet() { echo "Hello, $1!" } greet "Alice" greet "Bob"
Zsh functions are defined with name() { ... } or function name { ... }. They are called like commands — no parentheses, arguments separated by spaces. Parameters are positional: $1 is the first argument, $2 is the second, etc. Unlike Ruby, there is no parameter naming in the function signature.
Parameters & Argument Count
def describe(*args) puts "Got #{args.length} arguments: #{args.join(", ")}" end describe("one", "two", "three")
describe() { echo "Got $# arguments: $@" } describe "one" "two" "three"
Inside a Zsh function, $# is the argument count and $@ is all arguments as separate words. $* joins all arguments into one string. Use "$@" (quoted) when passing arguments forward to preserve spacing. These special variables are scoped to the function — they do not bleed out to the calling script.
Local Variables
counter = 10 def increment counter = 0 # Local — does not affect outer counter += 1 puts "Inside: #{counter}" end increment puts "Outside: #{counter}"
counter=10 increment() { local counter=0 # Must use 'local' keyword (( counter++ )) echo "Inside: $counter" } increment echo "Outside: $counter"
In Zsh, variables inside functions are global by default — without local, assigning to a variable inside a function modifies the outer scope. This is the opposite of Ruby, where all variables are local to their scope. Always use local for function-internal variables to avoid subtle bugs.
Return Values
def is_even?(number) number.even? end if is_even?(4) puts "4 is even" end
is_even() { (( $1 % 2 == 0 )) # Returns 0 (true) if even, 1 (false) if odd } if is_even 4; then echo "4 is even" fi if ! is_even 7; then echo "7 is odd" fi
Zsh functions do not return values — they return exit codes (0 = success/true, 1–255 = failure/false). The exit code is set by the last command executed or by an explicit return n statement. An arithmetic expression like (( n % 2 == 0 )) exits 0 when the expression is non-zero (true in arithmetic but "success" in exit-code terms).
Returning a String via Global Variable
def double(number) number * 2 end result = double(21) puts result # 42
double() { # Cannot use $() to capture — use a global "return" variable result_var=$(( $1 * 2 )) } double 21 echo $result_var # 42
Since Zsh functions only return exit codes, returning a string value requires a workaround: write to a known global variable. The convention is to use a variable like result_var or a nameref (typeset -n). The $(function_call) command-substitution pattern also works but requires fork(), which is unavailable in this browser runtime.
Default Parameter Values
def greet(name = "World") puts "Hello, #{name}!" end greet # Hello, World! greet("Alice") # Hello, Alice!
greet() { local name="${1:-World}" echo "Hello, $name!" } greet # Hello, World! greet "Alice" # Hello, Alice!
Zsh has no default parameter syntax in the function signature. The idiomatic substitute is local var="${n:-default}" inside the function body — this uses the positional parameter if provided, otherwise uses the default. Ruby's def foo(x = default) is cleaner and handles any expression as the default value.
Recursive Function
def factorial(number) return 1 if number <= 1 number * factorial(number - 1) end puts factorial(5) # 120
factorial() { if (( $1 <= 1 )); then echo 1 return fi local prev factorial $(( $1 - 1 )) prev=$result_var result_var=$(( $1 * prev )) } factorial 5 echo $result_var # 120
Recursion works in Zsh but is awkward since return values must flow through global variables rather than a clean call stack. The local keyword correctly scopes variables to each invocation. Deep recursion is expensive — each function call is a new stack frame, and shell scripts are much slower than Ruby for recursive algorithms.
Nameref Variables (Reference Parameters)
def push_item(collection, item) collection << item end items = [] push_item(items, "apple") puts items.inspect # ["apple"]
push_item() { typeset -n collection=$1 # nameref to the variable named by $1 collection+=("$2") } items=() push_item items "apple" echo "${items[@]}" # apple
typeset -n (Zsh 5.1+) creates a nameref — a reference to another variable by name. The nameref variable acts as an alias: reading or writing it reads or writes the target variable. typeset -n is the idiomatic Zsh form; declare -n (Bash-style) also works. This enables functions to modify arrays and hashes in the caller's scope.
Pattern Matching
Glob Pattern Matching
filename = "report_2026.txt" if filename.end_with?(".txt") puts "Text file" end if filename.start_with?("report") puts "Is a report" end
filename="report_2026.txt" if [[ $filename == *.txt ]]; then echo "Text file" fi if [[ $filename == report* ]]; then echo "Is a report" fi
Inside [[ ]], the right-hand side of == and != is treated as a glob pattern when unquoted. * matches any sequence of characters, ? matches exactly one. Quoting the pattern forces a literal string comparison. This is the fastest way to do prefix/suffix/substring checks in Zsh.
Regular Expression Matching
email = "user@example.com" if email.match?(/\A[\w.%+-]+@[\w.-]+\.[a-z]{2,}\z/i) puts "Valid email" end
email="user@example.com" if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then echo "Valid email" fi
The =~ operator inside [[ ]] matches the left side against an extended regular expression (ERE). The pattern is unquoted — quoting it forces a literal string match. Zsh uses POSIX ERE syntax (no \d, use [0-9] instead). Successful matches populate the match array with capture groups.
Regex Capture Groups
version = "2.3.1" if (match = version.match(/^(\d+)\.(\d+)\.(\d+)$/)) puts "Major: #{match[1]}" puts "Minor: #{match[2]}" puts "Patch: #{match[3]}" end
version="2.3.1" if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then echo "Major: ${match[1]}" echo "Minor: ${match[2]}" echo "Patch: ${match[3]}" fi
After a successful =~ match, Zsh stores capture groups in the match array (1-indexed: ${match[1]} is the first capture). The full match is in $MATCH. This differs from Bash, which uses BASH_REMATCH. The match array is analogous to Ruby's MatchData object.
Extended Glob Patterns
filename = "config.yaml" unless filename.match?(/\.(txt|md)$/) puts "Not a text or markdown file" end
setopt EXTENDED_GLOB filename="config.yaml" if [[ $filename != *.(txt|md) ]]; then echo "Not a text or markdown file" fi
Zsh's extended glob patterns are enabled with setopt EXTENDED_GLOB. The (a|b) syntax matches exactly one alternative (like Bash's @(a|b)). Zsh also adds ^pattern for negation and #/## as quantifiers. These are often cleaner than regex for filename matching.
Pattern Matching in case
input = "yes" case input when /^y(es)?$/i then puts "Affirmative" when /^no?$/i then puts "Negative" else puts "Unknown" end
input="yes" case $input in [Yy]|[Yy][Ee][Ss]) echo "Affirmative" ;; [Nn]|[Nn][Oo]) echo "Negative" ;; *) echo "Unknown" ;; esac
case patterns support character classes ([YyNn]), alternation (pat1|pat2), and glob wildcards (*, ?). They do not support regular expressions — for regex matching, use [[ =~ ]]. The character-class approach shown here is idiomatic for case-insensitive yes/no prompts.
Error Handling
Exit Codes & $?
begin raise "Something went wrong" rescue => error puts "Error: #{error.message}" puts "Handling it..." end
false # A command that always fails (exit code 1) echo "Exit code: $?" true # A command that always succeeds (exit code 0) echo "Exit code: $?"
Every command in Zsh produces an exit code stored in $?. Exit code 0 means success; any other value means failure. This is the fundamental error-signaling mechanism — there are no exceptions. The builtins true and false exist solely to produce exit codes 0 and 1. Ruby's exceptions are a higher-level abstraction over this concept.
Automatic Exit on Error
# Ruby raises exceptions by default; you must handle them explicitly def risky raise "Oops" end # risky # Would propagate and crash unless rescued
set -e # Exit immediately if any command fails echo "Step 1" echo "Step 2" # false # Uncommenting this would exit the script here echo "Step 3" set +e # Turn off strict mode
set -e (or set -o errexit) makes the script exit immediately if any command returns a non-zero exit code. This makes Zsh scripts behave more like Ruby — failing early rather than silently continuing. set +e turns it off. Many production scripts combine set -euo pipefail for maximum strictness.
Unset Variable Protection
# Ruby raises NameError for undefined variables # undefined_variable # => NameError: undefined local variable
set -u # Treat unset variables as errors name="Alice" echo "Hello, $name" # echo "Hello, $undefined" # Would cause: unbound variable set +u
set -u (or set -o nounset) causes the script to exit with an error if any unset variable is expanded. By default, Zsh silently expands unset variables to empty strings — a common source of bugs. With set -u, Zsh behaves more like Ruby's strict variable-must-be-defined enforcement.
Error Handling with ||
def connect(host) raise "Connection failed to #{host}" unless host == "localhost" "Connected to #{host}" end begin result = connect("remotehost") rescue => error puts "Error: #{error.message}" puts "Using fallback" end
connect() { [[ $1 == "localhost" ]] || return 1 echo "Connected to $1" } connect "remotehost" || { echo "Connection failed — using fallback" }
The command || { fallback commands; } pattern is Zsh's idiomatic error handler. The right side of || runs only when the left side fails (non-zero exit code). The braces group multiple fallback commands. This is more concise than if ! command; then ... fi for simple one-shot error handling.
Cleanup with trap
at_exit { puts "Cleanup!" } puts "Working..." # Cleanup! is printed when the script exits
cleanup() { echo "Cleanup!" } trap cleanup EXIT # Run cleanup() when the script exits echo "Working..." # "Cleanup!" prints when the script reaches the end
trap registers a command to run when a signal or special event occurs. EXIT triggers on any exit (normal or error). INT triggers on Ctrl-C. ZERR triggers when any command fails (Zsh's equivalent of Bash's ERR). This is Zsh's equivalent of Ruby's at_exit and ensure blocks.
Output & Formatting
echo vs printf
puts "Hello, World!" # Adds newline print "Hello, World!" # No newline printf "Name: %s ", "Alice"
echo "Hello, World!" # Adds newline printf "Hello, World!" # No newline printf "Hello, World! " # With explicit newline printf "Name: %s " "Alice"
echo always appends a newline (suppress with -n). printf uses C-style format strings and appends no newline unless you include \n. In scripts, printf is preferred over echo for portability. Zsh's print builtin is an enhanced alternative: print -P supports prompt-style escape sequences.
printf Format Strings
printf("%-10s %5d %8.2f ", "Alice", 30, 98.6) printf("Hex: %x, Oct: %o ", 255, 255)
printf "%-10s %5d %8.2f " "Alice" 30 98.6 printf "Hex: %x, Oct: %o " 255 255
Zsh's printf supports the same format specifiers as C: %s (string), %d (integer), %f (float), %x (hex), %o (octal). Width and precision modifiers (%10s, %-10s, %.2f) work identically to Ruby's Kernel#printf.
ANSI Color Codes
puts "e[32mGreen texte[0m" puts "e[1;34mBold bluee[0m" puts "e[31;47mRed on whitee[0m"
printf "\033[32mGreen text\033[0m\n" printf "\033[1;34mBold blue\033[0m\n" printf "\033[31;47mRed on white\033[0m\n"
ANSI escape sequences are identical in Ruby and Zsh — the escape character is \033 (octal) or \e. Color codes: 30–37 for foreground colors, 40–47 for backgrounds, 0 to reset, 1 for bold. Zsh also has the %F{color}...%f prompt escape syntax, but that only works in prompts via print -P.
Formatted Table Output
header = "%-12s %8s %10s" printf(header + " ", "Name", "Score", "Grade") printf("-" * 32 + " ") printf(header + " ", "Alice", 95, "A") printf(header + " ", "Bob", 87, "B")
header="%-12s %8s %10s " printf "$header" "Name" "Score" "Grade" printf "%s " "--------------------------------" printf "$header" "Alice" "95" "A" printf "$header" "Bob" "87" "B"
Storing a format string in a variable and reusing it with printf "$format" is idiomatic for consistent tabular output. Width specifiers align columns: %-12s left-aligns in 12 characters; %8s right-aligns in 8. This approach avoids the need for external tools like column or awk.
Special Shell Variables
puts $0 # Script name (in Ruby: $PROGRAM_NAME) puts $$ # Process ID (in Ruby: Process.pid) puts $?.to_i # Last exit code (in Ruby: after system())
echo "Script name: $0" echo "Process ID: $$" echo "Zsh version: $ZSH_VERSION" echo "Hostname: $HOST" false echo "Last exit: $?"
Zsh has its own set of automatic special variables: $0 (script/function name), $$ (process ID), $? (last exit code), $! (last background PID), $ZSH_VERSION, $HOST (hostname), $RANDOM (random integer), $SECONDS (seconds since shell start). Note $ZSH_VERSION vs Bash's $BASH_VERSION.
Here-Documents
name = "Yukihiro" order_id = 42 text = <<~HEREDOC Dear #{name}, Your order ##{order_id} has shipped. Thanks! HEREDOC puts text
# Here-documents (<<EOF) require file descriptors and are not # available in the browser WASM runtime. # Standard usage: # cat <<EOF # Dear $name, # Your order #$order_id has shipped. # Thanks! # EOF
Here-documents (<) feed a multiline string to a command's standard input, with variable expansion. Using <<-EOF strips leading tabs. Using <<'EOF' (quoted) suppresses variable expansion (like Ruby's <<~'HEREDOC'). File descriptor support is required, so here-docs are not runnable in this browser runtime.