The zsh Shell

Nacho Caballero

You looked at a tutorial, you saw a few terminal screenshots, you heard about oh-my-zsh, you wanted the pretty, you hated the bloat, you switched to zprezto, you liked the coloured chevron, you use zsh, now you need to know more.

Basically we’re going to learn a little bit about zsh and why we bothered moving off bash. In my case, I wasn’t comfortable updating the outdated version of bash that comes with OS X so I looked for alternatives and saw that zsh was more updated than bash (though not up to date).

Demo Files

Run the following commands to set up the demo environment. Feel free to not bother or to delete the folder when you see fit!

# run me to get the party started

# create the folder structure
mkdir -p zsh_demo/{data,calculations}/africa/{kenya,malawi}/ zsh_dem
o/{data,calculations}/europe/{malta,poland}/ zsh_demo/{data,calculat
ions}/asia/{nepal,laos}/

# create dummy files inside the data folder
for country_folder in zsh_demo/data/*/*; do
    dd if=/dev/zero of="${country_folder}/population.txt" bs=1024 co
    unt=1
    dd if=/dev/zero of="${country_folder}/income.txt" bs=2048 count=1
    dd if=/dev/zero of="${country_folder}/literacy.txt" bs=4096 count=1
    # we say these are dummy files because they don't have any content,
    # but we are making them occupy disk space
done

# create dummy files inside the calculations folder
for country_folder in zsh_demo/calculations/*/*; do
    touch "${country_folder}/population_by_province.txt"    # this file is empty
    dd if=/dev/zero of="${country_folder}/median_income.txt" bs=2048 count=1
    dd if=/dev/zero of="${country_folder}/literacy_index.txt" bs=4096 count=1
done

# because all the files are nested within the zsh_demo folder you will
# be able to easily delete them by running:
# rm -r zsh_demo

File Picking

We shall start off with globbing. When your pathetic attempts to use ls with other commands fail and your pitiful find -exec grep broke in several places you arrived here. Witness the asterix

ls zsh_demo/**/*.txt # <= this is a glob

Globs are variables for directories and files. The asterix after the dir name will be a stand-in for all files in that directory. Another /* will represent all files in the next directory level down. **/* will list all files anywhere below the root dir selected. **/*.txt will list of files with an extension of ‘.txt’ within the root dir. Great globs of fire, one and all.

Glob Operators

Glob Qualifiers

Glob qualifiers come at the end of the glob and are encased in parenthesis. They filter the glob by attribute and appear quite similar to the type of flags one can pass to the find command.

ls -l zsh_demo/**/*(.Lm-2mh-1om[1,3]) is something of a mouthful/handful/spoonful.foolful. Glob qualifiers cannot be spaced out in a more readable manner but can be combined so one gets a string of seeming junk. When you look a bit closer it starts to make sense like magic eye… * . - files only * Lm-2 - files smaller than 2 mb * - for smaller * + for larger * m for megabytes * k for kilobytes * mh-1 show files modified in the last hour * - files modified within the last X units of time * + files modified more than X units of time * M Months * w weeks * h hours * m minutes * s seconds * om sorts by modification date * o sorts by most recent * O reverse order * m modification date * L size * [1,3] show the first 3 files * [2] show a single file, in this case the second one

So that was pretty intense. The options aren’t that complicated when you break it down. On a final note, how would one search for directories that do not have any files within? Observe…

# show every continent that doesn't contain a country named malta
print -l zsh_demo/*/*(e:'[[ ! -e $REPLY/malta ]]':)

What happened? Parsing time :

Variable Transformations

Modifiers

To further muddy the waters of understanding you can add another thing to the parenthesis at the end of the glob. We use the : to separate the modifiers from the qualifiers.

Modifiers and qualifiers can be combined if the need arises. Modifiers are not only for globs : they can also be used with variables. This is called parameter expansion.

An example : my_file=(zsh_demo/data/europe/poland/*.txt([1])). This sets a glob to a variable. Note that you must use parenthesis when doing so.

Let’s say we wanted to calculate the maximum income for each country and store it in a file named {country}_max_income.txt in the corresponding calculations folder. We can do this easily using mu favourite modifier, :s.

for file in zsh_demo/data/**/income.txt; do
    output_dir=${file:h:s/data/calculations/}
    country=${output_dir:t}
    output_file="${output_dir}/${country}_max_income.txt"
    echo "The max salary is $RANDOM dollars" > $output_file
done

Phew! Let’s have a look at what we just created. Run this : grep "" zsh_demo/calculations/**/*_max_income.txt. Just briefly, we used the empty quotes in grep to force grep to both run and to show the name of each file with its contents. Alternatively we could try head bunch_of_files instead.

Let’s work through this :

A little more on the substitution flag. You can use any character to separate the :s and the strings :

my_variable="path/abcd"
echo ${my_variable:s/bc/BC/} # path/aBCd
echo ${my_variable:s_bc_BC_} # path/aBCd
echo ${my_variable:s/\//./} # path.abcd (escaping the slash \/)
echo ${my_variables:s_/_._} # path.abcd (slightly more readable)

In order to make multiple substitutions one must use the global flag :gs. An example :

my_variable="aaa"
echo ${my_variable:s/a/A/} #Aaa
echo ${my_variable:gs/a/A/} #AAA

Expansion Flags

After the glob operators, glob qualifiers, the modifiers you might think that’s enough of this nonsense but you would be wrong. Let’s talk ‘expansion flags’.

# Let's say somebody gave you these updated files
# and told you to replace the old ones
echo $RANDOM > zsh_demo/africa_malawi_population_2014.txt
echo $RANDOM > zsh_demo/asia_nepal_income_2014.txt
echo $RANDOM > zsh_demo/europe_malta_literacy_2014.txt
# How would you move them to their appropriate folders?
# Try this wizardry
for file in zsh_demo/*.txt; do
    file_info=(${(s._.)file:t})
    continent=$file_info[1]
    country=$file_info[2]
    data=$file_info[3]
    
    mv -f $file zsh_demo/data/${continent}/${country}/${data}.txt
done
# Check the contents of the files (.) modified (m) in the last 
# 5 minutes (m-5) to see what you just did
grep "" zsh_demo/**/*(.mm-5)

Gosh, we really need to elaborate the above.

Lovely! You can see how one could parse a whole bunch of files quickly using the filename. The s flag, split expansion, is used here but there are plenty more in the ‘14.3.1 section of the manual’ including the j flag which does the opposite :

my_array=(a b c d)
echo ${(j.-.)my_array} # a-b-c-d
echo ${(j_._)my_array} # a.b.c.d

Magic Tabbing

Event Designators

And we reach the end. The summit is in sight, the cloudy peak nearly crested. But how does one go that final step, climb the last rung, tweak the last knob (?) ? Now we turn to magic. Magic arrives in the form of event designators that references on of the commands that we have previously entered. Magic, fireworks and event designators start with a bang (!) :

# show the previous command
echo a b c
!! # instead of pressing <Enter>, press <Tab>, then press <Enter>  
# show two commands ago
echo d e f
echo g h i
!-2 # press <Tab>, then press <Enter>

Pressing rather than fills the last command in and requires other hit of that . Now, I know, these commands aren’t magic nor interesting. After all, how many of us simply hit the up key to get historic commands? Many I feel. So what?

# add the last argument
ls zsh_demo/data/asia/laos/population.txt
ls -l !!1 # press <Tab>, then press <Enter>
# add all the previous arguments
echo a b c
print -l !!* # press <Tab>, then press <Enter>

Some examples that may prove useful :

mv zsh_demo/data/asia/laos/population.txt !#1
# press <Tab>
# now you can easily change the second argument
# (use Control W to delete every up to the first slash)

ls zsh_demo/data/europe/malta/literacy.txt
awk '$1 > 3' !$
# press <Tab>
# !$ is a shortcut for !!$

ls zsh_demo/*/*/nepal/literacy.txt
ls zsh_demo/*/*/malta/literacy.txt
ls -l !-2:1
# press <Tab>
# now you can see the details of the nepal file

# Expanding more stuff
ls zsh_demo/*/*/nepal/literacy.txt
# press <Tab>

my_var="1 2 3"
echo ${my_var}
# press <Tab>

ls z/d/a/l
# press <Tab>
# Mind Blown!