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).
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_demoWe 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.
ls -l zsh_demo/**/*<1-10>.txt - list all text
files that end with a number in the file name.ls -l zsh_demo/**/[a]*.txt - list all text files that
start the letter ‘a’.ls -l zsh_demo/**/(ab|bc)*.txt - list text files that
start with either ‘ab’ or ‘bc’.ls -l zsh_demo/**/[^cC]*.txt - list any text file that
does NOT start with either an uppercase nor lowercase ‘c’. Swish.print -l zsh_demo/**/*(/) - print only directoriesprint -l zsh_demo/**/*(.) - print only filesls -l zsh_demo/**/*(L0) - list empty filesls -l zsh_demo/**/*(Lk+3) - show files greater than
3kbprint -l zsh_demo/**/*(mh-1) - print files that have
been modified in the last hourls -l zsh_demo/**/*(om[1,3]) - sort files from most to
least recently modified and show the last 3Glob 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 :
e: after the ‘e’ the string has to be delimited by a
suitable character and the code must surrounded by single quotes.$REPLY is a variable that contains every file name of
the ones specified by the glob but only a single file at a
time.[[ -e file ]] is a conditional expression used often
with ‘if’ statements. The result of the expression will return true or
false : the ‘!’ is akin to ‘not’ so we’re looking for NOT
TRUE in this case.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.
print -l zsh_demo/data/europe/poland/*.txt plain old
glob.print -l zsh_demo/data/europe/ploand/*.txt(:t) return
the file name (t stands for tail).print -l zsh_demo/data/europe/poland*.txt(:t:r) return
the filename (‘t’) sans extension (‘r’).print -l zsh_demo/data/europe/poland/*.txt(:e) return
the extension only (‘e’).print -l zsh_demo/data/europe/poland/*(:h) returns the
parent folder of the file (‘h’ - head)print -l zsh_demo/data/europe/poland/*(:h:h) returns
the parent folder of the parent.print -l zsh_demo/data/europe/poland/*([1]:h) returns
the parent folder of the first fileModifiers 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.
print -l $my_fileprint -l $my_file(:h) this is the syntax are used
toprint -l ${my_file:h} instead one can encapsulate the
variable and modifier in curly bracesprint -l ${my_file(:h)} this does not work, do not mix
the two syntaxesprint -l ${my_file:u} the ‘u’ modifier changes the case
to upperLet’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
donePhew! 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 :
zsh_demo/data/africa/kenya/income.txt.:h modifier to get rid of the file name :
zsh_demo/data/africa/kenya/.:s modifier to substitute ‘data’
with ‘calculations’ : zsh_demo/calculations/africa/kenya/.
This is very much like the venerable sed.:t modifier is used to to get the name of the
country (‘kenya’) and then it’s stored in the $country variable.zsh_demo/calculations/africa/kenya/kenya_max_income.txt.>) to the $output_file.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/} #AAAAfter 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.
:t tail modifier to remove everything left
of the first slash.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.dAnd 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
# 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>!! is the previous command!-2/3/4/5/6 is the second, third, fourth etc..
commands!# references the current command!!1 grabs the first argument of the previous command.
Substitute the number with whichever argument you fancy or use the last
one with !!$.!-2:1 use a colon for arguments from further back
commands because !-21 means the 21st command as opposed to
second command argument 1.!!*, !-2:* selects all the arguments of
previous commands.!!2*, !-2:2* selects all arguments bar the
first.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!