Tuesday, July 22, 2008

Fancy Globbing With Zsh On Linux and Unix

Hey there,

Today we're going to look at some stuff that's probably not much of an education for the zsh aficionado, but that I find pretty cool (Considering that I'm hopelessly stuck on the Korn shell no matter how many so-called "improvements" they keep making to all the other ones ;) I did what I could to verify that this stuff only works with zsh (or, doesn't work on sh, ksh, ash, bash, etc - didn't test csh/tcsh) and hope you find it as interesting as I did ...and do :) Zsh actually has quite a few features which (while you can get around them other ways in other shells) are very convenient for everyday use.

We started looking at this stuff in an older post regarding I/O redirection differences between shells and will definitely come back around to that. But, for today, globbing is going to give us plenty of material :)

1. Filename globbing: Aside from the standard "ls *.sh" type of globbing done by most shells (which it supports), zsh also supports extended globbing. It generally isn't turned on by default, but if you just type this on the command line (or add it to your .zshrc), you can do a lot of cool things:

zsh # setopt extendedglob

Now, since we're trying not to cover stuff other shells can do (and this is the last time I'm going to write that ;), we'll blow by *.extension and *.[range] type matches, as these can be done in a few other shells. With extended globbing in zsh, you can do all sorts of other things, as well.

2. Listing opposite globs (match everything but the glob pattern using the ^ symbol, like in a sed range):

zsh # ls 
a.txt b.wri c.txt d.doc
zsh #ls ^*.txt
b.wri d.doc


3. Listing crazy extended numeric ranges:

zsh # ls           
file1 file1234 file1235 file1236 file1278 file1299 file2
zsh # ls file<1234-1236>
file1234 file1235 file1236
zsh # ls file<1234-1299>
file1234 file1235 file1236 file1278 file1299


4. Listing files using Perl-style piped OR's:

zsh # ls
file1 file1235 file1278 file2
file1234 file1236 file1299 folder1234
zsh # ls (file|folder)1234
file1234 folder1234


5. Searching subdirectories for globbed filenames recursively (Almost like find, except for the default output style):

zsh # find . -name "*bob*"
./subdir/a/bob.txt
./subdir/ab/c/bob2.txt
./subdir/ab/bob1.txt
zsh # ls **/*bob*
subdir/a/bob.txt subdir/ab/bob1.txt subdir/ab/c/bob2.txt


6. Using qualified suffixes to pick out files of certain types. For instance, you can use the "(*)" suffix to list out all executables, or, alternately, add the "(x)" suffix to your search to do the same thing, like this:

zsh # ls -l
total 8
-rwxr-xr-x 1 mg131 techserv 3 Jul 21 14:50 file1
-r--r--r-- 1 mg131 techserv 0 Jul 21 15:07 file1234
-rw-r--r-- 1 mg131 techserv 0 Jul 21 15:08 file1235
-rw-r--r-- 1 mg131 techserv 0 Jul 21 15:07 file1236
-rw-r--r-- 1 mg131 techserv 0 Jul 21 15:07 file1278
-rw-r--r-- 1 mg131 techserv 0 Jul 21 15:07 file1299
-rwxr-xr-x 1 mg131 techserv 3 Jul 21 14:50 file2
-r--r--r-- 1 mg131 techserv 0 Jul 21 15:10 folder1234
drw-r-wr-- 4 mg131 techserv 4096 Jul 21 15:13 subdir
zsh # ls *(*)
file1 file2
zsh # ls *(x)
file1 file2


readable and writable files can also be found this way (note that the files "file1234" and "folder1234" don't show up under the write ("w") qualifier). You can also combine qualifiers:

zsh # ls *(r)
file1 file1235 file1278 file2
file1234 file1236 file1299 folder1234

subdir:
a ab
zsh # ls *(w)
file1 file1235 file1236 file1278 file1299 file2

subdir:
a ab
zsh # ls *(rwx)
file1 file2


You can also list only the contents of directories:

zsh # ls *(/)
a ab


Pretty much every symbol that you can find in the output of "ls -F" can be used as a qualifier to your ls statement in this fashion. This is how we derived the "(*)" qualifier for executable files and "(/)" for directories. The same character replacement holds true for all the different filetype indicators listed by "ls -F":

zsh # ls -F
file1* file1235 file1278 file2* subdir/
file1234 file1236 file1299 folder1234


Interesting to note is that, insofar as r, w an x are concerned, we're only matching if the file is readable, writable or executable by the owner. If you want to match for others, just use the capital version (I'm not sure you can differentiate between the "group" bit and "other" bit in this manner, but I may very well be wrong:

zsh # ls *(X)
file1 file2
zsh # ls *(Xw)
file1 file2
zsh ls *(XR)
file1 file2
zsh # ls *(XW)
zsh: no matches found: *(XW)
<-- This is okay since, according to our ls output above, no file matches this qualification (being executable by "others" AND writable by "others")

There are actually a lot more cool things you can do with zsh, and not do with any other shell, but we'll leave those for another time. Hope you enjoyed this little glob of info-tainment and can put it to some good use :)

Cheers,

, Mike