#[1]Sitewide ATOM Feed [2]Sitewide RSS Feed
[3]Reason I am here
* [4]Nacho Caballero
* [5]Article Index
* [6]Tags
Master Your Z Shell with These Outrageously Useful Tips
11 January 2014
If you had previously installed Zsh but never got around to exploring
all of its magic features, this post is for you.
If you never thought of using a different shell than the one that came
by default when you got your computer, I recommend you go out and check
the Z shell. Here are some [7]Linux [8]guides that explain how to
install it and set it as your default shell. You probably have Zsh
installed you are on a Mac, but there’s nothing like the warm fuzzy
feeling of running the latest version (here’s [9]a way to upgrade using
Homebrew).
While you’re at it, you should also get oh-my-zsh, a framework that
makes Zsh easier to configure. It’s pretty easy to install, just run
this:
curl -L
https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh |
sh
echo $0
# if you don't see /bin/zsh you might need
# to open up a new window, or manually run: zsh
__________________________________________________________________
The Zsh manual is a daunting beast. Just the [10]chapter on expansions
has 32 subsections. Forget about memorizing this madness in one
sitting. Instead, we’ll focus on understanding a few useful concepts,
and referencing the manual for additional help.
The three main sections of this post are file picking, variable
transformations, and magic tabbing. If you’re pressed for time, read
the beginning of each one, and come back later to soak up the details
(make sure you stick around for the bonus tips at the end).
You only learn by doing
Reading this post will only take you 10% of the way into Zsh paradise;
to really grok what it’s all about, you need to run the commands
yourself. I’m giving you everything you need to create the file
structure that we’ll be using for the entire post. Simply copy and
paste this into your Zsh window:
# run me to get the party started
# create the folder structure
mkdir -p zsh_demo/{data,calculations}/africa/{kenya,malawi}/ zsh_demo/{data,calc
ulations}/europe/{malta,poland}/ zsh_demo/{data,calculations}/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 count=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
Your file structure should look like this
zsh_demo/{data,calculations}/{continent}{/country}/{file}.txt:
zsh_demo
├── data
│ ├── africa
│ │ ├── kenya
│ │ │ ├── literacy.txt
│ │ │ ├── income.txt
│ │ │ └── population.txt
│ │ └── ...
│ ├── asia
│ │ ├── ...
│ └── europe
│ ├── ...
└── data
├── africa
│ ├── kenya
│ │ ├── literacy_index.txt
│ │ ├── median_income.txt
│ │ └── population_by_province.txt
│ └── ...
├── ...
Although you might not initially care about continents and countries,
try to relate the examples we’ll be looking at with the type of file
structure you usually work with.
1. File picking
Warning for purists: some of the features I talk about are not
exclusive to Zsh, but I have explain them anyway before we can move on
to sexier commands.
First off, globbing! A glob is a short expression that lets you select
a bunch of files. 99% of the time, there’s an asterisk involved.
# run me please
ls zsh_demo/**/*.txt # <= this is a glob
Globs get replaced by the names of the files that match the glob
expression. For example, the glob above lists every text file located
anywhere in the zsh_demo folder. Let’s break it down to see what each
part is doing:
# list every file directly below the zsh_demo folder
ls zsh_demo
# list every file in the folders directly below the zsh_demo folder
ls zsh_demo/*
# list every file in every folder two levels below the zsh_demo folder
ls zsh_demo/*/*
# list every file anywhere below the zsh_demo folder
ls zsh_demo/**/*
# list every file that ends in .txt in every folder at any level below the zsh_d
emo folder
ls zsh_demo/**/*.txt
We already used a glob when we specified the location of each file:
for country_folder in zsh_demo/data/*/*; do
# create data files for each country
done
This loop runs six times. See for yourself:
print -l zsh_demo/data/*/*
# zsh_demo/africa/kenya
# zsh_demo/africa/malawi
# zsh_demo/asia/laos
# zsh_demo/asia/nepal
# zsh_demo/europe/malta
# zsh_demo/europe/poland
# you could use echo instead of print -l, but the folders would be
# separated by spaces instead of newlines
Glob operators
So, what else can you stick inside a glob besides asterisks? Glance at
section [11]14.8.1 of the manual if you want to know all the options.
Here are the ones that I find most useful:
# list text files that end in a number from 1 to 10
ls -l zsh_demo/**/*<1-10>.txt
# list text files that start with the letter a
ls -l zsh_demo/**/[a]*.txt
# list text files that start with either ab or bc
ls -l zsh_demo/**/(ab|bc)*.txt
# list text files that don't start with a lower or uppercase c
ls -l zsh_demo/**/[^cC]*.txt
Glob qualifiers
Now that we got the basic stuff out of the way, let’s dive a little
deeper. We previously mentioned this glob: zsh_demo/**/*, which lists
every file anywhere below the zsh_demo folder. But, what if we only
want to list folders and not regular files, or vice versa? What if we
only want to list files bigger than 3 KB? Or maybe, just the last
modified file? You can do all that in Zsh using glob qualifiers:
# show only directories
print -l zsh_demo/**/*(/)
# show only regular files
print -l zsh_demo/**/*(.)
# show empty files
ls -l zsh_demo/**/*(L0)
# show files greater than 3 KB
ls -l zsh_demo/**/*(Lk+3)
# show files modified in the last hour
print -l zsh_demo/**/*(mh-1)
# sort files from most to least recently modified and show the last 3
ls -l zsh_demo/**/*(om[1,3])
Glob qualifiers are surrounded in parentheses (), and appear at the end
of a glob to make it more stringent. Globs filter files by their name,
and glob qualifiers filter by any other attribute (file type, size,
modification date). They can be a bit confusing if you don’t know the
syntax. Consider this glob qualifier:
ls -l zsh_demo/**/*(.Lm-2mh-1om[1,3])
# you won't typically write at this level of obfuscation
ls -l zsh_demo/**/*(. Lm-2 mh-1 om [1,3])
# this is more parseable, but unfortunately Zsh doesn't allow spaces
# between qualifiers, so you'll get an error
You need to be familiar with the individual options to make sense of
this madness. Five different things are going on at the same time:
1. The . tells the glob to only show regular files (no directories,
symbolic links, or other types of files).
2. The Lm-2 tells the glob to show files smaller than 2 MB.
+ Use - for smaller, and + for greater; don’t use anything if
you want to specify the exact size (Lm2).
+ Use m for megabytes, k for kilobytes, or nothing for just
bytes (notice that these letters must appear before the sign).
3. The mh-1 tells the glob to show files modified in the last hour
+ Use - if you want files modified within the last X units of
time, and + for files modified more than X units of time ago.
+ Use M for Months, w for weeks, h for hours, m for minutes, and
s for seconds (notice that these leters must appear before the
sign).
4. The om tells the glob to sort the remaining files by their
modification date.
+ A lowercase o sorts by most recent first, to use the reverse
order, make it uppercase O.
+ Use m to sort by modification date, and L to sort by size
(oL).
5. The [1,3] tells the glob to show the first 3 files (since we just
sorted the files, these will be the most recently modified ones).
+ You can also show a single file (for example, the second one
[2])
Syntax is a trade-off between terseness and obscurity. To people that
are familiar with the Zsh jargon, the ability to combine five different
filters by only typing a few characters is an awesome feature; to
everybody else, it’s incomprehensible mumbo-jumbo. Fortunately, useful
shortcuts get used more often, and they become easier to remember.
Head over to [12]section 14.8.7 of the manual if you’d like to be
showered in details.
Pro Tip
Here’s a cool tip for all you advanced devils (feel free to skip to
section 2 if you’ve had enough file pickin’ for a day). How can we
select folders that don’t contain a given file? In the manual you’ll
find information about a qualifier called estring, which runs the code
specified by the string, and only keeps the file names that return
true. For example:
# show every continent that doesn't contain a country named malta
print -l zsh_demo/*/*(e:'[[ ! -e $REPLY/malta ]]':)
# zsh_demo/calculations/africa
# zsh_demo/calculations/asia
# zsh_demo/data/africa
# zsh_demo/data/asia
Let’s parse this magic:
* After the e, the string has to be delimited by a convenient
character (in this case, a colon :), and the code must be
surrounded by single quotes ', so the actual command is just [[ !
-e $REPLY/malta ]].
* The $REPLY variable contains every file name of the ones specified
by the glob zsh_demo/*/* in turn, but only a single file at a time.
* [[ -e file ]] is a [13]conditional expression that returns true if
the file exists. We want it to return true when the file called
malta doesn’t exist, so we reverse it with !.
* When the code is executed, the $REPLY variable takes the value of
the next file and the code is executed again.
2. Variable transformations
Modifiers
To complicate things even further (or to make them more awesome,
depending on your perspective), you can stick one more thing inside the
parentheses at the end of your globs: modifiers.
Each modifiers is preceded by a colon :, which makes them easily
distinguishable from qualifiers.
# A plain old glob
print -l zsh_demo/data/europe/poland/*.txt
# Return the file name (t stands for tail)
print -l zsh_demo/data/europe/poland/*.txt(:t)
# Return the file name without the extension (r stands for remove_extension, I t
hink)
print -l zsh_demo/data/europe/poland/*.txt(:t:r)
# Return the extension
print -l zsh_demo/data/europe/poland/*.txt(:e)
# Return the parent folder of the file (h stands for head)
print -l zsh_demo/data/europe/poland/*.txt(:h)
# Return the parent folder of the parent
print -l zsh_demo/data/europe/poland/*.txt(:h:h)
# Return the parent folder of the first file
print -l zsh_demo/data/europe/poland/*.txt([1]:h)
# Remember you can combine qualifiers and modifiers.
Modifiers are not only for globs, you can also use them with variables
(the technical term is [14]parameter expansion):
# run me, you'll like it
my_file=(zsh_demo/data/europe/poland/*.txt([1]))
# If you want to store a glob in a variable, you must use parentheses
print -l $my_file
print -l $my_file(:h) # this is the syntax we saw before
print -l ${my_file:h} # I find this syntax more convenient
print -l ${my_file(:h)} # don't mix the two, or you'll get an error
print -l ${my_file:u} # the :u modifier makes the text uppercase
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 my
favorite modifier (:s):
# run me if you like to run things
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
# let's see what we just did
grep "" zsh_demo/calculations/**/*_max_income.txt
Note: The grep "" bunch_of_files command is a quick-and-dirty way to
show the name of each file and its contents (we could have also used
head bunch_of_files, try it).
So, what’s going on here?
* Each time the for loop runs, the $file variable is set to a
different income file: zsh_demo/data/africa/kenya/income.txt.
* We use the :h modifier to get rid of the file name:
zsh_demo/data/africa/kenya/,
* and then we use the :s modifier to substitute data with
calculations: zsh_demo/calculations/africa/kenya/,
* then we store that substituted path in the $output_dir variable.
* We use the :t modifier to get the name of the country (kenya) and
we store it in the $country variable
* Then we stick a slash / between the $output_dir and $country
variables, and append _max_income.txt to get our output file path:
zsh_demo/calculations/africa/kenya/kenya_max_income.txt
* The $RANDOM variable gives you a random number every time you call
it (it’s just a quick way of generating some content).
* The right arrow > saves the calculation to the output file.
__________________________________________________________________
A few more things about the :s modifier:
* You can use any character to separate the :s and the strings:
# run me and see for yourself
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_variable:s_/_._} # path.abcd (slightly more readable)
# this is useful when you want to substitute the slash (/)
# without having to escape it with a backslash (\)
* The :s modifier only performs one substitution, if you want to do
more, use the :gs modifier (g stands for global)
# run me just for fun
my_variable="aaa"
echo ${my_variable:s/a/A/} # Aaa
echo ${my_variable:gs/a/A/} # AAA
Expansion flags
Now that you’ve learned all about glob operators, glob qualifiers, and
modifiers, let’s add one more spice to the pot: expansion flags.
# run me, it's free
# 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)
Let’s tear down the example to understand what’s going on.
* We are using a for loop to cycle through the new text files, and
we’re storing each file name in the $file variable.
echo $file
# zsh_demo/europe_malta_literacy_2014.txt
* We don’t want the whole path, so we use the :t modifier to get rid
of everything to the left of the first slash /.
echo ${file:t}
# europe_malta_literacy_2014.txt
* We use the (s) expansion flag to split the file name at each
underscore _.
echo ${(s._.)file:t}
# europe malta literacy 2014.txt
* We surround everything with parentheses so we can save it into an
array variable.
file_info=${(s._.)file:t}
echo $file_info
# the parentheses are missing, file_info contains the wrong information:
# europe_malta_literacy_2014.txt
file_info=(${(s._.)file:t})
echo $file_info
# with parentheses, it works:
# europe malta literacy 2014.txt
* We use an auxiliary variable for continent, country, and data.
Since $file_info is now an array, we can refer to its elements by
using a numeric index.
echo ${file_info[3]}
# literacy
* We use the auxiliary variables to specify the path where we want to
move the new files
zsh_demo/data/${continent}/${country}/${data}.txt.
There are a bunch of other flags described in the [15]14.3.1 section of
the manual. Check them out if you’re curious. We have already covered
the split expansion flag (s); the only other one I use is the join
expansion flag (j), which does the opposite of the split flag.
my_array=(a b c d)
echo ${(j.-.)my_array}
# a-b-c-d
# Since we are joining using dots (.), it makes more sense to
# use underscores (_) to separate the dots and the j
echo ${(j_._)my_array}
# a.b.c.d
3. Magic tabbing
Event designators
Let’s introduce the last member of our Zsh jargon family: after glob
operators and qualifiers, modifiers and expansion flags, I give you
event designators.
An event designator references one of the commands that we have
previously entered. They always 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>
Note: If you press <Enter> instead of <Tab>, the event designator will
also get replaced, but you’ll still have to press <Enter> one more time
to run it.
I find that these designators are not super useful by themselves
because pressing the up arrow key is all we need to do to pull up the
previous command (use Control R if you get tired of pressing). When
they really come in handy is to add previous arguments to our current
command.
# 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>
So, we reference previous arguments in two steps:
1. Specify which command you are interested in
+ The previous command !! is the one you’ll use most often.
+ If you want to go back farther, use the minus sign - and a
number: !-2, !-3.
+ You can also use the current command !#
2. Pick what arguments you want to reuse
+ To pick an argument from the previous command, just add a
number !!1, !!2. Use !!$ for the last argument.
+ To pick an argument from two or more commands ago, add a colon
: before the number !-2:1 (because!-21 means something else).
+ If you want to reference all the arguments, use an asterisk *
!!* !-2:*.
+ If you want skip all the arguments except the first one or
two, add a number before the asterisk !!2*, !-2:2*.
Some useful examples
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
If you don’t believe there is such a thing as being too productive,
check out the [16]history expansion section of the manual for
additional shortcuts.
Pressing <Tab> lets you expand not only old commands, but globs,
variables (when they use the ${} syntax), and even lazily-typed paths!
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!
Bonus tips
There is a ton of stuff we haven’t covered, but I can point you to
other people’s awesome tips. They are all totally worth it:
* Andrew Hays encourages us to [17]love our terminal by installing
the elegant [18]Solarized color scheme, and customizing our prompt.
Follow his instructions and make all those hours in front of your
terminal a more enjoyable experience.
* Danilo Petrozzi over at Zsh Wiki shares a powerful alternative to
global aliases. If you find yourself typing stuff like | head |
column -t | less -S at the end of your commands, [19]check out his
method to turn any sequence of characters into a convenient
snippet.
* Also at [20]Zsh Wiki, learn about a way to rename multiple files by
using the zmv command. It’s extremely convenient for replacing
spaces with underscores, changing file extensions, and renaming
files located in a nested folder structure. Always run zmv using
the -n option once, so you are know what the command will actually
do.
* We used brace expansion multiple times in this tutorial, but we
didn’t cover all of its features. Tomasz Muras wrote [21]a nice
post about it.
* I wasn’t able to find a blog post explaining [22]associative
arrays, but I’ve found them incredibly useful to deal with sets of
parameters. If there’s interest, I might go into them in a future
post.
Parting thoughts
I’ve you read this entire thing in one sitting your brain is probably
melting. Don’t fret about remembering every little detail. What’s
important is to realize that there are easy ways of doing a lot of
things that you didn’t know could be done, and recalling where to go
for additional information.
I have a file where I keep all my Zsh snippets. Every time I have to
look up the syntax for a command, I open up the file and I write it
down. I recommend you do the same. It’s the act of progressively
improving this file that will help you achieve mastery.
Feel free to leave a comment if you’d like to share a tip.
[INS: :INS]
__________________________________________________________________
Related posts
* [23]Switching Between Long and Wide Formats in R
* [24]AWK GTF! How to Analyze a Transcriptome Like a Pro - Part 3
* [25]AWK GTF! How to Analyze a Transcriptome Like a Pro - Part 2
* [26]AWK GTF! How to Analyze a Transcriptome Like a Pro - Part 1
__________________________________________________________________
*
* [27]zsh 1
* [28]tutorial 5
© 2015 Nacho Caballero
[29]RSS • [30]Twitter • [31]About
References
1.
http://reasoniamhere.com/atom.xml
2.
http://reasoniamhere.com/rss.xml
3.
http://reasoniamhere.com/
4.
http://reasoniamhere.com/about.html
5.
http://reasoniamhere.com/archive.html
6.
http://reasoniamhere.com/tags.html
7.
http://blog.coolaj86.com/articles/zsh-is-to-bash-as-vim-is-to-vi.html
8.
http://linuxg.net/how-to-install-zsh-shell-how-to-set-it-as-a-default-login-shell/
9.
http://zanshin.net/2013/09/03/how-to-use-homebrew-zsh-instead-of-max-os-x-default/
10.
http://zsh.sourceforge.net/Doc/Release/Expansion.html
11.
http://zsh.sourceforge.net/Doc/Release/Expansion.html#Filename-Generation
12.
http://zsh.sourceforge.net/Doc/Release/Expansion.html#Filename-Generation
13.
http://zsh.sourceforge.net/Doc/Release/Conditional-Expressions.html#Conditional-Expressions
14.
http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion
15.
http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion
16.
http://zsh.sourceforge.net/Doc/Release/Expansion.html#History-Expansion
17.
http://www.andrewhays.net/2012/11/29/love-your-terminal.html
18.
http://ethanschoonover.com/solarized
19.
http://zshwiki.org/home/examples/zleiab
20.
http://zshwiki.org/home/builtin/functions/zmv
21.
http://jmuras.com/blog/2012/brace-expansion-in-bash-and-zsh/
22.
http://zsh.sourceforge.net/Doc/Release/Parameters.html#Array-Parameters
23.
http://reasoniamhere.com/2013/09/26/switching-between-long-and-wide-formats-in-r
24.
http://reasoniamhere.com/2013/09/18/awk-gtf-how-to-analyze-a-transcriptome-like-a-pro-part-3
25.
http://reasoniamhere.com/2013/09/17/awk-gtf-how-to-analyze-a-transcriptome-like-a-pro-part-2
26.
http://reasoniamhere.com/2013/09/16/awk-gtf-how-to-analyze-a-transcriptome-like-a-pro-part-1
27.
http://reasoniamhere.com/tags.html#zsh-ref
28.
http://reasoniamhere.com/tags.html#tutorial-ref
29.
http://feeds.feedburner.com/TheReasonIAmHere
30.
https://twitter.com/nachocaballero
31.
http://reasoniamhere.com/about.html