1 use super::InteractiveShell; 2 use ion_shell::{builtins::Status, Value}; 3 4 use regex::Regex; 5 use std::time::{SystemTime, UNIX_EPOCH}; 6 7 #[derive(Debug, Default)] 8 pub struct IgnoreSetting { 9 // Macro definition fails if last flag has a comment at the end of the line. 10 /// ignore all commands ("all") 11 all: bool, 12 /// ignore commands with leading whitespace ("whitespace") 13 whitespace: bool, 14 /// ignore commands with status code 127 ("no_such_command") 15 no_such_command: bool, 16 /// used if regexes are defined. 17 based_on_regex: bool, 18 /// ignore commands that are duplicates 19 duplicates: bool, 20 // Yes, a bad heap-based Vec, however unfortunately its not possible to store Regex'es in Array 21 regexes: Vec<Regex>, 22 } 23 24 /// Contains all history-related functionality for the `Shell`. 25 impl<'a> InteractiveShell<'a> { 26 /// Updates the history ignore patterns. Call this whenever HISTORY_IGNORE 27 /// is changed. ignore_patterns(&self) -> IgnoreSetting28 pub fn ignore_patterns(&self) -> IgnoreSetting { 29 if let Some(Value::Array(patterns)) = self.shell.borrow().variables().get("HISTORY_IGNORE") 30 { 31 let mut settings = IgnoreSetting::default(); 32 // for convenience and to avoid typos 33 let regex_prefix = "regex:"; 34 for pattern in patterns.iter() { 35 let pattern = format!("{}", pattern); 36 match pattern.as_ref() { 37 "all" => settings.all = true, 38 "no_such_command" => settings.no_such_command = true, 39 "whitespace" => settings.whitespace = true, 40 "duplicates" => settings.duplicates = true, 41 // The length check is there to just ignore empty regex definitions 42 _ if pattern.starts_with(regex_prefix) 43 && pattern.len() > regex_prefix.len() => 44 { 45 settings.based_on_regex = true; 46 let regex_string = &pattern[regex_prefix.len()..]; 47 // We save the compiled regexes, as compiling them can be an expensive task 48 if let Ok(regex) = Regex::new(regex_string) { 49 settings.regexes.push(regex); 50 } 51 } 52 _ => continue, 53 } 54 } 55 56 settings 57 } else { 58 panic!("HISTORY_IGNORE is not set!"); 59 } 60 } 61 62 /// Saves a command in the history, depending on @HISTORY_IGNORE. Should be called 63 /// immediately after `on_command()` save_command_in_history(&self, command: &str)64 pub fn save_command_in_history(&self, command: &str) { 65 if self.should_save_command(command) { 66 if self.shell.borrow().variables().get_str("HISTORY_TIMESTAMP").unwrap_or_default() 67 == "1" 68 { 69 // Get current time stamp 70 let since_unix_epoch = 71 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); 72 let cur_time_sys = ["#", &since_unix_epoch.to_owned().to_string()].concat(); 73 74 // Push current time to history 75 if let Err(err) = self.context.borrow_mut().history.push(cur_time_sys.into()) { 76 eprintln!("ion: {}", err) 77 } 78 } 79 80 // Push command itself to history 81 if let Err(err) = self.context.borrow_mut().history.push(command.into()) { 82 eprintln!("ion: {}", err); 83 } 84 } 85 } 86 87 /// Returns true if the given command with the given exit status should be saved in the 88 /// history should_save_command(&self, command: &str) -> bool89 fn should_save_command(&self, command: &str) -> bool { 90 // just for convenience and to make the code look a bit cleaner 91 let ignore = self.ignore_patterns(); 92 93 // without the second check the command which sets the local variable would 94 // also be ignored. However, this behavior might not be wanted. 95 if ignore.all && !command.contains("HISTORY_IGNORE") { 96 return false; 97 } 98 99 // Here we allow to also ignore the setting of the local variable because we 100 // assume the user entered the leading whitespace on purpose. 101 if ignore.whitespace && command.chars().next().map_or(false, char::is_whitespace) { 102 return false; 103 } 104 105 if ignore.no_such_command 106 && self.shell.borrow().previous_status() == Status::NO_SUCH_COMMAND 107 { 108 return false; 109 } 110 111 if ignore.duplicates { 112 self.context.borrow_mut().history.remove_duplicates(command); 113 } 114 115 // ignore command when regex is matched but only if it does not contain 116 // "HISTORY_IGNORE", otherwise we would also ignore the command which 117 // sets the variable, which could be annoying. 118 if !command.contains("HISTORY_IGNORE") 119 && ignore.regexes.iter().any(|regex| regex.is_match(command)) 120 { 121 return false; 122 } 123 124 // default to true, as it's more likely that we want to save a command in 125 // history 126 true 127 } 128 } 129