Added command history saving with rustyline
[evpf.git] / src / main.rs
index dcf215f..8e54265 100644 (file)
-use std::io;
 use regex::Regex;
+use colored::*;
+use std::env;
+use rustyline::Editor;
+use std::path::PathBuf;
 
-// describe the operators
-enum Operator {
-       NUM, ADD, SUB, MUL, DIV,
+const ILLEGAL_EXP : &'static str = "Illegal Expression";
+const ERR_PARSING_NOT_MATCH : &'static str = "Error parsing expression! \
+                                Must be a number or operator (+,-,* or /)";
+const ERR_POSTFIX_INCOMPLETE : &'static str = "Postfix expression incomplete!";
+const HELP_TEXT : [&str ; 4] = 
+                               ["Type a postfix expression to evaluate.",
+                                "Example: 4 5 + 12 -",
+                                "Supported operators: +, -, *, /",
+                                "Type q, Q to quit"
+                                ];
+                                
+const HOMEDIR_NOT_FOUND : &'static str = "User home directory not found \
+                               (missing environment?).";
+                               
+const HISTORY_FILE_NOT_FOUND : &'static str = "History file not found.";
+
+const SAVE_HISTORY_ERROR : &'static str = "Unable to save command history!";
+
+const ERROR : &'static str = "Error";
+const ERROR_HELP : &'static str = "Type ? or h or H for help";
+
+// Describe an operator - one of add, subtract, multiply or divide
+enum Operator { 
+       ADD,
+       SUB,
+       MUL,
+       DIV,
 }
 
 // structure to hold an expression in the stack
 struct Expression {
        value : f32,
-       operator : Operator,
 }
 
 // function to compute a result by popping the stack and 
 // pushing back the result
-fn get_result (t : &mut Vec::<Expression>) {
-       // pop the stack to get the operator
-       // if nothing - panic with error
-       let op = match t.pop () {
-               Some(x) => x,
-               None => panic! ("Illegal expression!"),
-       };
+fn get_result (t : &mut Vec<Expression>, op : Operator) -> Result<f32,String> {
        // pop the stack for last operand 
        // if nothing - panic with error
-       let n1 = match t.pop () { 
-               Some (x) => x,
-               None => panic! ("Illegal expression!"),
-       };
+       let n1 = t.pop ();
+       if n1.is_none () { 
+               return Err (ILLEGAL_EXP.to_string());
+       }
        // pop the stack for the first operand
        // if nothing - panic with error
-       let n2 = match t.pop () {
-               Some (x) => x,
-               None => panic! ("Illegal expression!"),
-       };
-
-       let mut res : f32 = 0.0;
+       let n2 = t.pop ();
+       if n2.is_none () {
+               return Err (ILLEGAL_EXP.to_string());
+       }
+       
+       let num1 = n1.unwrap().value;
+       let num2 = n2.unwrap().value;
        
        // depending on the operation, set the result
-       match op.operator {
-               Operator::ADD => res = n1.value + n2.value,
-               Operator::SUB => res = n2.value - n1.value,
-               Operator::MUL => res = n1.value * n2.value,
-               Operator::DIV => res = n2.value / n1.value,
-               _       => panic! ("Illegal operator in expression!"),
+       match op {
+               Operator::ADD => {                              
+                                                       t.push (Expression{value: num2 + num1});
+                                                       Ok (num2 + num1)
+                                                },
+               Operator::SUB => {                              
+                                                       t.push (Expression{value: num2 - num1});
+                                                       Ok (num2 + num1)
+                                                },
+               Operator::MUL => {                              
+                                                       t.push (Expression{value: num2 * num1});
+                                                       Ok (num2 * num1)
+                                                },
+               Operator::DIV => {                              
+                                                       t.push (Expression{value: num2 / num1});
+                                                       Ok (num2 / num1)
+                                                },
        }
-       // push the result back to the stack
-       t.push (Expression {value : res, operator : Operator::NUM });
 }
 
 // evaluation function
-fn evaluate (expr : &str) {
+fn evaluate (expr : &str, match_num : &regex::Regex) -> Result<f32,String> {
        let mut ops = Vec::<Expression>::new ();
        // tokenize the individual words by splitting at whitespace
        let words = expr.split_whitespace ();
-       // regular expression to match a number 
-       let match_num = Regex::new (r"^\d+?.*?\d*?$").unwrap ();
+
        // iterate over the words
        for word in words {
                match word {
                // if the word matches one of +, -, *, / then push it on the stack
                // and immediately evaluate the expression by popping the operator 
                // and the last two operands and push the result back on to the stack           
-               "+" => {ops.push (Expression{ value: 0.0, operator: Operator::ADD });
-                          get_result (&mut ops) },
-               "-" => {ops.push (Expression{ value: 0.0, operator: Operator::SUB });
-                          get_result (&mut ops) },
-               "*" => {ops.push (Expression{ value: 0.0, operator: Operator::MUL});
-                          get_result (&mut ops)},
-               "/" => {ops.push (Expression{ value: 0.0, operator: Operator::DIV});
-                          get_result (&mut ops) },
+               "+" => {
+                               let m = get_result (&mut ops, Operator::ADD);
+                               if ! m.is_ok () {
+                                       return Err (m.unwrap_err().to_string());
+                               }
+                          },
+               "-" => {
+                               let m = get_result (&mut ops, Operator::SUB);
+                               if ! m.is_ok () {
+                                       return Err (m.unwrap_err().to_string());
+                               }
+                          },
+               "*" => {
+                               let m = get_result (&mut ops, Operator::MUL);
+                               if ! m.is_ok () {
+                                       return Err (m.unwrap_err().to_string());
+                               }
+                          },
+               "/" => {
+                               let m = get_result (&mut ops, Operator::DIV); 
+                               if ! m.is_ok () {
+                                       return Err (m.unwrap_err().to_string());
+                               }
+                          },
                        // if word matches a number, push it on to the stack
                _ =>  if match_num.is_match (word) {
-                               let num : f32 = word.parse ().unwrap ();
-                               ops.push (Expression {value: num, operator: Operator::NUM });
-                       }
+                               let num = word.parse ().unwrap ();
+                               ops.push (Expression { value: num });
+                         }     
                        // if word doesn't match either operator or number then panic.                  
                        else {
-                               panic! ("Error parsing expression!
-                                Must be a number or operator (+,-,* or /)");
-                       }               
+                               return Err (ERR_PARSING_NOT_MATCH.to_string());
+                       }
                }
        }
+       
+       if ops.len () > 1 {
        // if the stack has more than one value, it means that the postfix
        // expression is not complete - so display the stack status
-       if ops.len () > 1 {
-               println! ("Postfix Expression not complete. Current stack: ");
-               for exp in ops {
-                       println! ("{}", exp.value);
+               return Err (ERR_POSTFIX_INCOMPLETE.to_string());
+       } else { 
+       // stack has only one item which is the result so display it
+               let res = ops[0].value;
+               return Ok (res);
+       }
+}
+
+// Single command mode - command line arguments mode - evaluate the expression
+// given in the command line and quit
+fn run_command_line (args : &Vec<String>, match_num : &regex::Regex) {
+       let mut expr = String::new ();
+       let mut i = 0;
+       // create the expression string to evaluate
+       for arg in args.iter() {
+               if i > 0 {
+                       expr.push_str (&arg);
+                       expr.push_str (" ");
                }
+               i += 1;
        }
-       // stack has only one item which is the result so display it
-       else {
-               println! ("Result: {}", ops[0].value);
+       // evaluate the result 
+       let res = evaluate (&expr, &match_num);
+       // if Result is OK then print the result in green
+       if res.is_ok () {
+               let restxt = format! ("{}", res.unwrap());
+               println! ("{}", restxt.green ());
+       } else {
+       // print the error in purple
+               let errtxt = format! ("{}: {}", ERROR, 
+                                                                               res.unwrap_err());
+               eprintln! ("{}", errtxt.purple ());
        }
-       println! ();    
 }
 
-fn main() {
+// get the history file name as string - home dir + .evpfhistory
+fn get_history_file () -> Result<String,String> {
+       // get the environment variable HOME
+       let home_path = env::var ("HOME");
+       // if not found, return an error
+       if home_path.is_err () {
+               return Err (HOMEDIR_NOT_FOUND.to_string());
+       }
+       // build the path for the history file i.e. homedir + .evpfhistory in
+       // platform independent way
+       let mut hist_file = PathBuf::new ();
+       hist_file.push (home_path.unwrap());
+       hist_file.push (".evpfhistory");        
+       
+       // if cannot convert to string return error 
+       if hist_file.to_str ().is_none () {
+               return Err (HOMEDIR_NOT_FOUND.to_string());
+       }
+       
+       // return the history file path as a string
+       let hist_file_path = String::from (hist_file.to_str().unwrap());
+       return Ok (hist_file_path);
+}
+
+// Interactive mode - display the prompt and evaluate expressions entered into
+// the prompt - until user quits
+fn run_interactive_mode (match_num : &regex::Regex) {
        // get a line from input and evaluate it 
        let mut expr = String::new ();
-       // loop until a blank line is received
+
+       let mut rl = Editor::<()>::new ();
+       let hist_file = get_history_file ();
+       if hist_file.is_err () {
+               eprintln! ("{}", &hist_file.unwrap_err());
+       } else {
+               if rl.load_history (&hist_file.unwrap ()).is_err () {
+                       eprintln! ("{}", HISTORY_FILE_NOT_FOUND.purple());
+               }
+       }
+       // loop until a blank line is received  
        loop {
-               expr.clear ();  
-               println!("Enter an expression (postfix) (blank to quit): ");
-               // read a line of text
-               io::stdin().read_line (&mut expr).expect ("Error reading line!");
-               // trim the text
-               let expr = expr.trim ();
-               // quit if the expression is blank
-               if expr == "" { 
+               expr.clear ();
+               
+               let line = rl.readline ("evpf> ");
+               if line.is_err () {
+                       break;
+               }
+               let hist = line.unwrap ();
+               rl.add_history_entry (&hist);
+               expr.push_str (&hist);
+
+               if expr == "q" || expr == "Q" {
+               // quit if the expression is q or Qs 
                        break;
+               } else if expr == "?" || expr == "h" || expr == "H" {
+               // display help text
+                       for text in HELP_TEXT.iter() {
+                               println! ("{}", text.cyan() );
+                       }
+
+                       continue;
+               } else if expr == "" {
+               // continue without proceeding
+                       continue;
+               }
+               
+               // Evaluate result
+               let res = evaluate (&expr, &match_num);
+               
+               // if Result is OK then print the result in green
+               if res.is_ok () {
+                       let restxt = format! ("{}", res.unwrap());
+                       println! ("{}", restxt.green ());
+               } else {
+               // print the error in purple
+                       let errtxt = format! ("{}: {}", ERROR, 
+                                                                                       res.unwrap_err());
+                       eprintln! ("{}", errtxt.purple());
+                       eprintln! ("{}", ERROR_HELP.purple());
+               }
+       }
+       // save the history 
+       let hist_file = get_history_file ();
+       if ! hist_file.is_err () {
+               if rl.save_history (&hist_file.unwrap()).is_err () {
+                       eprintln! ("{}", SAVE_HISTORY_ERROR);
                }
+       } 
+}
+
+fn main() {
+       // collect the command line arguments - if any 
+       let args : Vec<String> = env::args().collect ();
+       // regular expression to match a number 
+       let match_num = Regex::new (r"^\d+?\.*?\d*?$").unwrap ();
+       
+       if args.len () > 1 {
+       // if arguments are provided run in command line mode - i.e. print the 
+       // result and exit      
+               run_command_line (&args, &match_num);
                
-               evaluate (&expr);
+       } else {
+               // if arguments are not provided run in interactive mode - 
+               // display a prompt and get the expression 
+               // repeat until the user quits
+               run_interactive_mode (&match_num);
+
        }
 }