1. Overview

Most Linux distributions have accustomed us to Bash as the interactive login shell and shell language. However, this is not the only choice for us. We have the Z shell Zsh, KornShell ksh, or the Berkeley UNIX C shell tcsh at our disposal as well.

In this tutorial, we’ll compare the loop statement of the Bash and Zsh shells, with additional emphasis on string splitting and globbing.

2. Installing Zsh

If our distribution doesn’t come with the Zsh shell, we need to install it. On Ubuntu, let’s use apt:

$ sudo apt install zsh

Afterward, we should create an empty configuration file .zshrc in our home folder:

$ touch .zshrc

Of course, we can fill it with our preferred settings later on. Finally, let’s open the Zsh shell by issuing in the terminal:

$ zsh

Throughout this tutorial, we’ll indicate the Zsh command line snippets with the % prompt, while Bash’s ones with $.

To run the script with Zsh, we should change the script’s usual Bash shebang #!/bin/bash to:

#!/bin/zsh

For clarity, the Zsh script will bear the zsh extension, while we leave the bash ones without an extension.

3. Loop Syntax

Let’s recall the basic syntax of the for loop, which is common for both Zsh and Bash:

for <variable> in <list_of_items>; do <command_1>; ... ; <command n>; done

Then let’s run a simple example:

$ for x in foo bar foobar; do echo $x; done
foo
bar
foobar

Note that we separated the elements in this explicitly defined list with a space. In the case when we want to use items with spaces inside, we should enclose them in single quotes:

$ for x in 'item 1' 'item 2' 'item 3'; do echo $x; done
item 1
item 2
item 3

4. Zsh Loop Alternatives

The Zsh shell offers a different syntax for the loop:

% for x (foo bar foobar); do echo $x; done                  
foo
bar
foobar

So, we dropped the in keyword and put the items list into parenthesis.

Another interesting Zsh feature is looping with multiple indices. Let’s assume that we want to print data concerning popular DNS providers with the multi_index.zsh script:

#!/bin/zsh

printf "%-12s %-12s %-12s\n" "Provider" "Primary DNS" "Secondary DNS"
for provider primary secondary (Google 8.8.8.8 8.8.4.4 'Control D' 76.76.2.0 76.76.10.0 Quad9 9.9.9.9 149.112.112.112)
do
    printf "%-12s %-12s %-12s\n" "$provider" "$primary" "$secondary"
done

The result pretty well explains this syntax:

% ./multi_index.zsh
Provider     Primary DNS  Secondary DNS
Google       8.8.8.8      8.8.4.4   
Control D    76.76.2.0    76.76.10.0  
Quad9        9.9.9.9      149.112.112.112

5. Arrays

In both Bash and Zsh, we can define arrays using parenthesis. So, let’s create a four-element array containing letters letters=(a b c d). Now, let’s print all elements in Zsh with the array.zsh script:

#!/bin/zsh

letters=(a b c d)

for x in $letters
do
    echo $x
done

Equally, we can skip in and surround the reference to d with parenthesis:

#!/bin/zsh

letters=(a b c d)

for x ($letters)
do
    echo $x
done

The Bash syntax is a bit more complex:

#!/bin/bash

letters=(a b c d)

for x in "${letters[@]}"
do
    echo $x
done

Note that Zsh recognizes the last form too.

6. Splitting and Globbing

Although string splitting and globbing are features in their own right, we often use their result to feed the loop.

6.1. String Splitting

The splitting occurs in every place where multiple items are expected, as in the loop item list. As a result, a string is divided into words. Usually, the delimiter is a whitespace character as space, tab, or newline. We can change it by setting the IFS (Internal Field Separator) variable.

Let’s run an example script string_split in Bash:

#!/bin/bash

text="a b c d"

for x in $text
do
    echo $x
done

As we loop over words that result from splitting the text variable, we’ll obtain the following:

$ ./string_split
a
b
c
d

But when we run this script with Zsh (by changing the shebang from #!/bin/bash to #!/bin/zsh), the result is different:

% ./string_split
a b c d

Unlike Bash, Zsh doesn’t split strings by default. Therefore, we need to set the shell option shwordsplit first. Let’s check the string_split.zsh script::

#!/bin/zsh

text="a b c d"

setopt shwordsplit

for x in $text
do
    echo $x
done

unsetopt shwordsplit # unset when unnecessary

Finally, we’ll get the desired result:

% ./string_split.zsh
a
b
c
d

6.2. Globbing

The glob term refers to recognizing text as a pattern followed by expanding matching filenames. Let’s take a look at perhaps the most common example:

$ for file in *; do echo $file; done

The star symbol is expanded to a list filled with the names of all files in the current directory. We can run this example in both Bash and Zsh with the same result. However, things get complicated when we get down to variable substitution. Let’s use a variable named pattern with string  * as a value and check Bash:

$ pattern=*; for file in $pattern; do echo $file; done
array1.zsh
array.zsh
glob.zsh
string_split
string_split.zsh

So, we’ve got the same result as in the bare star symbol case. Now let’s give Zsh the whirl:

% pattern=*; for file in $pattern; do echo $file; done
*

By default, the variable value is printed literally, without any expansion. Similar to string splitting, we can change this behavior by setting an appropriate option, globsubst in this case:

#!/bin/zsh

pattern=*

setopt globsubst

for x in $pattern
do
    echo $x
done

unsetopt globsubst # unset when unnecsssary

6.3. More on Zsh Splitting and Globbing

As we’ve observed, Bash performs string splitting and filename expansion by default. In Zsh, we can enable both features using shell options. However, we have more straightforward possibilities with special syntax.

So, with $=variable, we can perform word splitting:

% letters="a b c d"; for letter ($=letters); do echo $letter; done
a
b
c
d

With $~variable, we ask for globbing:

% pattern=*; for file ($~pattern); do echo $file; done
array1.zsh
array.zsh
glob.zsh
string_split
string_split.zsh

Finally, we can use $~=variable to perform both. Here is a rather silly example, which lists the content of the current directory twice:

% pattern="* *"; for file ($=~pattern); do echo $file; done
array1.zsh
array.zsh
glob.zsh
string_split
string_split.zsh
array1.zsh
array.zsh
glob.zsh
string_split
string_split.zsh

7. Conclusion

In this article, we looked at looping, string splitting, and globbing in the Bash and Zsh shells. First, we studied the for loop syntax and highlighted the Zsh specifics. Then, we moved to string splitting and globbing, using both these features to fuel loops.

We highlighted that, unlike Bash, Zsh didn’t perform splitting and globbing operations by default. Finally, we learned the related, specific forms of Zsh variable reference operator $.