pub mod shell { use std::process::Command; use std::io::stdin; use std::io::stdout; use std::io::Write; use std::io::ErrorKind; use std::path::Path; use std::env; use std::process::Child; use std::process::Stdio; use std::fs::File; use std::fs::OpenOptions; use std::fs; use std::io::{self, prelude::*, BufReader}; use users::{get_user_by_uid, get_current_uid}; use gethostname::gethostname; //used for home directory extern crate dirs; fn process_line(input: String) -> bool { let mut commands = input.trim().split(" | ").peekable(); let mut previous_command = None; while let Some(command) = commands.next() { let mut parts = command.trim().split_whitespace(); let mut command = parts.next().unwrap(); let args = parts; match command { "cd" => { let new_dir = args.peekable().peek().map_or("/", |x| *x); let root = Path::new(new_dir); if let Err(e) = env::set_current_dir(&root) { eprintln!("{}", e); } previous_command = None; }, "exit" => return true, "history" => { let stdout = Stdio::inherit(); print_history(); }, command => { let stdin = previous_command.map_or( Stdio::inherit(), |output: Child| Stdio::from(output.stdout.unwrap()) ); let stdout = if commands.peek().is_some() { Stdio::piped() } else { Stdio::inherit() }; let output = Command::new(command) .args(args) .stdin(stdin) .stdout(stdout) .spawn(); match output { Ok(output) => { previous_command = Some(output); }, Err(e) => { previous_command = None; eprintln!("{}", e); }, }; } } } if let Some(mut final_command) = previous_command { // block until the final command has finished final_command.wait(); } return false; } fn write_to_history(command: &str) -> Result<(), std::io::Error> { //Write a command to history let filepath = dirs::home_dir().unwrap().join("history.rshell"); let mut file = OpenOptions::new() .write(true) .append(true) .create(true) .open(&filepath) .unwrap(); let number = match get_history_number() { Ok(n) => n + 1, Err(_) => 1 }; writeln!(file, "{} {}", number, command.trim())?; return Ok(()) } fn get_history_number() -> Result { //Get the latest number found in history file let filepath = dirs::home_dir().unwrap().join("history.rshell"); let file = File::open(filepath)?; let mut reader = BufReader::new(file).lines(); let myline = String::from(reader.last().unwrap_or(Ok(String::from("1")))?); let mut number = myline.split(' ').next().expect("???").parse().unwrap(); return Ok(number); } fn print_history() -> Result<(), std::io::Error> { let filepath = dirs::home_dir().unwrap().join("history.rshell"); let file = File::open(filepath)?; let contents = BufReader::new(file).lines(); for line in contents { println!("{}", line.unwrap()); } return Ok(()); } fn get_hist_from_number(number: &i32) -> Option { //Returns a command from its number in hist file let filepath = dirs::home_dir().unwrap().join("history.rshell"); if filepath.exists() == false { return None; } let file = File::open(filepath) .expect("Error opening history file..."); if file.metadata().unwrap().len() < 1 { return None; } let mut reader = BufReader::new(file).lines(); for line in reader { let current_line = line .expect("Empty history file ? Please rm it"); let mut s = current_line.split_whitespace(); let n: i32 = s.next() .expect("Error reading a line in hist file") .parse() .unwrap(); let c = s.next().expect("No command!"); if &n == number { return Some(String::from(c)); } } return None; } fn treat_history_callback(line: &str) -> Option { let mut mystring = String::from(line); mystring = mystring.trim().to_string(); let mut chars = mystring.chars(); //Skip the ! chars.next(); let mynbr: i32 = chars.as_str().parse().unwrap(); get_hist_from_number(&mynbr) } fn build_prompt() -> String { let user = String::from( get_user_by_uid(get_current_uid()) .unwrap() .name() .to_str() .unwrap() ); let host = String::from( gethostname::gethostname() .to_str() .unwrap() ); let current_dir = String::from( env::current_dir() .unwrap() .to_str() .unwrap() ); let s = current_dir.split('/'); let dir_last = String::from( s.last() .unwrap() ); let prompt = String::from(format!("[{}@{} in {}] ", user, host, dir_last)); return prompt; } fn replace_signs(line: String) -> String { let mut ayo = String::from(&line); if ayo.contains('~') { let homedir = dirs::home_dir().unwrap().into_os_string().into_string().unwrap(); let result = str::replace(&ayo, "~", &homedir); ayo = String::from(result); } if ayo.contains("!!") { let prev_comm = get_hist_from_number(&get_history_number().unwrap()).unwrap(); let result = str::replace(&ayo, "!!", &prev_comm); ayo = String::from(result); } return ayo; } pub fn run(){ loop { print!("{}", build_prompt()); stdout().flush(); let mut input = String::new(); let bytes = stdin().read_line(&mut input).unwrap(); input = replace_signs(input); //history callback if (bytes > 0) && (input != String::from("\n")) && (input.starts_with('!')) { let command_found = treat_history_callback(&input); match command_found { Some(c) => { write_to_history(&c); process_line(c); }, None => () }; //Regular command } else if (bytes > 0) && (input != String::from("\n")) { write_to_history(&input); if process_line(input) { return } //Nothing } else { //Command was empty, new line continue; } } } #[cfg(test)] mod tests { use super::*; fn inittests() { let filepath = dirs::home_dir().unwrap().join("history.rshell"); let file = File::create(&filepath).expect("Could not create test history"); writeln!(&file, "1 ls"); writeln!(&file, "2 pwd"); } #[test] fn test_gethistnumber() { inittests(); let mynumber = get_history_number().unwrap(); assert_eq!(mynumber, 2); } #[test] fn test_gethistfromnumber() { inittests(); let mycmd = get_hist_from_number(&1).unwrap(); assert_eq!(mycmd, String::from("ls")); } #[test] fn test_history_callback() { inittests(); let expected = String::from("ls"); assert_eq!(treat_history_callback("!1"), Some(expected)); } #[test] fn test_replace_signs() { inittests(); let homedir = dirs::home_dir().unwrap().into_os_string().into_string().unwrap(); assert_eq!(replace_signs(String::from("ls ~ && echo !!")), String::from(format!("ls {} && echo pwd", homedir))); } #[test] fn test_writehistline() { inittests(); let towrite = String::from("pwd"); write_to_history(&towrite); //Now check directly against the file let filepath = dirs::home_dir().unwrap().join("history.rshell"); let file = File::open(filepath).expect("Error"); let mut reader = BufReader::new(file).lines(); let myline = String::from(reader.last().unwrap().expect("Error")); let expected = String::from("3 pwd"); assert_eq!(myline.trim(), expected.trim()); } } }