ScriptEase Tutorial and Annotated Scripts

The previous chapters defined the ScriptEase programming language; this chapter teaches ScriptEase by example. A simple text editor, named CmmEdit, will be written one step at a time, much as any program might be written. At the end of the chapter are some less complicated scripts with annotations describing how they work.

All code listed in this chapter is included with the ScriptEase files. As each stage of the editor is developed, the corresponding file is named CmmEditA.cmm, CmmEditB.cmm, CmmEditC.cmm, and so on. The final, complete editor is called CmmEdit.cmm.

We recommend that instead of using the code in the files, you enter it yourself. This will give you a much better feel for the program and its structure. You can enter the code in any text editor.

CmmEdit: A Simple Editor

Plan ahead: What should the editor do?

In general terms an editor is used to read in an ASCII text file, display it on the screen, allow users to make changes to the file, and then save it. Examples of editors are EDIT that comes with DOS, and NOTEPAD that comes with Windows. We will call our editor CmmEdit.

The key to CmmEdit is that it will be simple. The user will start the program in this manner (assuming the ScriptEase for DOS version):

SEDOS CmmEdit [filename]

If filename is not supplied then CmmEdit will prompt for one. Initially, there will be no fancy editor features such as search or replace, paragraph or line-wrapping, marking, moving, macros, etc... All CmmEdit is intended to do is to read in a file, edit the file, and write out the edited text if any editing was done. The keyboard will always be in insert mode (not overwrite). Also, this program should work in all operating systems that support ScriptEase. Finally, we're not interested here in advanced programming techniques or strict adherence to stubborn programming styles; all we want is a simple program that works!

After we have written CmmEdit and have it working, you may want to add other advanced features to the program. Go ahead and spruce it up however you like. Any enhancements to this program that you'd like to share with other ScriptEase users--indeed, any ScriptEase programs that you'd like to may be sent to us via email, our website (http://www.nombas.com/us/).

CmmEditA: Program outline

The plan for the editor in the previous section is straightforward: get the name of a file, read the file, edit the file, and write the file. Here is the code to do just that:

/*********************************************************************

*** CmmEdit - A simple text editor. This is the tutorial program ***

*** from chapter 3 of the ScriptEase manual. ***

*********************************************************************/

 

main(ArgCount,ArgList)

{

FileName = GetFileName(ArgCount,ArgList);

ReadFile(FileName);

if ( Edit() ) // Edit returns TRUE if changes made to file

WriteFile(FileName);

}

There! We have now written the program. Breaking the program into individual tasks (or pieces) and giving those tasks clear names (such as GetFileName() and ReadFile()) has turned a task that was difficult into a task that is easy.

In fact, it was too easy. If you execute this code now you will get a message like the following:

Could not locate function "GetFileName".

Error near: File "CmmEditA.CMM", Line Number 8

If this is your first program, then congratulations are in order -- you've just found your first bug. None of the functions that main() calls have been written yet, as ScriptEase informed us when we tried to execute the program.

CmmEditB: Stub routines

With a relatively complex program like this, it's best to write it in sections and test them as you go. Finding errors ("bugs") in a program can be frustrating and painstakingly slow work, so writing and testing small sections like this can save you a lot of aggravation. To prevent the processor from aborting when it tries to read a function that hasn't been written yet, we will add stubs (code that doesn't do anything) to the source file for each of the functions called. The code listed here includes the additions to CmmEditA.cmm that make CmmEditB.cmm.

GetFileName(argc,argv)

// return a file name from the program input arguments, or

// prompt user for the file name if none was supplied at the

// command line. exit() program if no file name is entered.

{

printf("Stub for GetFileName(); returning Dummy.tmp.\n");

return("Dummy.tmp"); // temporary return value for dummy stub

}

 

// read FileSpec into global data. exit() if error.

ReadFile(FileSpec)

{

printf("Stub to ReadFile() named %s.\n",FileSpec);

}

 

WriteFile(FileSpec) // write global data to back to FileSpec.

// exit() if error.

{

printf("Stub to WriteFile() named %s.\n",FileSpec);

}

 

Edit() // Edit file. This is were the hard work happens.

// exit() if error.

{ // Return FALSE if no editing was done, else return TRUE.

printf("Stub to edit() text.\n");

return TRUE;

}

With these lines added, the CmmEditB program will now execute. There are no errors and this program is now bug-free. CmmEditB won't actually DO anything useful, but that's OK at this stage. Note that GetFileName() had to return something, or an error would have resulted where it was called in main() expecting a return value. We are now ready to develop the individual pieces of the program--now isolated as functions--one at a time.

CmmEditC: GetFileName()

We could begin enhancing this program with any of the dummy functions. GetFileName() is a good choice because it executes first and is not too complicated. According to our plan, GetFileName() returns the filename from the input parameter (argv) (if it was supplied), otherwise it prompts for a file name.

All of the functions used in CmmEdit (except those we explicitly write ourselves, of course) are fully described in the second half of this manual.

GetFileName(argc,argv)

// return a file name from program input arguments, or prompt

// user for the file name if none was supplied at the command

// line. exit() program if no file name is entered.

{

// If at least one argument was supplied to main() in addition

// to the source file name (which is always supplied), then that

// argument is the file name.

if ( 1 < argc )

return(argv[1]);

 

// File name wasn't supplied on command line; prompt for name.

printf("Enter file name to edit: ");

filespec = gets();

if ( filespec == NULL || filespec[0] == 0 )

//quit; no name entered

exit(EXIT_FAILURE);

return(filespec);

}

Now the program does something, (though not much) and you can run it and experiment with the results. You may want to experiment by invoking the program in these ways:

SEDOS CmmEditC C:\Config.sys

SEDOS CmmEditC C:\Config.sys 2 3 4 5

SEDOS CmmEditC C:\Config.sys

SEDOS CmmEditC ""

SEDOS CmmEditC " "

SEDOS CmmEditC My dog has fleas.

SEDOS CmmEditC

You may also want to experiment with the prompt code by executing CmmEditC with no arguments (as in the final preceding example line) and entering these lines:

C:\Config.sys

config.sys

config.sys

My dog has fleas.

Or simply pressing return without entering any text. As you will see when testing CmmEditC.cmm, the GetFileName() function is not particularly robust or intelligent. There are many bad inputs you can supply to it that it will not handle very well; for instance, extra spaces at the beginning or end of the input line will throw it off. You may want to make changes now or later to clean up the input. For example, the following lines added before return(filespec); would handle some of the cases of erroneous spaces in the input line:

if ( 1 != sscanf(filespec,"%s",filespec) )

exit(EXIT_FAILURE);

This, instead, would take care of more:

if ( 1 != sscanf(filespec,"%s%s",filespec,dummy) )

exit(EXIT_FAILURE);

Experiment!

CmmEditD: The Text variable

It is not enough just to read a file, we must read it into somewhere. For this reason we will create a variable named Text. It will be an array of strings, where each string represents a line of the edit file. ReadFile() initializes this variable, Edit() edits the strings in this variable, and WriteFile() will overwrite the file with the lines in Text. Text doesn't have to be a global variable, but instead could be returned to main() by ReadFile() and passed to the other functions as an input parameter. (Similarly, GetFileName() could have set a global variable instead of returning the value.) Making Text global may be less "pure", but it seems like an easy thing to do.

Adding the following line of ScriptEase code to the front of the program initializes Text as an array of strings that currently holds only one line, and that line is a zero-length string(empty).

Text[0] = ""; // Text is an array of text strings; one for each

file line

CmmEditE: ReadFile()

You are now prepared to write ReadFile(), which will initialize Text with all of the text in the file. For files that don't exist, Text is OK as it is, it already represents a file without any text in it. If the file doesn't exist, ask the user if they want to create it. If they don't want to create a new file, the program assumes that they entered the file name in error and will exit the program.

ReadFile(FileSpec) // read FileSpec into global data. exit() if error

{

// Open the file, in text mode, for reading into Text.

fp = fopen(FileSpec,"rt");

if ( fp == NULL ) {

// The file doesn't exist, and so ask user if they want to

// quit. If they don't want to create file then simply exit

// this program. If they do want to create file then we're

// done, as Text is already initialized.

printf("File \"%s\" does not exist. Create file? Y/N ",

FileSpec);

do {

key = toupper(getch()); // uppercase to compare to Y and N

if ( key == 'N' )

exit(EXIT_FAILURE);

} while( key != 'Y' ); // wasn't Y or N, and so try again

} else {

// File opened. Read each line of file into the next element

// of Text.

for( LineCount = 0; NULL != (line = fgets(fp)); LineCount++) {

// line set to new string for next line in the text file.

// Set the next line of Text to this line.

Text[LineCount] = line;

}

fclose(fp); // Always close a file that has been opened.

}

}

CmmEditF: Testing ReadFile()

Before writing more code, we should test what we already have. This may seem overly cautious, but it can save lots of misery later.

Edit() follows ReadFile() in execution, and Edit() needs for ReadFile() to have done its job correctly. We'll put code into Edit() at this point to print out each line of Text(). This will show us if Text() has been set up as expected. Here is the new, temporary, Edit()function:

Edit() // Edit file. This is where the hard work happens.

// exit() if error.

{ // Return FALSE if no editing was done, else return TRUE.

LineTotal = 1 + GetArraySpan(Text);

printf("Edit() file has %d lines\n",LineTotal);

for( i = 0; i < LineTotal; i++ )

puts(Text[i]);

return TRUE;

}

You need a file to test this program on--one that won't hurt you if this program accidentally destroys it. Copy some simple text file into this directory and name it test. We want Edit() to display all of the lines of test.

If you run this program on test now (I strongly suggest that you do test it now) then you'll see that if test looks like this:

line 1 of test

line 2 of test

line 3 of test

line 4 of test

then the output of CmmEditF test will be this:

Edit() file has 4 lines

line 1 of test

 

line 2 of test

 

line 3 of test

 

line 4 of test

 

Stub to WriteFile() named test.

This is not what we wanted. An extra line is being displayed between each text line of test. Oh no! another bug. You can add lines at this point, such as:

printf("<%s>",Text[0]);

to help understand what is going wrong. You should also make sure you are using all of your functions correctly. If you look up gets() in the manual, you discover it leaves a newline at the end of strings. The function puts() is appending a newline after it prints a string. So, two newlines are printed for each string in test. This accounts for the extra lines.

We must decide now if we want the newline in each string of the file that is read in. We could add code to ReadFile() to remove the newline at the end of each string, such as:

if ( line[strlen(line)-1] == '\n' )

line[strlen(line)-1] = 0;

No. Let's just let the newline stay where it is. This will work OK as long as we remember, when coding the rest of the program, that this decision was made. (As the program grows, we'll get a feel for whether this was a good decision.)

CmmEditG: Fix bug from testing ReadFile()

This file fixes the problem we discovered with the debugging test we put in Edit():

Edit() // Edit file. This is where the hard work happens.

// Return FALSE if no editing was done, else return TRUE. //

// exit() if error.

{

LineTotal = 1 + GetArraySpan(Text);

printf("Edit() file has %d lines\n",LineTotal);

for( i = 0; i < LineTotal; i++ )

printf( "%s", Text[i] );

return TRUE;

}

Testing CmmEditG with the test file will show the expected output now, without added newlines. Note that, as usual, there was more than one way to fix this bug. Instead of changing puts( Text[i ]) to printf("%s",Text[i]) , it could have changed to fputs(Text[i],stdout) because fputs() does not add a newline. ( stdout is the pre-defined file handle for treating the console as if it were a file for output.)

CmmEditH: WriteFile()

WriteFile() is the opposite of ReadFile(): it writes each line from Text to the file. This is a good time to write the WriteFile() code, even though it executes after Edit(), because it is so much like ReadFile() and because it's a simple function to write. WriteFile() must open the file (deleting the previous contents of the file if it existed), write the lines of Text to the file, and then close the file.

WriteFile(FileSpec) // write global data to back to FileSpec.

// exit() if error.

{

// Open FileSpec for writing in text mode. If the file already

// exists then overwrite it. If the file doesn't exist create it.

fp = fopen(FileSpec,"wt");

if ( fp == NULL ) {

printf("\aUnable to open \"%s\" for writing.\a\n");

exit(EXIT_FAILURE);

}

 

// write every line of Text into fp

for ( i = 0; i <= GetArraySpan(Text); i++ )

fputs( Text[i], fp );

// close fp

fclose(fp);

}

You can do a quick test to see if WriteFile() is working. The first test might be to run the program on the test file and then see if that date-time stamp on test has changed. Further tests might include passing a different file name into WriteFile() and verifying that test was copied to this new file. You may also want to write protect the test file to see that this code works correctly when it cannot open the output file for writing. Finally, check to see whether CmmEditH creates an empty file when you give it the name of a file that doesn't exist.

CmmEditI: Skeleton for Edit()

Edit()is the really difficult function in this program, and that is the primary reason we have put it off until the end. You already know that the easiest way to handle a difficult task is to break it into manageable pieces. Edit() needs to handle the keyboard input and screen display, so skeletons for these functions will be written now.

Edit() // Edit file. This is where the hard work happens.

// exit() if error.

{ // Return FALSE if no editing was done, else return TRUE.

LineCount = 1 + GetArraySpan(Text); // how many lines in file

 

// Initialize screen: get its dimensions, and cursor location.

ScreenClear();

ScreenDimension = ScreenSize();

CursorCol = CursorRow = 0; // set cursor position

 

// Starting at row 0, draw all lines on screen. Initialize

// Start as structure for upper-left visible portion of file.

// Then draw the file.

Start.Row = Start.Col = 0;

DrawVisibleTextLines( Start, ScreenDimension, LineCount );

 

// FileWasEdited is boolean to say if changes made

FileWasEdited = FALSE;

 

// Stay here getting all keyboard input until escape is pressed

#define ESCAPE_KEY '\033'

while ( (key = getch()) != ESCAPE_KEY ) {

// haven't written code for keyboard input yet. If any key

// other than escape was pressed, assume that file was changed

FileWasEdited = TRUE;

}

 

// Return TRUE if file was edited, else false

ScreenClear();

return(FileWasEdited);

}

 

DrawVisibleTextLines(StartPosition,ScreenSize,TextLineCount)

// display visible portion of file. StartPosition is initial

// .row and .col that is visible. ScreenSize show .col and .row

// width and height of screen.

{

printf("Stub for DrawVisibleTextLines()\n");

}

A lot of variables were created in this code, such as ScreenDimension, CursorCol, and so forth. If this were an official, mission-critical, and shared application then we might have a section at the top of the routine to describe what each variable is and how it is used.

You may test CmmEditI to verify that it acts OK so far. It won't quit until you press escape. Notice that the DrawVisibleTextLines() routine was called from Edit() because this seemed like a process that could naturally stand alone and might be needed later. Stub code was added for that function so that CmmEditI could be tested without reporting: "Could not locate function "DrawVisibleTextLines".

CmmEditJ: DrawVisibleTextLines()

This function will draw all the lines in Text that are visible on the screen (i.e., not clipped outside of the screen region). The final version of CmmEdit will have to scroll up and down and right and left, so we need to know the row and column to start displaying from.

DrawVisibleTextLines(StartPosition,ScreenSize,TextLineCount)

// display visible portion of file. StartPosition is initial

// .row and .col that is visible. ScreenSize show .col and .row

// width and height of screen.

{

// verify that the screen position is not invalid;

// negative would be bad.

assert( 0 <= StartPosition.row && 0 <= StartPosition.col );

// Also, this function assumes that at least some lines are

// visible at the top of the screen, So verify that this is true.

assert( StartPosition.row < TextLineCount );

 

// draw all visible lines from Text; leave bottom line free

// for messages.

for ( row = 0; row < (ScreenSize.row-1); row++ ) {

Line = Text[StartPosition.row + row];

// draw this line on the screen from StartPosition.row,

// remembering to clip at the right edge of screen if the line

// is too long.

LineLen = strlen(Line) - StartPosition.col;

if ( 0 < LineLen ) { // only print if characters to print

ScreenCursor(0,row);

printf("%.*s",ScreenSize.col,Line + StartPosition.col);

}

}

}

The assert() statements at the beginning of this function are not necessary, in fact they slow the program down, but they are put there JUST IN CASE; that is, just to make sure that the parameters coming in to the function conform to the assumptions that were made in coding the function.

CmmEditK.cmm: CursorStatus()

The technique of only adding a little code at a time will continue here. The bottom line of the screen has been reserved for a status line. After any change in the file being edited, we want to update this status line. It is sensible to add a function that will set the status. The main thing to change in this line is the state of the cursor position. This is also a convenient place to set the cursor to the right location. Note that a couple of references are added to the Edit() function where CursorStatus() is called. It is called before keys are checked for and then again after every key is processed

CursorStatus(CRow,CCol,StartPosition,ScreenSize)

{

// show current file cursor position; based at 1

ScreenCursor(5,ScreenSize.row-1);

printf("Status: row %-3d col %-3d",CRow + 1,CCol + 1);

 

// put cursor at correct position on screen

ScreenCursor(CCol - StartPosition.Col,CRow - StartPosition.Row);

}

CmmEditL.cmm: Cursor movement

The first keys we'll handle here are cursor movement, so we can move around in the file. The cursor will move around the screen unless it extends beyond the currently visible screen when the entire screen must be redrawn. First, to make the code easier to read, some #defines are added for the codes returned by getch() for cursor-movement keys. These keys may not be returned in a straightforward way by getch(), so at this point we are going to write our own routine to get keys. Getch() will be replaced by instead calling our own routine GetKeyChar(). GetKeyChar() will return the regular key code for regular ascii keys, and extended keys for cursor-movement keys.

// define movement keys - Give values over 0x100 to distinguish

// from text

 

#define UP 0x101

#define DOWN 0x102

#define LEFT 0x103

#define RIGHT 0x104

#define PG_UP 0x105

#define PG_DN 0x106

#define HOME 0x107

#define END 0x108

#define BK_TAB 0x109

#define DELETE 0x110

 

GetKeyChar() // return key from keyboard, ascii for above #defined

{

if defined(_DOS_) || defined(_OS2_) {

// DOS and OS/2 return 0 on first getch for extended keys

KeyCode = getch();

if ( KeyCode == 0 ) {

// set value for extended key; these value found

// using KeyCode.cmd

switch( getch() ) {

case 0x48: KeyCode = UP; break;

case 0x50: KeyCode = DOWN; break;

case 0x4B: KeyCode = LEFT; break;

case 0x4D: KeyCode = RIGHT; break;

case 0x49: KeyCode = PG_UP; break;

case 0x51: KeyCode = PG_DN; break;

case 0x47: KeyCode = HOME; break;

case 0x4F: KeyCode = END; break;

case 0x0F; KeyCode = BK_TAB; break;

case 0x53; KeyCode = DELETE; break;

default: break; // return 0, which will do nothing

}

}

} else {

// Windows version

KeyCode = getch();

if ( 0x100 < KeyCode ) {

switch ( KeyCode ) {

// special values in the following table come

// from KeyCode.cmm

case 0x126: KeyCode = UP; break;

case 0x128: KeyCode = DOWN; break;

case 0x125: KeyCode = LEFT; break;

case 0x127: KeyCode = RIGHT; break;

case 0x121: KeyCode = PG_UP; break;

case 0x122: KeyCode = PG_DN; break;

case 0x124: KeyCode = HOME; break;

case 0x123: KeyCode = END; break;

case 0x109; KeyCode = BK_TAB; break;

case 0x12E; KeyCode = DELETE; break;

default: KeyCode = 0; break;

}

}

}

return(KeyCode);

}

Here is the Revised keyboad-handling code in Edit().

while ( (key = GetKeyChar()) != ESCAPE_KEY ) {

 

// special keyboard codes are returned if getch() first

switch( key ) {

case UP: CursorRow--; break;

case DOWN: CursorRow++; break;

case LEFT: CursorCol--; break;

case RIGHT: CursorCol++; break;

case HOME: CursorCol = 0; break;

case END:

// go to end of visible line, but not including newline

CursorCol = strlen(Text[CursorRow]);

if ( (0 < CursorCol) &&

( Text[CursorRow][CursorCol-1] == '\n')

CursorCol--;

break;

case PG_UP:

CursorRow -= (ScreenDimension.row - 1);

Start.Row -= (ScreenDimension.row - 1);

break;

case PG_DN:

CursorRow += (ScreenDimension.row - 1);

Start.Row += (ScreenDimension.row - 1);

break;

 

#define TABSIZE 8

case '\t':

CursorCol += TABSIZE;

CursorCol -= CursorCol % TABSIZE;

break;

case BK_TAB:

CursorCol -= TABSIZE;

CursorCol -= CursorCol % TABSIZE;

break;

 

default:

// the key that was pressed was not handled. Beep at

// the user as a warning, but otherwise alter nothing.

putchar('\a');

break;

}

 

// Check that cursor position has not gone out of range

if ( CursorRow < 0 ) CursorRow = 0;

if ( CursorCol < 0 ) CursorCol = 0;

if ( LineCount <= CursorRow ) CursorRow = LineCount - 1;

// Check that Start.Row has not gone out of range

MaxStartRow = LineCount - (ScreenDimension.row - 1)

if ( MaxStartRow < Start.Row )

Start.Row = MaxStartRow;

if ( Start.Row < 0 )

Start.Row = 0;

 

// If cursor does not now fit on visible screen, then move

// screen so that cursor does fit on it.

while( CursorRow < Start.Row ) Start.Row--;

while( CursorCol < Start.Col ) Start.Col--;

while( Start.Row + ScreenDimension.Row - 1 <= CursorRow )

Start.Row++;

while( Start.Col + ScreenDimension.Col <= CursorCol )

Start.Col++;

 

// if screen must be redrawn, then do so now

if ( DrawnStart != Start ) {

ScreenClear();

DrawVisibleTextLines( Start, ScreenDimension, LineCount );

DrawnStart = Start;

}

 

// key was processed, so redisplay screen state

CursorStatus(CursorRow,CursorCol,Start,ScreenDimension);

}

CmmView.cmm: File viewer

Notice at this point that the CmmEditL.cmm program is now a simple file viewer, that allows no editing at all. For this reason it was copied to CmmView.cmm and a few changes were made to make it clearer that this file does not change the source file. First, Edit() was changed to View(), and it doesn't return a boolean to indicate if changes were made. Second, the WriteFile() routine was removed, since it won't ever be called anyway.

CmmEditM.cmm: InsertAsciiCharacter()

Finally, here's the code to handle input of ascii characters. This code is the default: section of the switch in Edit().

default:

if ( isprint(key) ) {

InsertAsciiCharacter(key,Text[CursorRow],CursorCol++);

FileWasEdited = TRUE;

// redraw this row

ScreenCursor(0,CursorRow - Start.row);

printf("%.*s",ScreenDimension.col,Text[CursorRow]+Start.col);

} else {

// the key that was pressed was not handled. Beep at the user

// as a warning, but otherwise alter nothing.

putchar('\a');

}

break;

Of course, the InsertAsciiCharacter() function must be written (oh darn!). Remember that we're always in INSERT mode and there is a newline at the end of each line (except possibly the last line).

InsertAsciiCharacter(c,str,offset) // insert c in str at offset

{

// The newline at the end of the string can be a problem later

// for now temporarily remove the newline then we'll put it back

// in when we're done.

len = strlen(str);

AddNewLine = ( len != 0 && str[len-1] == '\n' );

if ( AddNewLine )

str[--len] = 0;

 

// If the current cursor position is longer than the line,

// then add spaces.

while( len < offset )

str[len++] = ' ';

 

// If this character won't be at end of the string, then move all

// characters from here to the end of the string one space

// forward. This may be done with a strcpy because ScriptEase

// ensures that overwriting is safe.

if ( CursorCol < len ) {

strcpy(str + offset + 1,str + offset);

len++;

}

// At last, put the character in the string

str[offset] = c;

 

if ( AddNewLine ) // put newline character back into the string

strcat(str,"\n");

}

CmmEditN.cmm: Delete and Backspace

How to delete characters? The Delete and Backspace cases will be added to the Edit() function.

#define BACKSPACE '\010'

case BACKSPACE:

// Back space is deleting from one column to the left,

// check that we're not on the first column, then move

// left a column and let control fall to DELETE

if ( --CursorCol < 0 ) {

// backspace from beginning of line;

// move to end of previous line

if ( CursorRow == 0 ) {

// cannot backup to earlier row, so do nothing

CursorCol = 0;

break;

}

CursorCol = strlen(Text[--CursorRow]) - 1;

}

case DELETE:

if ( DeleteCharacterAtCursor(CursorRow,CursorCol,LineCount) )

{

FileWasEdited = TRUE;

DrawnStart.row = -1; // force screen redraw

}

break;

Because there is no break statement at the end of the backspace section, the computer will continue interpreting and executing code until it hits the break at the end of the delete section. Here is the DeleteCharacterAtCursor() function that was just added:

DeleteCharacterAtCursor(row,col,TotalLineCount)

// delete character at cursor position. Return TRUE if character

// was deleted else return FALSE. This function may alter

// TotalLineCount.

{

str = Text[row];

len = strlen(str);

if ( row < (TotalLineCount - 1) )

len--;

 

if ( col < len ) {

// This is the simple case. copy string to this location

// from the next char.

strcpy(str + col,str + col + 1);

}

else {

// deleting from the end of the string or beyond. Must bring

// in from next row.

if ( row == (TotalLineCount - 1) )

return(FALSE); // no following text to copy to here

 

// fill in spaces from end of text to this location

for( i = len; i <= col; i++ )

str[i] = ' ';

 

// copy from next string to the end of this string

strcpy( str + col, Text[row+1] );

// One newline has been removed, there are now one fewer lines

// in the file. Copy all of the rows from this point on down

// one element in the Text array.

TotalLineCount--;

for ( i = row + 1; i < TotalLineCount; i++ )

Text[i] = Text[i+1];

SetArraySpan(Text,TotalLineCount - 1);

}

return(TRUE);

}

CmmEditO.cmm: Carriage-return, the final piece

So far, there is no way to add a newline, which is what we'd expect when the user presses a carriage-return. So this case will be added to the switch statement in Edit().

case '\r':

// Add a newline at the current position

InsertAsciiCharacter('\n',Text[CursorRow],CursorCol);

FileWasEdited = TRUE;

 

// a line must be opened up in Text, and all the

// data moved to account for it.

for( i = CursorRow + 1; i < LineCount; i++ )

strcpy(Text[i+1],Text[i]);

LineCount++;

 

// move text after cursor to next line, end this line

Text[CursorRow+1] = Text[CursorRow] + CursorCol + 1;

Text[CursorRow][CursorCol + 1] = 0;

 

// finally, move cursor to beginning of next line,

// and redraw screen

CursorRow++, CursorCol = 0;

DrawnStart.row = -1; // force screen redraw

break;

At last, finally, hooray! The editor is finished.

CmmEditP-Q.cmm: DebugPrintf() for Enter-key

Before really saying the editor is finished, we need to test it. In particular, let's test the case that was just added in CmmEditO.cmm. (You may want to copy CmmEditO.cmm to test so we have a nice big file to test with.) If you edit test now, and insert a carriage-return in a line, you'll see a problem. When a newline is added, the following line is repeated again and again. Another bug!

Suppose you were to study the code we just wrote for case '\r': and still could not figure out what went wrong. At this point you need more information about what is happening while the program is running. printf() statements are often used during debugging to show what's going on during program execution. In this program the printf()'s would mess up the display. Instead, add the following function to print debugging messages on the bottom of the screen and wait for a key and then return.

DebugPrintf(FormatString,arg1,arg2,arg3/*etc...*/)

// printf() line on bottom of string, then get key

{

// format message into a string using vsprintf

va_start(VaList,FormatString);

vsprintf(msg,FormatString,VaList);

va_end(VaList);

 

// Save the cursor position. Display this message on the bottom

// of the screen, get a key, and then return. This is very

// non-intrusive.

SaveCursor = ScreenCursor();

ClearBottomLine();

msg[ScreenSize().Col - 1] = '\0'; // don't let line get too long

 

// change newlines to underbars

while ( NULL != (nl = strchr(msg,'\n')) )

nl[0] = '_';

ScreenCursor(0,ScreenSize().Row-1);

printf("%s",msg);

GetKeyChar();

ClearBottomLine();

ScreenCursor(SaveCursor.Col,SaveCursor.Row);

}

 

ClearBottomLine() // called by DebugPrintf() to clear last lie

{

ScreenCursor(0,ScreenSize().Row - 1);

printf("%*s",ScreenSize().Col-1,"");

}

DebugPrintf() can now be called from anywhere in the program to print out messages without disturbing anything but the bottom line of the screen. This is A GOOD DEBUGGING TOOL. Let's use it now to debug the carriage-return problem that we suspect is hidden in this section of code:

// a line must be opened up in Text, and all the data moved

for( i = CursorRow + 1; i < LineCount; i++ )

strcpy( Text[i+1], Text[i] );

LineCount++;

Add some DebugPrintf() statements:

// a line must be opened up in Text, and all the data moved

DebugPrintf("about to move all text lines down by one");

for( i = CursorRow + 1; i < LineCount; i++ ) {

DebugPrintf("Move \"%.20s...\" to \"%.20s...\"",

Text[i],Text[i+1]);

strcpy( Text[i+1], Text[i] );

}

LineCount++;

DebugPrintf("Finished with shifting lines down.");

Then run the program on the original file. Perhaps the information from these debug calls is enough to show what is going wrong. The problem appears to be that after the first line is copied down one line further in the Text array, then the same line is copied again and again. The problem is that we are copying from the beginning of the array to the end. So the fix, as appears in CmmEditQ.cmm, is to copy from the end to the beginning.

// a line must be opened up in Text, and all the data moved

for( i = LineCount++; CursorRow + 1 < i; i-- )

strcpy( Text[i], Text[i-1] );

Now, if we test response to carriage returns in this program, (you are strongly encouraged test! test! test!) we see that pressing the Enter key does not cause the same problem as before. The next line no longer repeats itself again and again.

Now there seems to be a new problem. When the Enter key is pressed, everything looks OK except that the new line created by carriage return is blank. That is, when pressing carriage return when the cursor is on the fourth character of this file:

Momma gets a wig.

Poppa only gets big.

we want to get:

Mom

ma gets a wig.

Poppa only gets big.

but instead, we get (due to the bug):

Mom

 

Poppa only gets big.

Again, we stare and stare at the code but cannot see where it's going wrong. So CmmEditR.cmm contains the following code changes with DebugPrintf() statements to help see what's going on during program execution.

case '\r':

// Add a newline at the current position

InsertAsciiCharacter('\n',Text[CursorRow],CursorCol);

FileWasEdited = TRUE;

 

// a line must be opened up in Text, and all the data moved

for( i = CursorRow + 1; i < LineCount; i++ )

strcpy(Text[i+1],Text[i]);

LineCount++;

 

// move text from after cursor to next line, and end this line

DebugPrintf("1 - Text[CursorRow] = \"%s\"", Text[CursorRow]);

DebugPrintf("1 - Text[CursorRow+1] = \"%s\"",

Text[CursorRow+1]);

DebugPrintf("About to set next row to beyond this

row cursor");

Text[CursorRow+1] = Text[CursorRow] + CursorCol + 1;

DebugPrintf("2 - Text[CursorRow] = \"%s\"", Text[CursorRow]);

DebugPrintf("2 - Text[CursorRow+1] = \"%s\"",

Text[CursorRow+1]);

DebugPrintf("About to null terminate this line beyond

cursor");

Text[CursorRow][CursorCol + 1] = 0;

DebugPrintf("3 - Text[CursorRow] = \"%s\"",Text[CursorRow]);

DebugPrintf("3 - Text[CursorRow+1] = \"%s\"",

Text[CursorRow+1]);

 

// finally, move cursor to beginning of next line, and redraw

// screen

CursorRow++, CursorCol = 0;

DrawnStart.row = -1; // force screen redraw

break;

You can test this code by watching what happens when you press the enter key on the first byte of the file you're editing. The DebugPrintf() messages all look OK until the last one; after the message "About to null terminate this line beyond cursor" we see that Text[CursorRow] gets terminated correctly, but Text[CursorRow+1] has somehow become null-length at the same time. It may take some thought, and some re-reading of the section in the previous chapter on Array Arithmetic, to understand why this code isn't working.

The problem is that this code:

Text[CursorRow+1] = Text[CursorRow] + CursorCol + 1;

is setting the Text[CursorRow+1] array to point into the same array data as Text[CursorRow] , but only with a different offset. So

Text[CursorRow][CursorCol + 1]

is the same data byte as

Text[CursorRow+1][0]

and so when you set one byte to zero, you're setting the other byte as well. The fix for this bug is to copy the data from one array to another, using strcpy(), instead of only setting one array to be relative to another.

CmmEdit.cmm: The final program (for now)

This final bug fix is incorporated into the final ScriptEase program: CmmEdit.cmm. This is by no means the world's greatest editor. It doesn't correctly handle tabs or other special characters, it doesn't search or replace or move blocks of text, and it's as slow as molasses.

If you intend to use this program as an editor, then it would be much more convenient under DOS or OS/2 to rename it as CmmEdit.bat or CmmEdit.cmd, respectively, and add the few lines that run ScriptEase code from a batch file (as described in chapter 5 of this manual).

You may want to improve the code now by experimenting with it--and learning in the process. You might want to speed it up, add new commands such as line-delete, handle tabs, etc... The DebugPrintf() routines have been left in, but commented out, so that if you decide to enhance the program you can easily restore DebugPrintf().

You may also consider changing the code so that each line of Text does not end in a newline. Many routines in this program may then become a lot cleaner.

What makes the program particularly slow is when the screen has to be redrawn, such as when the cursor moves beyond the edge of the screen or when text is deleted. You could speed up character deletion just be redrawing only the line where a delete occurs. For scrolling, you may want to add a call to the operating-system's routines to scroll just a section of the screen (see interrupt() for DOS and DynamicLink() for Windows and OS/2) and then only printing the new line. As mentioned at the beginning of this chapter, Nombas welcomes any additions to this program -- Let us know what you come up with!

Annotated Scripts

These scripts are all included with the ScriptEase disks. Most of them work with all operating systems; the exception is IS_DAY.BAT, a script for DOS that demonstrates how to incorporate a script into a batch file.

A note about comments in the supplied scripts: Somewhere at the beginning of each Nombas-supplied script is a comment which begins with three tildes and a number, followed by a brief description of what the script does. This peculiar comment line is used by Nombas to automatically generate its script library webpages and should be ignored.

Datediff.cmm: Comparing dates

This script compares two dates and returns the number of days between them. It expects to receive two parameters when it is called, the two dates to be compared, like this:

datediff 3/14/96 3/27/94

The first thing the script does is define the variable SecondsPerDay. Because it is defined outside of any function it is a global variable, and therefore available for all functions to use.

main() is the first function in the script. The first thing it does is make sure that you have entered the required parameters (i.e., it makes sure you have provided two dates to compare). If these two parameters are not provided, the program assumes you don't know how to use the script, so ScriptEase will beep (\a) and then print a brief message demonstrating the proper way to load the script. These commands are carried out by the first statement block in the function main(), consisting of the if statement and the statements in the braces which follow.

// Display differences in dates (in MM/DD/YYYY format).

 

SecondsPerDay = 60 * 60 * 24;

 

main(argc,argv)

{

if ( argc != 3 ) {

printf("\aDateDiff - display difference in dates\n");

printf("USAGE: SEDOS DateDiff MM/DD/YY MM/DD/YY\n");

ByeBye();

}

 

Time1 = GetTimeFromDateString(argv[1]);

Time2 = GetTimeFromDateString(argv[2]);

 

DiffTime = Time2 - Time1;

DiffDay = DiffTime / SecondsPerDay;

printf("Date difference is %d days\n",DiffDay);

}

The next two lines call the function GetTimeFromDateString() once for each of the command line parameter. Remember that argv[0] is the name of the program being called (datediff, in this case), so the parameters following it are argv[1] and argv[2]. GetTimeFromDateString() puts the dates into a format the computer can work on directly. This will be a number representing the number of seconds since a certain date, defined in your operating system (usually 1/1/1970). Dates from before this time may not be recognizeable by the computer.

The final three lines of the function are pretty straightforward; Date2 is subtracted from date one, and the result is then divided by the number of seconds in a day to get the difference in days. This result is printed out, and the script ends.

GetTimeFromDateString(String)

{

if ( 3 != sscanf(String,"%d/%d/%d",Month,Day,Year) ) {

printf("\aUnrecognized date \"%s\"\n",argv[1]);

ByeBye();

}

if ( Year < 1000 ) {

Year += ( Year < 20 ) ? 2000 : 1900 ;

}

if( Month<1 || Month>12 ) {

printf("Month %d is out of the range of 1 to 12.\n",Month);

ByeBye();

}

if( Day<1 || Day>31 ) {

printf("Day %d is out of the range of 1 to 31.\n",Day);

ByeBye();

}

 

// convert to time

tm.tm_year = Year - 1900;

tm.tm_mon = Month - 1;

tm.tm_mday = Day;

ResultTime = mktime(tm);

if ( ResultTime < 1 ) {

printf("%d/%d/%d is not within range of dates I can calculate.\n",

Month,Day,Year);

ByeBye();

}

return ResultTime;

}

The function GetTimeFromDateString() first makes sure that the date is a valid date. It reads in the date with a sscanf statement, which takes a string and a template, and assigns variable names to parts of the string according to the template. Items in the template which are to be converted to text from variables are represented with a % preceeding a character that specifies how the variable is to be represented. The string expects to have three things separated by slashes; these three things will be saved as the variables Month, Day and Year.

The sscanf function will return the number of variables it created, so if there weren't three of them, it sends back a message saying it cannot recognize the date.

The next three statement blocks make sure that the Month, Day and Year are all valid. If Year is less than 1000, it adds 1900 or 2000 (if Year is less than 20) to your date to make a complete year (e.g., 85 will be turned into 1985, and 19 will be turned into 2019). It then checks to make sure month is a number from 1 to 12, and Day is from 1 to 31.

If the data checks out, it stores it in a time structure. This structure has members for the day, month, year, and other time related elements; the whole structure can be converted into a number (the number of seconds since a particular date) with the function mktime(). These two date/time formats (the long number format and the broken down structure) are used for different things: the time structure is easier for humans to understand and allows you to extract useful information from the data (Is it Monday, e.g.). The long number form lets you add and subtract dates. For more information on the structure format, see the function localtime() in the standard library section.

Since mktime() returns 1 if it can't create a viable date/time number, the computer checks to see if 1 was returned and prints an error message to the screen. Otherwise, it returns the date/time number back to the function that called it.

ByeBye()

{

if( (defined(_WINDOWS_) || defined(_NWNLM_)) && !defined(_SHELL_)) {

printf("press any key to exit...");

getch();

}

exit(EXIT_FAILURE);

}

This final function makes sure that the script works with all operating systems. If you are running from a text-based operating system, the output will be printed on your screen. But if you are working from a windows based system and ran the script from a run line, ScriptEase will create and open a new window to display the data in. This window will close as soon as the script terminates, which happens next. If you are running this script from a windowing system, you would see the data window fly open and shut again, so quickly you don't have time to read it. The Byebye() function prevents this from happening by first checking to see if you are using a windowing system, and if you are, it prints "press any key to exit..."and waits for a user to hit a key. Then it exits.

Border.cmm: Drawing text and cursor navigation

Border.cmm is a simple script that draws a border of asterisks around the edge of the screen or active window. It first draws the top of the box, then the bottom, and finally the sides, and places the cursor in the upper left-hand corner.

if defined(_WINDOWS_)

ScreenSize(30,30);

 

DrawAsteriskBorder();

ScreenCursor(1,1);

getch();

 

DrawAsteriskBorder()

{

size = ScreenSize();

ScreenClear();

// draw top border

ScreenCursor(0,0);

for ( col = 0; col < size.col; col++ )

putchar('*');

// draw bottom border

ScreenCursor(0,size.row-2);

for ( col = 0; col < size.col; col++ )

putchar('*');

// draw left and right borders

for ( row = 0; row < size.row - 1; row++ ) {

ScreenCursor(0,row);

putchar('*');

ScreenCursor(size.col - 1,row);

putchar('*');

}

}

The first thing this script does is call the defined function to see whether the Windows version of ScriptEase is running or not. If it is, the size of the screen is set to 30 x 30. These lines demonstrate using the defined() function to allow greater control over your scripts in cross-platform environments.

The next line calls the function DrawAsteriskBorder(), which appears at the end of the script, and does the actual work of drawing the asterisks on the screen. Once the border has been drawn, the next line, (the call to ScreenCursor()), resets the cursor so that it will appear in the upper left hand corner of the recently created asterisk box, while the last line calls getch(), waiting for user to hit a key before the program exits.

The function DrawAsteriskBorder() begins by getting the size of the screen and storing it in the variable "size". Size is a structure with two elements, size.row and size.col, representing the width and height of the screen.

The screen is then cleared with a call to ScreenClear, and the cursor is set to the upper left-hand corner. A for loop is then initiated to create the top border of asterisks; starting at 0, the loop counts up to the value of the variable size.col, putting an asterisk with each iteration of the loop.

Once again, the function ScreenCursor is called to put the cursor at the bottom left of the screen. The cursor is put at 0, size.row - 2 (instead of 0, size.row) for two reasons. Firstly, the screen height is given as the number of rows on the screen, but the first row is counted as 0, not 1, so one must be subtracted from the row height. The second is that the script writes the bottom row of asterisks on the second to last row of the screen, not the last one, to leave a little space at the bottom.

The remaining part of the function draws the sides of the asterisk box. A loop is set up, just as when the top of the box was being drawn, only this time it loops once for every row of screen instead of every column. For each iteration x, it draws an asterisk at the point 0, x (the left edge of the screen), and then at (size.row-1), x. (One is subtracted from size.row for the same reason as before: counting the number of rows, the first row counted is row #1, but the first row counted in the for loop is zero.

IsDay_1.bat: Using ScriptEase with batch files

IsDay_1.bat is a file that checks whether today is a certain day or not. It is a DOS batch file with an included ScriptEase script. This script shows you how to use ScriptEase with batch files. For the most part, it is a straight ScriptEase script; three lines at the beginning of the file and one at the end are all that is needed to run a ScriptEase script with a .bat extension. You can easily convert this to a regular ScriptEase script by removing these lines.

The first four lines of code are in DOS batch language. The first merely says "don't echo these commands to the screen", and the second is a comment describing the file. The third line starts up the ScriptEase processor, sending three parameters: the name of the script being run (the first parameter sent, or "isday_1"), and up to two parameters that follow.

When a script is launched from a batch file, all of the code between the lines "GOTO SE_EXIT" and ":SE_EXIT" will be interpreted as ScriptEase code. These two phrases serve as identifiers to the processor, so each should be printed on its own line.

After DOS launches ScriptEase, ScriptEase initializes the script and begins to execute the function main(), which takes argc and argv as variables. Argc is the number of parameters passed, and argv is an array of all those parameters. Remember that the name of the script being called is a parameter (argv[0]), so if the script was correctly called:

isday_1 WED

it will return 2 for argc; argv[0] will be "isday_1" and argv[1] will be "WED".

The first thing that the script does is set the value of IsDay to 0, which is equivalent to FALSE. If the script discovers that indeed, it is the correct day, the value will be changed to 1 and this result returned to the calling program.

Then the script makes sure that it has correct number of parameters, which in this case is two: the name of the script being called, and the day being checked. If the number of parameters (argc) is not equal to two, or if there are less than three letters in the second parameter, it assumes that you don't know how to use the script and calls the function ShowHelp(), which prints out a list of instructions and terminates the script.

If the parameters check out OK, the script gets the current time and converts it into an ascii string with the two functions time() and ctime(). Time() gets the current time, as a number representing the number of seconds elapsed since the computer began keeping track, and ctime() takes this huge number and translates it into an ascii string that looks something like this:

Wed Dec 18 09:45:33 1996

Notice how the two functions are combined into one statement, with one function nested within the parenthesis of the other. Basically what this is saying is that whatever is returned from the innermost function (the long number returned from time()) will be passed as an argument to the outermost function.

The next line copies the argument argv[1] into the string "QueryDay". This helps improve legibility: when you read the variable name "QueryDay" it gives you a much better idea of what the variable represents than "argv[1]." The next line, QueryDay[3] = 0; sets the value of the fourth element of QueryDay to 0. This in effect cuts off everything after the third letter of the string, because 0 marks the end of a string.

As the comment indicates, the if statement searches for the string input as a parameter (argv[1], converted to the string QueryDay) and sees if it is contained within the larger date/time string returned above as TimeString. It first converts the two strings to all uppercase with the strupr() function, so that the search will be case sensitive. If strstr() finds anything (it will evaluate as false if it does not) it prints out the string saying "Yes, today is argv[1]" and sets the value of IsDay to 1, so that when the program exits it will return a value of 1 to any program that called it. If strstr() evaluates as false, then it prints out "No, today is not argv[1]" and exits. Since IsDay has not been changed, it will return a value of 0 (false) to the program that called it (if any).

 

@echo OFF

::~~~1 Test to see what day it is. Set ERRORLEVEL to 1 if it is, 0 if not.

 

SEDOS %0.bat %1 %2

GOTO SE_EXIT

 

main(argc,argv)

{

IsDay = 0 // Assume that it is not the correct day

if ( argc != 2 || strlen(argv[1]) < 3 ) {

// Invalid parameters were passed, so give instructions

ShowHelp()

} else {

TimeString = ctime(time()) // Standard C functions to get NOW

as a string

strcpy(QueryDay,argv[1]) // copy first argument to work with it.

QueryDay[3] = 0 // limit to search on first three letters

// case-inensitive (i.e., both string converted to upper-case)

// search for QueryDay in TimeString

if strstr(strupr(TimeString),strupr(QueryDay)) {

printf("Yes, today is %s\n",argv[1])

IsDay = 1

} else

printf("No, today is not %s\n",argv[1])

}

return IsDay

}

 

ShowHelp() // This routine is called above if input seems invalid

{

printf("\n\a") // This makes an audible beep

printf("IsDay.bat - CMM program to check if it is a certain day of

the week.\n")

printf(" Will print messages if it is or isn't, and return

with\n")

printf(" ERRORLEVEL 1 if it is the requested day, and 0 if

it is not.\n")

printf(" You must supply at least the first three letters

of the day.\n")

printf("Example:\n")

printf("\tISDAY <Friday | Fri | FRI | FRICASSE>\n\n");

}

 

:SE_EXIT

TReplace.cmm: Replace text in a document

This script opens with the function Instructions(), which prints a brief explanation of how the script is to be used:

Instructions()

{

puts("TReplace.cmm: Text replace. Replace text in a file.\a")

puts(``)

puts(`USAGE: SEDOS TReplace <filespec> <oldtext> <newtext>`)

puts(``)

puts(`WHERE: filespec - File specification for any text-mode file`)

puts(` oldtext - Any text to find in lines of filespec`)

puts(` newtext - New text to replace found old text`)

puts(``)

puts(`NOTE: This search is case-sensitive and slow, with very few `)

puts(` options, but the Cmm code is very simple.`)

puts(``)

puts(`EXAMPLE: SEDOS TReplace c:\autoexec.bat F:\UTL\CENVID F:\UTL\SEDOS`)

puts(``)

exit(EXIT_FAILURE);

}

This function is put first only for convenience: it lets people looking at the script itself have an idea of what the script is supposed to do and how it works without having to read the entire script and figure this out from the code. Instructions() will only be called if the script did not receive all of the information it needs to run. In such a case, it assumes that the user didn't know how the script is to be used and therefore needs instructions. Instructions() consists of a series of puts() statements, each of which puts a line of text to the screen.

Instructions() ends with an exit statement, so when the computer finishes writing the instructions, the script will end instead of returning to the function that called it.

#define TEMP_FILE_NAME "TREPLACE.TMP"

 

main(argc,argv)

{

if ( 4 != argc )

Instructions();

 

// Get input parameters

FileSpec = argv[1];

OldText = argv[2];

NewText = argv[3];

 

// Open old source file

SrcFP = fopen(FileSpec,"rt");

if ( !SrcFP ) {

printf("Unable to open source file \"%s\" for reading.\n\a",

FileSpec);

exit(EXIT_FAILURE);

}

The next statement in the script is a #define statement, meaning that every time the phrase "TEMP_FILE_NAME" appears in the script it will be replaced with "TREPLACE.TMP". This is the name of a temporary file the script uses. It is deleted before the script terminates.

Then the function main() begins. By default, main() is the first function called by the script, and so it receives the parameters that were entered on command line when the script was launched. The first thing main() does is check to make sure that the correct number of parameters has been entered, and prints out the instructions if it has not. The correct format for the input is:

SEDOS TREPLACE filename "cat" "dog"

The example above would replace every instance of the string "cat" with "dog" in the file "filename". Although normally this would be considered four parameters passed to the program SEDOS (SEDOS being argv[0]), the Scriptease processor understands the call to the engine implicitly, and interprets the name of the ScriptEase script being called as argv[0] instead of the engine. The parameters following the script name become argv[1], argv[2], etc, and the call to the processor (SEDOS in this case) is demoted to argv[-1]

The script then gives the command line parameters more descriptive names than "argv[1]" and "argv[3]". Argv[1], the name of the file being searched, is put into a variable filespec, the name of the text to be found is put into OldText, while the text to be replaced (argv[3]) is put into a variable called NewText.

Next, the file to be modified is opened with a call to fopen(). fopen() returns a handle to the open file (or NULL if the function was unable to open the file). Use this handle when doing anything with the contents of the file. The two parameters passed to fopen() are strings representing the name of the file to be opened and the mode the file is to be opened with (read-only, full read and write, append only, etc.). In this example the mode is "rt" to open the file for Reading in Text mode. For a list of these modes please consult the description of the function in the Standard Library chapter.

The if statement block, following the fopen() call, tests whether the file was opened incorrectly (i.e. returned 0 or NULL) and prints and error message and exits if it wasn't.

// open temporary file in same directory as source file

FileNameParts = SplitFileName(FileSpec);

sprintf(TemporaryFileSpec,"%s%s",FileNameParts.dir,TEMP_FILE_NAME);

DstFP = fopen(TemporaryFileSpec,"wt");

if ( !DstFP ) {

printf("Unable to open temporary file \"%s\" for

reading.\n\a",TemporaryFileSpec);

fclose(SrcFP);

exit(EXIT_FAILURE);

}

The above section of code opens a temporary file for the script to work in. It wants to open this file in the same directory as the source file, so it calls the function SplitFileName() to get the directory of the file being read. SplitFileName returns a structure containing the separate parts of the name of the file, the directory, name and extension.

From these parts, a new filename is created. The sprintf() function is used to format the string variable correctly, with the name of the temporary file (TEMP_FILE_NAME, defined at the top of the script) put after the directory of the original file (FileNameParts.dir). Now that a full file name has been created for the temporary file, we can open it (and make sure it was opened correctly) with a call to fopen() as we did in the preceeding section of code.

// replace all text in source

TextReplaced = WriteWithReplace(SrcFP,DstFP,OldText,NewText);

The function WriteWithReplace() is then called to do the actual text replacement. Although it doesn't appear until the end of the script, let's take a look at it now :

WriteWithReplace(src,dst,find,replace)

{

lCount = 0;

FindLen = strlen(find);

ReplaceLen = strlen(replace);

while( lFound = lLine = fgets(src) ) {

while ( lFound = strstr(lFound,find) ) {

lCount++;

strcpy(lFound+ReplaceLen,lFound+FindLen);

memcpy(lFound,replace,ReplaceLen);

lFound += ReplaceLen;

}

fputs(lLine,dst);

}

return lCount;

}

The first three lines assign values to variables: lCount is set to 0, and FindLen and ReplaceLen are set to the length of the strings find and replace, respectively, using the strlen() function to get the length of each string. The function strlen() returns the length of a string passed to it.

Although it looks complicated, the while loop that follows may be read as "while there is a string left in the file." The single equals signs are setting values, not testing them, so the variables lFound and lLine will be set to equal whatever is returned from the call to fgets(). Since fgets() returns the string that it retrieved from the file, these two variables are initialized as strings. If it is unable to return a string, it will return NULL if there was some other error and evaluate as false. As long as fgets() doesn't return 0 (false), the expression will evaluate as true and the computer will execute the commands in the loop.

The next while loop uses the strstr() function to search for one string (find) inside another (lFound, created in the previous statement). If the substring is not found, strstr returns 0, the expression evaluates as false, and the statement block is jumped over. The next line,

fputs(lLine,dst);

puts the line into the temporary destination file, and the first loop repeats again, until the fgets() function returns NULL, indicating that it has reached the end of the file (or that an error has occurred).

If the expression evaluates as true (i.e., the function finds the substring within the larger string), the loop is executed. Since strstr() returns the string beginning at the place where the substring starts, lFound now begins at the first letter of the substring.

lCount is incremented by one (lCount represents the number of times the found text is replaced, so every time ScriptEase finds the text to be replaced, it increments lCount by one).

The call to strcpy() resizes the string so that it will be the right length for the string after the replacement is made. The length of the text to be replaced is added to lFound, which resets the beginning of the string to the character just after the text to be replaced. This result is then copied back into lFound, but first it adds the length of the replacement text to lFound, leaving space for the replacement text in front of it.

This is a lot easier to understand with an example. Suppose we have a string, "Joe has bad breath." We want to replace "Joe" with "Alice". If we call the string lFound, then lFound + strlen("Joe") is equivalent to " has bad breath." The starting place of the string has been advanced by three spaces (strlen("Joe") is equal to three). (The `J' of Joe is still there, at string[-3]; however, when strings are used in operations, the negative members are ignored).

strcpy (lFound + strlen("Alice"), lFound + strlen("Joe"));

This line copies the second parameter into the first. The addition in the first parameter means to advance the starting point of the string by five spaces (the length of "Alice") before pasting in the new string. so the resulting string will be "Joe h has bad breath." The second string begins at the space after the lone letter `h'; as you can see, there are five characters before it, just the right amount of space to fit "Alice" into.

Next we call the memcpy() function to copy the replacement text into the space we just allotted for it. memcpy() copies the characters in one string into another, stopping after it has copied as many characters as the third optional parameter (if this third parameter is omitted, the entire string will be copied).

The final line of the loop,

lFound += ReplaceLen;

advances lFound to the point just past where you just inserted text, so when you test the line again, it won't test the part that you just fixed. This is only important when the text to be found is a substring of the replacement text, e.g. "North" being replaced with "North Carolina". If you didn't advance past the replacement text, it would continuously replace the "North" in "North Carolina", producing the string "North Carolina Carolina Carolina Carolina Carolina Carolina Carolina..."

The loop then repeats, going back to the strstr() statement to see if there are any more instances of the search string in the line. If so, the loop repeats; if not, the next line (after the loop) is carried out:

fputs(lLine,dst);

lLine was defined with lFound to be the string from the fgets() statement (at the very beginning of the loop). The two variable point to the same string; the only difference is that we mucked around with the starting point of lFound (with the strstr() statement), while leaving lLine alone. This line puts the string lLine into the file dst, which we opened in the main part of the script as DestFP, but passed to the function as dst.

The final line of the function returns lCount to main, so that we know how many times the string was replaced.

Having returned back to the main() function, the next lines check to see if there were any errors recorded in writing to the files. The function ferror(), passed a pointer to a file, will return False if there were no errors recorded while writing to that file. If there is, we call the function CleanUp(), which removes the temporary files we created and exits the program (the code for the CleanUp() function appears at the end of the listing).

SrcError = ferror(SrcFP);

DstError = ferror(DstFP);

if (SrcError)

CleanUp(TemporaryFileSpec);

Next, we close the files that are open.

fclose(SrcFP);

fclose(DstFP);

The variable TextReplaced, returned from the function WriteWithReplace(), represents the number of replacements that were made. If TextReplaced is true (i.e., is not equal to zero), rename the source file something else (if there's a problem later on, we'll want a backup copy) and then rename the file we just changed with the name of the original file. The function that renames a file, rename(), returns 0 for success, non-zero for failure, so rename(x,y) will evaluate as true if the computer is unable to rename the file "x" as "y". When we're done with all the renaming, call CleanUp() and exit.

if (TextReplaced){

if (DstError){

printf("Error writing to temporary file");

CleanUp(TemporaryFileSpec);

}

if (rename( FileSpec,AnotherTemporaryFile = tmpnam())){

printf("Error creating backup of original file");

CleanUp(TemporaryFileSpec, AnotherTemporaryFile);

}

if (rename (TemporaryFileSpec, FileSpec)){

printf("Unable to replace the old file with the new.")

 

if (rename(AnotherTemporaryFile, FileSpec)){

printf("The old file is saved as %s", AnotherTemporaryFile);

}

CleanUp(TemporaryFileSpec);

 

}

else CleanUp(AnotherTemporaryFileSpec);

}

else{

printf("The search string was not found.");

CleanUp(TemporaryFileSpec);

}

 

} //this is the closing brace to main()

The function CleanUp() deletes any files passed to it as parameters. Since there is no way of telling how many files will be passed to it, the parentheses following the function call are left empty, and the function's variables are accessed through the va_arg() function. (In this script, either one or two variables will be passed. However, you can use va_arg() with an indefinate number of variables).

va_arg() is first called in the for loop, where there are no parameters passed to it. In this form, va_arg() returns the number of parameters passed; it is a counterpart to the argc variable in main. This loop will run once for each parameter passed to the function.

The next time va_arg() is called, it has a parameter, which will correspond to the parameters that the function was passed. Counting starts at 0, so va_arg(0) is the first parameter, va_arg(1) is the second, and so forth. Notice that the parameters are contained in parenthesis ( () ), and not in brackets ( [] ) as is the case with main's argv[] variable. The remove function is used to delete a file.

CleanUp()

{

for (x=0; x<va_arg(); x++){

remove (va_arg(x));

}

exit(0);

}