We finish the course with a final example which pulls together many of the concepts we have seen in recent chapters. The example is an implementation of a Line Editor which can be used to edit a specific file a line at a time. At first sight this might seem a challenging prospect. The chapter aims to show that, given suitable general purpose classes, the task is in fact relatively easy. This should serve as a demonstration of the power of Abstract Data Typing as an approach to programming and should highlight the issues of encapsulation and re-use of code.
However, in to order to build our Line editor we do need to briefly digress for a moment to consider a final new topic - the issue of command line arguments to C++ programs.
You should be aware that most operating system commands may be given "command line arguments" when they are called. For example, the UNIX cat command may take the names of files to be displayed as its arguments:
cat myprog.C notes.txt
Two common uses of command line arguments are to specify filenames which commands will then access or to give flags to commands in order to modify their behaviour in some way. For example, the ls command can be instructed to give extra information about files by using the -l flag:
ls -l
On this course we have learned how to write, compile and then run our own C++ programs from within UNIX. Running a program simply involves entering the name of the file which contains its executable binary code. It would be very useful if our own programs could be given command line arguments whenever they were run in just the same way as normal UNIX commands and if these arguments could then be used inside the program.
This is possible in C++ by declaring special arguments for the function called main(). If you remember, main is the function where execution of a C++ program begins. It turns out that, like any other function, main can be declared with arguments. These will contain the particular command line arguments that were given to UNIX when the program was first invoked. If you want to use these arguments, you must declare main in the following way.
main(int argc, char **argv) { // program code }
We are using two arguments to main, argc and argv (in fact, there can be a third which we don't need to consider here). Argc is an integer which will contain the number of command line arguments that were given to the program. You need to be a little careful here because the name of the program is ALWAYS the first argument and so argc is always >= 1. For example, if our executable is in the file a.out and the user enters the command a.out -q fred jim at the UNIX shell then argc would have the initial value 4 at the start of function main.
The second argument, argv, is a pointer to a pointer to a char.
char **argv;
This double pointer is no mistake. Remembering that a pointer can represent an array, you should think of argv as pointing to the beginning of an array, each of whose elements itself points at an array of characters. In other words we have an array of character strings!
This array will contain each of the arguments given to the program in the order the were given. The first string will always be the name of the program itself. Thus, for the above example, argv would point to the following.
Remembering that we can use the [] operator with pointers, we arrive at the following.
And so on.
Extending this idea, argv[3][1] is the second character in the third argument (i.e. the single letter 'i').
The following program uses argc and argv to implement the UNIX echo command in C++.
// echo command line arguments // Steve Benford - November 1991 #include <stream.h> main(int argc, char **argv) { // echo arguments that were given for(int i = 1; i < argc; i++) cout << argv[i] << " "; cout << "\\n"; }
The goal of our line editor is to allow us to edit a UNIX file. However, unlike emacs which is a "screen editor", our line editor will only let us change specific lines at one time by explicitly giving their line numbers (e.g. delete line 6). This kind of limited editor can still be useful in some circumstances and UNIX contains a well known example called ed.
Our simple line editor will support the following:
Our solution will involve two steps. First, design an Editor Abstract Data Type which supports these functions in an abstract way and then implement this as a C++ class. Second, implement an interface built on top of this ADT. The advantage of separating the interface from the rest of the implementation is that we can easily alter the interface at a later time - perhaps even replacing it with a screen interface!
The following is the C++ class definition for the Editor class.
class Editor { private: int changed; List buffer; char* filename; public: Editor(char* filename); ~Editor(); int display(int m, int n); int insert(int n); int remove(int m, int n); int copy(int m, int n, int o); int save(); int quit(); };
This class uses the new class List which implements a linked list of Strings. Although we haven't seen the code for this it is the same at the linked list of characters from chapter 7 except that the value stored in each list element is a String not a char.
The constructor function takes the name of the file to be edited (a char*). Its job is to open the file and to read its contents into an edit buffer. Display shows the text between lines n and m. Remove deletes the text between lines n and m. Insert starts inserting new text after line n. It stops when a full stop is entered at the beginning of a line, on its own. Copy copies the lines between m and n and inserts them after line o. Save saves the current buffer back to the file. Quit quits the editor. If the changes have been made since the last save, it asks the user if they want to save to the file.
The Editor uses three internal data members:
The overall approach towards implementation is the following.
We now look at the code for each Editor method in turn.
Notice how the constructor has to allocate new memory to make a permanent copy of the filename. In addition, it has to check that the file exists and is readable. Finally, it has to note that the file doesn't need saving.
// initialise editor, open and load file Editor::Editor(char* infile) { // make a permanent copy of the filename int len = strlen(infile); filename = new char[len+1]; strcpy(filename, infile); istream ifile(filename, "r"); if(!ifile.is_open()) { cerr << "couldn't open file: " << filename << "\\n"; exit(-1); } // String object for reading from file String line; int n = 1; // load file a line at a time while(ifile >> line) if(!buffer.insertN(line, n++)) { cerr << "error building buffer" << "\\n"; exit(-1); } // set indication that file hasn't been updated changed = 0; // close the file and delete the file object ifile.close(); }
The destructor needs to free the memory that was allocated for the permanent copy of the filename.
// tidy up at finish Editor::~Editor() { // delete the space allocated for the filename delete filename; }
Display simply retrieves the specified range of elements from the buffer linked list. It displays all lines found and returns a status code indicating if the specified range was out of bounds.
// display lines start to finish int Editor::display(int start, int finish) { String line; for(int i = start; i <= finish; i++) if(buffer.retrieveN(line, i)) cout << i << '\\t' << line << "\\n"; else return 0; return 1; }
Insert reads new lines from the input and inserts them into the buffer. It also has to note that the buffer now differs from the file.
// insert text at line start with "." on a line of its own int Editor::insert(int start) { String line; String end("."); while(1) { cout << "> "; cin >> line; if(line == end) { changed = 1; return 1; } if(!buffer.insertN(line, start++)) return 0; } }
Delete simply calls the deleteN function on the list.
// delete lines start to finish int Editor::remove(int start, int finish) { for (int i = start; i <= finish; i++) if(!buffer.deleteN(start)) return 0; // note that file is changed changed = 1; return 1; }
Copy is also straight forward.
int Editor::copy(int start, int finish, int destination) { String line; // first, check that the destination is not within the range if ( ( start <= destination ) && ( destination <= finish ) ) return 0; // OK, do the copying for (int i = start; i <= finish; i++) if(!buffer.retrieveN(line,i)) return 0; else if(!buffer.insertN(line, destination++)) return 0; // note that file is changed changed = 1; return 1; }
The save method indicates that the buffer is being written back to the file and then opens the file in write mode. Each line of buffer is then written to the file and the data member changed set to the value 0.
// save the current buffer contents back to the file int Editor::save() { cout << "saving file: " << filename << "\\n"; // open file in write mode ostream ofile(filename, "w"); // check if file was opened OK if(!ofile.is_open() ) { cerr << "save: file " << filename << " not opened\\n"; return 0; } // write contents of file int len = buffer.length(); String s; // retrieve each line at a time and save it for(int i = 1; i <= len; i++) if(!buffer.retrieveN(s, i)) { cerr << "save: error in writing line " << i << "\\n"; return 0; } else ofile << s << "\\n"; // close file and delete associated file object ofile.close(); // note that file no longer needs saving changed = 0; return 1; }
Finally, quit only needs to check whether the file needs saving.
// quit editor - check if file needs saving int Editor::quit() { char c; if (changed) { cout << "File has been altered - do you want to save? (y/n)\\n"; cin >> c; if(c == 'y') save(); } return 1; }
This section outlines one possible interface to the Editor class. The syntax of its commands is loosely based on the UNIX ed line editor.
Assuming that the executable program is in the file edit, the user starts the editor by entering the UNIX command:
edit <filename>
They then see a $ prompt indicating that the editor is ready to receive commands. Each command is represented by a single letter followed by appropriate line numbers. A summary of the syntax is:
d m n (display lines numbered m to n) r m n (remove lines) c m n o (copy lines) s (save) q (quit) ? (help - display the menu of commands) i m (insert - then type text ending with a '.' on a line of its own).
The first job of the interface program is to process the command line arguments to get the filename to be edited. Notice that an error message is generated if no filename is supplied. Beyond this, the program enters a continuous while loop, processing commands through a switch statement until the user selects the quit option.
// line editor interface #include <stdlib.h> #include "Editor.h" main(int argc, char **argv) { char command, ch; int start, finish, dest; // process command arguments if(argc != 2) { cerr << "usage: edit filename\\n"; exit(-1); } // initialise editor object Editor ed(argv[1]); cout << "file successfully loaded\\n"; while(1) { cout << "$ "; cin >> command; if(command == 'd') { cin >> start; cin >> finish; if(!ed.display(start, finish)) cout << "bad line number\\n"; } else if (command == 'i') { cin >> dest; cin.get(ch); if(!ed.insert(dest)) cout << "bad line number\\n"; } else if (command == 'r') { cin >> start; cin >> finish; if(!ed.remove(start, finish)) cout << "bad line number\\n"; } else if (command == 'c') { cin >> start; cin >> finish; cin >> dest; if(!ed.copy(start, finish, dest)) cout << "bad line number\\n"; } else if (command == 's') { if(!ed.save()) cout << "error saving file\\n"; } else if (command == '?') { cout << "d m n display lines m through n\\n"; cout << "i n insert text at line n\\n"; cout << "r m n remove lines m through n\\n"; cout << "c m n o copy lines m through n to o\\n"; cout << "s save file\\n"; cout << "? display this message\\n"; cout << "q quit\\n"; } else if (command == 'q') { if(!ed.quit()) cout << "error closing down editor\\n"; exit(1); } else cout << "syntax error\\n"; } // while } // main
Notes converted from troff to HTML by an Eric Foxley shell script, email errors to me!