Computer Science II Lab 3 Parsing the Command Line and Building Complex Programs Introduction ============ This lab will guide you through building programs which have multiple files, as well as automating the build process. This is a review of some of the CS I material, but it also pushes your uses of classes just a little farther. We will also be adding a couple of functions to ansi.h, which will allow us to hide and show the cursor. I had originally left these out, and now I want to correct that! At the end of this lab, you should have a clear picture of class scoping, how command line arguments are processed, and how to write simple makefiles. The command line parser presented in this lab will be used in future labs, so be sure you get it right! First, we do your addendum for ansi.h. Guided Lab Part 1 - A slight addition to ansi.h =============================================== In lab 1, we wrote a nifty little file which allows us to color things on the terminal and make cool animation like effects. I had omitted one sequence because, at the time, I thought it wasn't as universal as it appears to actually be. These functions are the ones which will allow us to turn the cursor on and off. That would have made the game of life so much better! Well, live and learn. We won't be using these in this actual lab, but they will be used in our in-class examples, so you may as well add them. So, simply add the following lines to ansi.h: --begin end of ansi.h-- inline std::ostream& cursorOff(std::ostream &os) { return os << "\033[?25l"; } inline std::ostream& cursorOn(std::ostream &os) { return os << "\033[?25h"; } --end end of ansi.h-- Now you can turn the cursor off with: cout << cursorOff; And you can turn the cursor on with: cout << cursorOn; Now that we're done with that, let's dive into the main part of the lab! Parsing the Command Line (Guided) ================================= As we know from class, the UNIX command line is passed into a program via the argc and argv arguments to the main function. argc contains the number of arguments passed into the progamm and argv is a ragged array of strings containing the values of the command line arguments. argv[0] is the name of the program as invoked at the command line and the rest are the space delimited words which follow the name of the program. Now, there is a general format users have come to expected. Basically there are two categories of arguments. There are unnamed arguments, which are just values, and then there are named arguments or "switches" that they can turn on. Literal values are specified by themselves, and switches begin with a -. In addition to these rules, there is a second rule. Namely, a single - denotes a single character switch, and -- denotes an entire word being used. Take for instance, the mysql command line client. I could log into a database as follows: mysql -ubob -pfoo database_name Or mysql --user bob -pfoo database_name Note how -u and --user are equivalent. This is definitely the norm! The long words are easier to remember, while the short ones are shorter for typing. So most switches come in both forms. Also because -u is a single character "-u bob" and "-ubob" are equivalent. Thus they both set the internal variable user to "bob". So now, how can we parse all that? Well, we could establish some rules. The rules go like this: 1.) If a string starts with "--" then this is a full word argument. Check the next string, if it does not start with "-" then it is the value for the switch. Thus the switch gets a flag and a value, or it's a flag with a blank value. 2.) If a string starts with "-" then this is a single character argument. If the string contains more than one character after the "-", that is the value. Otherwise the next string is the value if it does not begin with "-". The result is as above in number 1. 3.) Otherwise, the string is a value without a switch/flag. Return a blank flag and the value. Now these rules aren't terribly complex, but they can be subtle in their implementation. So it would be best to write this once and then reuse the parser. We will do this using a class. The class will keep track of our position in the argument list and let us cycle through the strings and it will convert them into the two part arguments as it goes. We will implement this in the files cmdParser.h and cmdParser.cpp. These files follow. Go ahead and type them in now. Pay close attention to how the class does its job. Look at when the variables are available, and ask questions if you don't understand how it does its job! --begin cmdParser.h-- 1 /* 2 * File: cmdParser.h 3 * Purpose: The header for a class used for parsing command line arguments. 4 * There are three kinds of arguments. The first are single letter 5 * flags and they begin with -, and then there are multiple letter 6 * flags which begin with -- and then there are non-flagged values 7 * which do not begin with -. 8 * thus -a1 and -a 1 are both "a=1" 9 * --add 1 is add=1 10 * foo is a null flag, with value "foo" 11 * Author: Robert Lowe 12 */ 13 #ifndef PARSER_H 14 #define PARSER_H 15 #include 16 17 //the command line argument returned by the parser 18 typedef struct argument { 19 std::string flag; //the argument itself (named, if there is one) 20 std::string value; //the value of the argument (if there is one) 21 } Argument; 22 23 24 class CmdParser { 25 public: 26 // Constructor 27 CmdParser(int argc, char **argv); 28 29 //pull out the next command line argument 30 Argument nextArgument(); 31 32 //returns true if there is a next argument, false otherwise 33 bool hasNext(); 34 35 private: 36 int argc; //argument count 37 char **argv; //argument values as passed into constructor 38 int index; //current index of the 39 }; 40 41 #endif --end cmdParser.h-- --begin cmdParser.cpp-- 1 #include "cmdParser.h" 2 #include 3 4 using namespace std; 5 6 // Constructor 7 CmdParser::CmdParser(int argc, char **argv) { 8 //set up the argc and argv 9 this->argc = argc; 10 this->argv = argv; 11 12 //setup the index. We skip the program name 13 index = 1; 14 } 15 16 17 //pull out the next commmand line argument 18 Argument 19 CmdParser::nextArgument() { 20 Argument result; //the result 21 22 //fail safely if someone tries to read too much 23 if(index >= argc) 24 return result; 25 26 //if block to handle arguments 27 if(argv[index][0] != '-') { 28 //handle null flags 29 result.value = argv[index]; 30 } else { 31 //this is a flag! 32 if(argv[index][1] == '-') { 33 //handle the multiple character flag 34 result.flag = argv[index]+2; 35 } else { 36 //handle single character flag 37 result.flag = argv[index][1]; 38 39 //there is a chance the value part is in the same string 40 if(argv[index][2]!='\0') { 41 result.value = argv[index] + 2; 42 } 43 } 44 } 45 46 //advance the index 47 index++; 48 49 //ok, so now if we don't yet have a value, look at the next argument. 50 //if it isn't a flag, it's a value, so pull it in! 51 if(result.value.empty() && index < argc && argv[index][0]!='-') { 52 result.value = argv[index]; 53 index++; //go to the next index 54 } 55 56 return result; 57 } 58 59 60 //returns true if there is a next argument, false otherwise 61 bool 62 CmdParser::hasNext() { 63 //basically, we are just comparing index to the argc 64 return index < argc; 65 } --end cmdParser.cpp-- Now that we have that, let's get a test running. Here's one that just dumps out the parsed values to the screen: --begin testCmdParser.cpp-- 1 #include 2 #include 3 #include "cmdParser.h" 4 5 6 using namespace std; 7 8 int main(int argc, char **argv) { 9 CmdParser args(argc, argv); //parser for command line arguments 10 Argument argument; 11 12 //process all arguments 13 while(args.hasNext()) { 14 //get the argument and show it 15 argument = args.nextArgument(); 16 17 //print the flag if there is one 18 if(!argument.flag.empty()) { 19 cout << argument.flag << ": "; 20 } 21 22 cout << argument.value << endl; 23 } 24 25 return 0; 26 } --end testCmdParser.cpp-- Now, let's test it all! Compile with: g++ testCmdParser.cpp cmdParser.cpp -o testCmdParser Run your generated program, try giving it some command line arguments and see how it works. Study the test file and make sure you understand how to use the class. Now, let's use this to write a usable program. We will write the most super deluxe hello world program known to mankind! This program lets you change the words, the color, displays a help message, and it parses command lines! Here's the program: --begin hello.cpp-- 1 /* 2 * The most elaborate hello world known to man! 3 */ 4 5 #include 6 #include "cmdParser.h" 7 #include "ansi.h" 8 9 using namespace std; 10 11 int 12 main(int argc, char** argv) { 13 string w1 = "Hello"; 14 string w2 = "World"; 15 CmdParser args(argc, argv); 16 Argument argument; 17 18 //process the arguments 19 while(args.hasNext()) { 20 //get the next argument 21 argument = args.nextArgument(); 22 23 //handle the arguments 24 if(argument.flag == "1" || argument.flag == "word1") { 25 w1 = argument.value; 26 } else if(argument.flag == "2" || argument.flag == "word2") { 27 w2 = argument.value; 28 } else if(argument.flag == "r" || argument.flag == "red") { 29 cout << red; 30 } else if(argument.flag == "g" || argument.flag == "green") { 31 cout << green; 32 } else if(argument.flag == "b" || argument.flag == "blue") { 33 cout << blue; 34 } else if(argument.flag == "h" || argument.flag == "help") { 35 cout << argv[0] << endl 36 << " -1, --word1 first word" << endl 37 << " -2, --word2 second word" << endl 38 << " -r, --red Display result in red" << endl 39 << " -b, --blue Display result in blue" << endl 40 << " -g, --green Dispay result in green" << endl 41 << " -h, --help display this message" << endl; 42 } 43 } 44 45 //print the message & return terminal to normal 46 cout << w1 << " " << w2 << endl << normal; 47 48 return 0; 49 } 50 --end hello.cpp-- To compile this one, we'll create a makefile. Recall that makefiles are used to automate the build process. Also, the indentations on the line are accomplished with literal tabs. This makefile will build both the test program and the hello program. Here it is: --begin Makefile-- 1 all: testCmdParser hello 2 3 testCmdParser: testCmdParser.o cmdParser.o 4 g++ -o testCmdParser testCmdParser.o cmdParser.o 5 6 hello: hello.o cmdParser.o 7 g++ -o hello cmdParser.o hello.o 8 9 cmdParser.o: cmdParser.cpp cmdParser.h 10 g++ -c cmdParser.cpp 11 12 testCmdParser.o: testCmdParser.cpp 13 g++ -c testCmdParser.cpp 14 15 hello.o: hello.cpp 16 g++ -c hello.cpp 17 18 clean: 19 rm -f *.o testCmdParser hello 20 --end Makefile-- Now to verify that it works, run the following commands: gmake clean gmake Correct any compile errors, and try again. From here on in, gmake is your build command! (or make if you are on a system where you have access to traditional UNIX make). Study the hello.cpp file. Make sure you understand how it works. Play around with running the program. Also study the makefile. The challenge is coming! Study the structure of this program carefully. All labs after this one will require multiple header and cpp files, make sure you understand what each are doing! A Commmand Line Calculator (Challenge) ====================================== Ok, now it's time for you to fly on your own! You will use our little command line library to build a simple calculator. Our calculator will have the following functions: -a, --add addition -s, --sub subtraction -m, --mul multiplication -d, --div division It will take in arguments from left to right and compute the result. You do not need to worry about order of operations. The basic idea is that I can type in something like this: ./calc 40 -a 2 --sub 6 -m 5 And I will get 180. ((40+2) - 6) * 5). Also, you should add a -h option which prints a helpful message about how to use your program! HINT: You will need to convert strings to numbers from the arguments. The wise student would google for "atof" and especially "using atof with c++ strings". Extra Credit Opportunity ======================== Add the following: --sin Sine (radians) --cos Cosine (radians) --tan Tangent (radians) --sqrt Square root -e, --pow Exponent 2 -e 4 is 16. These may seem easy at first, except note that you have to process things a little differently. For example. Consider the following: ./calc 50 -a --sin 1.2 Note that I have to evaluate --sin 1.2 before I can add! If you try this, I wish you luck! (It is doable using only what we have covered so far.) Good luck, and as always, Enjoy!