Globs. Fun to say and pretty darn useful too. Most of us are probably familiar with *
, which expands to all of the files in a directory, but what else can globs do for you?
recursion
Suppose you want to generate a list of all filenames with a particular extension—to create a tags file, for instance. If your files are spread throughout multiple nested directories, you might use find
to do the job:
> find . -name '*.py' -exec ctags {} +
find
is a wonderful tool that's worth learning, but zsh provides a more concise way of doing the same thing:
> ctags **/*.py
Here we see a very special glob pattern in action: **
. It expands recursively to match files in the current directory and all subdirectories. There's also ***
, which follows symbolic links while recursing.
Note that the behavior of **
isn't quite the same as find
—if you try to run ls /**/*
(that is, list every[1] file on your system), you'll probably see the message "argument list too long." That's because globs are expanded on the command line, and zsh
simply isn't equipped to handle hundreds of thousands of arguments.
However, don't discount the power of **
just yet: On my system, using **
to generate a tags file for the entire Linux kernel source tree was just as fast as find
.
case-insensitive matching
Usually case-sensitive filenames aren't much of an issue, but occasionally one runs across a heretic who seems to take pleasure in Extremely_IN-con-VEN-ient_Names
. Don't worry, no amount of CamelCasing can deter zsh[2]:
> ls (#i)foo*
FOO FOo.awk foo.c Foo.gz fOO.jar FoO.js foO.pdf fOo.wav
Those parentheses before the pattern allow you to specify additional flags that control the behavior of the match. In this case the i
flag tells zsh to ignore the case of filenames. Try combining this one with **
when you need a convenient way to find a file and you can't quite remember how it was spelled.
approximate matching
If case-insensitive matching still isn't lenient enough, you can specify how many "mistakes" are allowed in the match. Observe:
> ls (#a1)foobar
fobar foobar foobra foopar fooobar
Mistakes include transposed characters, missing characters, extra characters, and characters which differ from your original pattern.
exclusion
I've done this on more than one occasion:
> mv * subdir
mv: cannot move 'subdir' to a subdirectory of itself, 'subdir/subdir'
mv
still does what I wanted it to, but it prints an error message and returns a non-zero error code. That behavior might not be optimal in a more complex shell script where it would be nice to know if an actual problem occurred. With zsh, it's possible to run a command on every file except the one you specify.
> mv ^subdir subdir
No more complaints from mv
!
You can also use ~
to match one pattern but not another: *.gz~*.tar.gz
would match all gzipped files which are not contained in a tar
archive.
ranges
So, your grandma asked you to send her some pictures from your vacation. Ranges are your friend.
> tar czf yosemite_pics.tar.gz IMG_<3399-3412>.jpg
permissions
You can even match files based on their mode bits:
chmod 755 *.sh(f777)
There, now you don't have to worry about someone else modifying your scripts.
Those parentheses at the end of a pattern allow you to specify glob qualifiers—additional conditions that files must meet, including the type of file (symbolic link, socket, pipe, device, etc.), who owns it, and more. You can even specify an arbitrary shell command that tests whether or not to include each file in the results.
regular expressions
Have you ever wanted to delete every file with a name that starts with one or more letters, followed by zero or more digits, and optionally has an extension of .sh or .py? No? Well here it is anyway.
> rm [[:alpha:]]##[[:digit:]]#(.(sh|py))(#c0,1)
Phew. Now these are just getting ridiculous... but as you can see, you can leverage globbing to select files using just about any criteria you can imagine.
It doesn't end here...
These few examples barely scrape the surface of what zsh can do. Check out the "FILENAME GENERATION" section of the zshexpn
man page and you'll probably find something you never knew you needed. Have fun!
[1] Well, almost every file. Glob patterns don't match a leading "." by default, so hidden files won't be listed unless you enable zsh's GLOB_DOTS
option or explicitly include the leading dot in your pattern (for example, **/.*
).
[2] You'll have to turn on the EXTENDED_GLOB option to enable some of thease features. Consult your local zsh man pages for the details.