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