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.
|