Cover V02, I01
Article
Listing 1
Listing 2
Listing 3
Listing 4

jan93.tar


A Project Directory Management Facility

Leor Zolman

Typical UNIX users spend most of their time either in their home directories or in subdirectories thereof, but system administrators are more likely to "be all over the place" in the UNIX filesystem. In the course of a typical day of my most recent SA position, I'd usually have several major projects going at a time in a number of different system or application directories. Whenever a user came along with a software problem of some kind, I'd have to change my mental gear, switch directories over to wherever the problem was, and take care of it. Afterwards, I'd ask myself, "Now which of the six different things I'm currently juggling was I working on before that interruption?" Even if I was lucky enough to actually recall the task I was tackling, I still had to get myself back to the proper directory -- and occasionally that took some time.

The First Iteration

Since my memory isn't cut out for this sort of heavy-duty recall, several years ago I began to tinker with a shell script that performs quick directory switching among projects scattered all across the filesystem. The idea was to program some shell functions (for the Bourne shell originally, and later for the Korn shell as well) that would squirrel away pathnames for me in the environment, by request, and then later perform automatic cd commands to return me to those saved locations.

To make this facility as easy to use as possible, I created a set of tersely named shell functions using a "register" metaphor to address the pathnames to be saved. Under this metaphor, I define three scratchpad registers named x, y, and z. For each project register there were two shell function definitions in the startup .profile: setR, to assign the pathname of the current directory to register R, and goR, to change directory to the pathname stored in register R (the code for these definitions is shown in Listing 1). Now, when I was interrupted in the middle of a project, I could just type, say,

$ setx

to save my current directory in scratchpad register x. I'd then go deal with the fire, and when I was ready to go back to my original task, the command

$ gox

would return me there quickly. To examine the values of any scratchpad registers, I could simply use the echo command to display the values, e.g.,

$ echo $x

These shell functions must be explicitly defined in the startup .profile for each desired scratchpad register. Trying to implement them as external shell scripts won't work -- external shell scripts are interpreted by subordinate shells, and subordinate shells cannot directly alter the environment (current working directory or environment variables) of their parent (login) shell. Thus, any shell functions that alter the working directory must be defined within the active shell's environment.

Note that some implementations of the Bourne shell support shell functions, and some do not. If your sh accepts the commands in Listing 1 without complaint, then it supports shell functions. If not, you'll need either a newer sh or a ksh (Korn Shell).

Now Let's Get Fancy

Under the early implementation shown in Listing 1, all stored project registers were cleared at the end of a login session because the paths were saved in nothing but volatile environment variables. Storing the pathnames in environment variables only was quick, efficient, and simple, but after using setx/gox, etc. for a while, I found that what I really wanted was "persistent" pathname memory: a way to define paths and have those definitions persist across login sessions. The only way to save the path definitions is to write them out to a file for safekeeping. Thus, my new versions of the functions "mirror" all scratchpad register assignments by writing them into associated status files as well as to the original environment variables. Every time a new login session initiates, the startup code can simply define all the environmental scratchpad registers from those status files, and then any subsequent change modifies both the status file and the associated environment variable.

Next, I wanted a new category of pathname registers that I could use exclusively for long-term projects. I use the scratchpad registers primarily to move back and forth between either two, or at most three, directories in the course of working on a single project; a new set of project registers for holding pointers to my long-term projects would allow me to conceptually separate the scratchpad registers from the project registers.

I built the user interface for the project register set on two levels: at the bottom level, a single shell function named p performs all the tasks relating to definition and access of the register set. The effect of the p function depends on the number of arguments it is passed. If you give p a single argument, that argument is interpreted as the numeric ID of a project register, and p switches you to the pathname saved in that register. If you pass it two arguments, then the first argument is still a numeric register ID, and the second argument is the path you want assigned to that particular register. For example,

$ p 5 /u/leor/proj/foo

sets project register #5 to the given path. I can then quick-change to that path at any time by saying:

$ p 5

The special path code . may be used to specify the current directory. In most cases this is the way I actually use p to define a path register value.

The p function is shown in Listing 2, lines 29-57. Being one who really hates superfluous keystrokes, I added a layer on top of the p function so that I could access each project register without needing to type the space between the letter p and the register number (a bit self-indulgent, I admit, but it was a great set-up for an interesting shell programming exercise). I began by writing a driver function definition for each project register, i.e.,

p1() { p 1 $*; }
p2() { p 2 $*; }

and so on. While this did the trick, it struck me as inelegant to duplicate the same code for each desired register. Granted, even ten or fifteen of these definitions would not exactly overload the environment space, but what if I could somehow generate the definitions automatically? If I know how many project registers I want to support, there ought to be a way to iterate through each register ID value and create the required function definition on-the-fly. For a long time, however, I was stuck trying to generate an interpolation on a "variable" variable name. I wanted to glue text such as "proj" in front of the interpolated value of an environment variable named, say, value to construct the name of a new environment variable that I could then manipulate. To illustrate, if id had the value 3, then I wanted the statement

proj$id=5

to set an environment variable named proj3 to the value 5. This would be the key to a loop that would iterate through all my desired values for id and generate the required shell functions.

The line of code shown above, however, just choked the shell interpreter. And it continued to do so no matter how many pairs of curly braces, single-, double-, or back-quotes I tried inserting. If you are a true UNIX Wizard, you might very well be laughing right now at my ignorance, or at least shaking your head with pity . . . but, with the experience born of many years of UNIX exploits, I did what always seems to work more quickly and easily than attempting to decipher the man pages: I sought help. Sydney Weinstein finally provided the missing link: the shell's eval command.

eval to the Rescue

Any arbitrary piece of shell script can be constructed first, then submitted for same-level interpretation by use of the shell intrinsic eval. Thus, if id has been set to 3, then the statement

eval "proj$id=5"

submits the text

proj3=5

for evaluation, and the shell ends up successfully setting variable proj3 to the value 5. Armed with this technique, I could now automate the dynamic construction of my shell functions at the start of a login session.

The Project Directory

The final version of the project directory system (Listing 2) uses a directory named .Proj in the home directory to hold one status file for each of the currently defined scratchpad and project registers. Each status file contains a single directory path as defined by either a setR function call (for scratchpad registers) or a pR function call (for project registers).

Scratchpad Registers

The scratchpad register status files are named Rdir, where R is the name of the register and the corresponding environment variables are simply named R (to shorten the amount of typing necessary when using the environment variable name in commands). Although I use only x, y, and z as scratchpad register names, the mechanism supports as many register names as you care to list in the SCRATCHDIRS variable (line 16). Each register name may be up to 10 characters in length on systems with a 14-character filename limit. The predefined functions for each scratchpad register R are:

  • setR -- assign the current directory to register R

  • goR -- change to the directory stored in register R

  • unsetR -- delete the definition of register R

    To see the list of all currently defined scratchpad registers and their values, with the most recently accessed or defined register marked with a pointer arrow, run the external shell command shows (Listing 3).

    Project Registers

    The project register status files are named projR, where R is the numeric ID of the register and the corresponding environment variables have exactly the same names. Again, there is no hard limit on the number of digits in the project register ID, but since each register must have several shell functions defined for it, I've never found any reason to burden the environment with more than ten or so project registers at any one time (the exact number of project registers supported is set by the NPROJDIRS assignment in line 15). The four forms of calling the pR function for each project register R are:

  • pR path -- assign the specified directory to register R

  • pR . -- assign the current directory to register R (a special case of the first form, for convenience)

  • pR -- change to the directory stored in register R

  • pR done -- delete the definition of register R

    To see the list of all currently defined project registers and their values, with the most recently accessed or defined register marked with a pointer arrow, run the external shell command showp (Listing 4). Note that neither shows nor showp need to be defined in the startup .profile, since they do not try to change the current directory. These scripts may be placed anywhere along your usual search path.

    Setting It Up

    If you run either sh or ksh, you can activate all the mechanisms described here simply by inserting all of Listing 2 somewhere into your startup .profile. The code in line 60 will create the $HOME/.Proj directory for you if it does not already exist.

    If you run ksh, then you may have set up your profile so that your standard system prompt includes the name of your current working directory. If so, then you should set variable SHOWCHANGE (line 17) to N to prevent the quick-change functions from reporting the names of the directories they change to, since your prompt is already giving you that information. If you are running sh, on the other hand, then you probably want to leave SHOWCHANGE as Y; I do not know of any way to get the current directory as part of your prompt under sh (so log as you are sticking with cd to change your path. You could define a new shell function that resets PS1 each time you change directories, but you'd have to call it something other than cd under the Bourne shell, and then remember to always use it and not cd. If I'm wrong about this, please let me know.)

    Set NPROJDIRS (line 15) to the maximum number of project directories you wish to maintain, list the names of all the scratchpad registers you want in the SCRATCHDIRS string (line 16), and log out. When you log back in, you should have the full set of scratchpad and project register commands at your disposal.

    Optimizing for ksh

    These scripts work for both the Bourne and Korn shells as written. If you are running the Korn shell, you can improve performance by taking advantage of the $PWD environment variable, which always contains the name of the present working directory. Since the Bourne shell does not automatically maintain this variable, I invoke the pwd command (with back-ticks) to get the name of the current directory. That method works under both shells. Make the following changes in Listing 2 if you wish to optimize it for the Korn shell:

    Line No. For sh (as printed) For ksh, change to:
    27 ... && pwd ... ... && echo $PWD...
    41 ... target=`pwd` ... target=$PWD
    88 ... $X=\`pwd\`; ... ... $X=\$PWD; ...

    Also, the Korn shell supports the coexistence of environment variables and shell functions with identical names, but the Bourne shell does not. Thus, my project register manipulation functions have the names p1, p2, etc., but the environment variables associated with those project registers are named proj1, proj2, etc., so as not to cause name collisions under the Bourne shell. Under the Korn shell, the environment variables can be named p1, p2, etc. If you prefer that, then modify Listing 2 as follows: change each instance of the text proj$1 in lines 37 and 44 to p$1, and change each instance of proj$i in lines 75-76 to p$i.

    About the Author

    Leor Zolman wrote BDS C, the first C compiler targeted exclusively for personal computers. Leor is currently an instructor on UNIX topics for Boston University's Corporate Education Center, a regular contributor to The C Users Journal and Sys Admin magazines, and "Tech Tips" editor for Windows/DOS Developer's Journal. His first book, Illustrated C, was recently published by R&D Publications, Inc. He may be contacted at 74 Marblehead St., North Reading, MA 01864, or on Usenet/Internet as: leor@bdsoft.com.


     



  •