Leor Zolman bought his first microcomputer (an IMSAI 8080) while in high school in L.A., carried it to M.I.T., withdrew, and wrote the BDS C compiler with it in assembly language. That was enough assembly language hacking to last a lifetime, so now he enjoys UNIX/Xenix system administration, article writing and raising his newborn daughter Katelyn. You can reach him at leor@rdpub.com or uunet!bdsoft!rdpub!leor.
Once upon a time I kept a regular written journal. Although I did have a personal computer (of the CP/M persuasion) then, it just didn't feel "personal" enough for automated journal-keeping, so I chose the venerable spiralbound notebook as my medium.
More recently, I again felt a stirring of the old journal-keeping urge. This time, however, taking advantage of my SuperSport 286 laptop seemed decidedly preferable to writing out all the entries in longhand. And (as if further rationalization were really necessary) a program to maintain a personal journal would provide an excellent opportunity to illustrate some handy C techniques.
My first two requirements for such a journal program were that it be as simple as possible to use (no new user interface to learn, for example) and that it maintain journal entries in a structured, easy-to-access manner for subsequent perusal and/or compression into archive files.
I wrote the initial version exclusively for DOS, then my wife Lisa expressed interest in using the program for herself, but not on the laptop. She wanted to use our main machine with the big VGA monitor. That system is now running SCO Xenix 24 hours a day and I've never really gotten VP/ix, the Xenix-based DOS emulator, to work for me in a satisfactory manner. So I decided to re-code the program with conditional defines to work under both DOS and Xenix.
To avoid confusion, I'll begin by describing the program from the DOS point of view only. Near the end of the column, I'll explain what differs when compiling under Xenix.
The User Design
To keep the usage simple, I named the program j and designed it to take only one optional parameter to support multiple users and/or subject classifications. All journal text is kept either in a master journal directory, defined as JOURN_DIR in the program, or in a subdirectory of JOURN_DIR.To initiate a new journal entry, just type the command
jor
j <subdir>If you omit the <subdir> parameter, the journal entry will be stored immediately in JOURN_DIR.If <subdir> is specified, then j tests for the existence of a subdirectory under JOURN_DIR having the specified name, and creates it if necessary (subject to the user's approval). The new journal entry will be placed into that subdirectory.
Next, j creates a temporary three-line ASCII file consisting of a leading blank line, a line containing the current date and time, and a trailing blank line. Then j invokes your favorite text editor on that temporary file, allowing you to compose the text of your journal entry.
The symbolic constant ED_INVOKE specifies how to invoke the text editor of choice via a call to the library function system. I use Lugaru's Epsilon, renamed as "e," and have therefore included some code specific to the DOS version of Epsilon (controlled by the symbolic constant EPSILON.)
When you have finished editing your journal entry, j checks to see if you have actually saved your changes by comparing the size of the temporary file before and after the editing session. If the size has not changed, j assumes you have intentionally scrapped your work, and terminates with a message to that effect. If not saving your work was unintentional, there's nothing j could really do about it, at that point anyway.
If j finds changes, it appends the new entry onto the end of the file containing the cumulative text of the current month's entries. All of these monthly files are kept in either the journal directory or its specified subdirectory. j creates new monthly files as necessary whenever it makes the first entry of a new month.
A Debugging Trick
When writing the j program, I knew it would be necessary to use the system library function for invoking the desired text editor. The process of building an appropriate command string to pass to the system function might require some debugging. Rather than insert statements for printing and pausing before each system call, I chose instead to create a generalized conditional macro definition to perform system calls, with optional built-in support for debugging the system-call text strings.Lines 46-48 of Listing 1 show the DBSYS parameterized macro definition used when the DEBUG constant is defined as true (non-zero). The macro, taking advantage of the comma operator, expands into a four-part compound expression. The first sub-expression is a call to printf displaying the value of the system-call command string supplied as the parameter. The second asks the user to press Return, the third waits for the user to press return, and the last sub-expression actually submits the command string to the system function for DOS to execute.
Once I was satisfied with the way j's system calls were constructed, I was able to turn off all debugging of system calls by changing the value of DEBUG to 0 (in line 27.) In this case, the definition of DBSYS turns into a straight call to system as defined in line 50.
Epsilon Specifics
The DOS version of Epsilon's shell-escape command maximizes available RAM for the subordinate shell by shuffling both itself and all text being edited out of main RAM and into either expanded memory or onto the hard disk. The annoying side effect of this feature, however, is that Epsilon refuses to start up again if invoked from the subordinate shell. Epsilon tests for a previous invocation of itself by looking in the environment for a line reading
EPSRUNS=YIf it finds such a line, Epsilon complains and quits. Rather than let Epsilon discover this condition, j checks for it on startup and terminates immediately if the environment line is found. Lines 78-83 accomplish this by iterating through the environment list (supplied by the DOS C runtime initialization code via the envp parameter to the main function) in search of the magic entry.A less troublesome feature of Epsilon I take advantage of is the option to start an editing session on a specific line of the existing text file by specifying the +n option on the command line. When compiled under DOS, j invokes Epsilon with the +3 option, to place the cursor on line 3, in the generalized editor invocation string ED_INVOKE defined in line 34.
Testing For Subdirectories
After checking for an active Epsilon session, j checks for existence of both the master directory (line 95) and any named subdirectory (lines 97-102) by calling the testdir function. testdir calls the stat function to get information about the supplied pathname parameter. If it exists and is a directory, no problem. If it exists and is not a directory, the program prints a message and terminates.If the supplied pathname does not exist at all (the stat call returned 0), then lines 172-178 ask if the user wants to create a directory with the given name. If the response is negative, the program terminates. Otherwise, the program creates the directory.
Creating The Journal Entry
j creates a temporary file (lines 107- 110) to hold the text of the new journal entry. The program uses the time, localtime, and asctime functions to obtain a time/date string that is written into the temporary file in line 114. It calls fstat to obtain the size in bytes of the resulting text file. Note that fflush must be called before fstat to guarantee that the information returned by fstat reflects the results of the most recent buffered output operations.After closing the temporary file, j creates the command string for invoking the text editor in line 121. With all preparations made, j now invokes the text editor on the temporary file.
Upon return from a successful editor invocation, the program reopens the temporary file (line 126) and calls fstat once again to obtain the file's new size. If the size has not changed, the program terminates. Otherwise, the program constructs the name of the current month's cumulative text file and executes a system command to concatenate the temporary file onto the end of the cumulative file, using the cat command (line 146).
I chose to use cat here, instead of doing all the file I/O via function calls, because cat is both ideally suited to this application and much easier to use. The cat command is standard on UNIX-compatible systems. For those who haven't written or obtained a version for DOS, mine is shown in Listing 2.
The Xenix Version
The main difference between running j under DOS and under Xenix is in the location of the master journal directory. Under DOS, this directory is in a fixed location on the hard disk. Under Xenix, the location is always relative to the invoking user's home directory path, typically specified by the shell variable $HOME. To obtain the value of $HOME, the Xenix version of j uses the getenv function.Because all file I/O is performed in directories under the user's home directory, any number of users may invoke j simultaneously. No one user should start more than a single j session at a time, however.
The character used to delimit directories in pathnames is / for UNIX-compatibles and \ for DOS. To generalize the code, the symbolic constant DIR_DELIM is set to a string containing the appropriate character. I use a string constant, instead of a character constant, to support functions such as strcat and sprintf.
Finally, I used "old style" function headers and "type-only" prototypes to keep the Xenix C compiler happy. For some reason, the Xenix C compiler accepts prototypes of the form
int foo(char *, char *);while choking on
int foo(char *str1, char *str2);Exercises
1. As written, the text-editor invocation string (ED_INVOKE) is hard-wired into the program. Different people using the program may have different editor they would prefer to use. Add support for individualized editor specifications. Here are some ideas:Under UNIX-compatible systems, you might have each user's .profile/.login file define an environment variable to serve as a customized ED_INVOKE string. j can then access the value of that string using the getenv function.
Under DOS, you can do something similar with shell variables in conjunction with my "Login" program presented in an earlier column, for example. (See CUJ, February, 1991.) Or you might make the <subdir> parameter mandatory and hard-code different ED_INVOKE strings for different specific values of <subdir>.
2. As mentioned above, the Xenix version makes the assumption that no single user has more than one j session active at any one time. To get around this limitation, the name of the temporary file used for the editing session could be constructed with help from the tmpnam system call. Modify the code to have j place the temporary file in the system /tmp directory, using a unique filename obtained via tmpnam.