start page | rating of books | rating of authors | reviews | copyrights

Unix Power ToolsUnix Power ToolsSearch this book

9.23. Using Shell Arrays to Browse Directories

Even a graphical file manager might not be enough to help you step through a complicated directory tree with multiple layers of subdirectories. Which directories have you visited so far, and which are left to go? This article shows a simple way, using shell arrays, to step through a tree directory-by-directory. The technique is also good for stepping through lists of files -- or almost any collection of things, over a period of time -- of which you don't want to miss any. At the end are a couple of related tips on using arrays.

9.23.1. Using the Stored Lists

Let's start with a quick overview of expanding array values; then we'll look at specifics for each shell. A dollar sign ($) before the name of a shell variable gives you its value. In the C shells and zsh, that gives all members of an array. But, in the Korn shell and bash2, expanding an array value without the index gives just the first member. To pick out a particular member, put its number in square brackets after the name; in ksh and bash2, you also need to use curly braces ({}). A hash mark (#) gives the number of members. Finally, you can use range operators to choose several members of an array.

Here's a practical example that you might use, interactively, at a shell prompt. You're cleaning your home directory tree. You store all the directory names in an array named d. When you've cleaned one directory, you go to the next one. This way, you don't miss any directories. (To keep this simple, I'll show an example with just four directories.)

NOTE: If you don't want to use shell commands to browse the directories, you could use a command to launch a graphical file browser on each directory in the array. For instance, make the nextdir alias launch Midnight Commander with mc $d[1].

Let's start with the C shell:

% set d=(`find $home -type d -print`)
% echo $#d directories to search: $d
4 directories to search: /u/ann /u/ann/bin /u/ann/src /u/ann/lib
% alias nextdir 'shift d; cd $d[1]; pwd; ls -l'
% cd $d[1]
   ...clean up first directory...
% nextdir
/u/ann/bin
total 1940
lrwxrwxrwx    1 ann    users      14 Feb  7  2002 ] -> /usr/ucb/reset
-r-xr-xr-x    1 ann    users    1134 Aug 23  2001 addup
   ...clean up bin directory...
% nextdir
/u/ann/src
   ...do other directories, one by one...
% nextdir
d: Subscript out of range.

You store the array, list the number of directories, and show their names. You then create a nextdir alias that changes to the next directory to clean. First, use the C shell's shift command; it "throws away" the first member of an array so that the second member becomes the first member, and so on. Next, nextdir changes the current directory to the next member of the array and lists it. (Note that members of a C shell array are indexed starting at 1 -- unlike the C language, which the C shell emulates, where indexes start at 0. So the alias uses cd $d[1].) At the end of our example, when there's not another array member to shift away, the command cd $d[1] fails; the rest of the nextdir alias isn't executed.

Bourne-type shells have a different array syntax than the C shell. They don't have a shift command for arrays, so we'll use a variable named n to hold the array index. Instead of aliases, let's use a more powerful shell function. We'll show ksh and bash2 arrays, which are indexed starting at 0. (By default, the first zsh array member is number 1.) The first command that follows, to store the array, is different in ksh and bash2 -- but the rest of the example is the same on both shells.

bash2$ d=(`find $HOME -type d -print`)
ksh$ set -A d `find $HOME -type d -print`
  
$ echo ${#d[*]} directories to search: ${d[*]}
4 directories to search: /u/ann /u/ann/bin /u/ann/src /u/ann/lib
$ n=0
$ nextdir( ) {
>   if [ $((n += 1)) -lt ${#d[*]} ]
>   then cd ${d[$n]}; pwd; ls -l
>   else echo no more directories
>   fi
> }
$ cd ${d[0]}
   ...clean up first directory...
$ nextdir
/u/ann/bin
total 1940
lrwxrwxrwx    1 ann    users      14 Feb  7  2002 ] -> /usr/ucb/reset
-r-xr-xr-x    1 ann    users    1134 Aug 23  2001 addup
   ...do directories, as in C shell example...
$ nextdir
no more directories

If you aren't a programmer, this may look intimidating -- like something you'd never type interactively at a shell prompt. But this sort of thing starts to happen -- without planning, on the spur of the moment -- as you learn more about Unix and what the shell can do.

9.23.2. Expanding Ranges

We don't use quite all the array-expanding operators in the previous examples, so here's a quick overview of the rest. To expand a range of members in ksh and bash2, give the first and last indexes with a dash (-) between them. For instance, to expand the second, third, and fourth members of array arrname, use ${arrname[1-3]}. In zsh, use a comma (,) instead -- and remember that the first zsh array member is number 1; so you'd use ${arrname[2-4]} in zsh. C shell wants $arrname[2-4]. If the last number of a range is omitted (like ${arrname[2-]} or $arrname[2-]), this gives you all members from 2 through the last.

Finally, in all shells except zsh, remember that expanded values are split into words at space characters. So if members of an array have spaces in their values, be careful to quote them. For instance, Unix directory names can have spaces in them -- so we really should have used cd "$d[1]" in the newdir alias and cd "${d[$n]}" in the newdir function.[39] If we hadn't done this, the cd command could have gotten multiple argument words. But it would only pay attention to the first argument, so it would probably fail.

[39]We didn't do so because the syntax was already messy enough for people getting started.

To expand a range of members safely, such as ${foo[1-3]} in bash2 and ksh, you need ugly expressions without range operators, such as "${foo[1]}" "${foo[2]}" "${foo[3]}". The C shell has a :q string modifier that says "quote each word," so in csh you can safely use $foo[1-3]:q. It's hard to quote array values, though, if you don't know ahead of time how many there are! So, using ${foo[*]} to give all members of the foo array suffers from word-splitting in ksh and bash2 (but not in zsh, by default). In ksh and bash2, though, you can use "${foo[@]}", which expands into a quoted list of the members; each member isn't split into separate words. In csh, $foo[*]:q does the trick.

-- JP



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.