use regex::Regex; use colored::*; use std::env; use rustyline::Editor; use std::path::PathBuf; 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, } // function to compute a result by popping the stack and // pushing back the result fn get_result (t : &mut Vec, op : Operator) -> Result { // pop the stack for last operand // if nothing - panic with error 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 = 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::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) }, } } // evaluation function fn evaluate (expr : &str, match_num : ®ex::Regex) -> Result { let mut ops = Vec::::new (); // tokenize the individual words by splitting at whitespace let words = expr.split_whitespace (); // 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 "+" => { 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 = word.parse ().unwrap (); ops.push (Expression { value: num }); } // if word doesn't match either operator or number then panic. else { 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 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, match_num : ®ex::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; } // 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 ()); } } // get the history file name as string - home dir + .evpfhistory fn get_history_file () -> Result { // 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 : ®ex::Regex) { // get a line from input and evaluate it let mut expr = String::new (); let mut rl = Editor::<()>::new (); // load the history file let hist_file = get_history_file (); // if unable to load the history file, display the appropriate error 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 (); 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 = 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); } 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); } }