1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use std::env;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::fs::File;
use std::io::Read;
fn read_ignoring_utf_errors(path: &PathBuf) -> String {
let mut f = File::open(path).unwrap_or_else(|_| panic!("McFly error: {:?} file not found", &path));
let mut buffer = Vec::new();
f.read_to_end(&mut buffer).unwrap_or_else(|_| panic!("McFly error: Unable to read from {:?}", &path));
String::from_utf8_lossy(&buffer).to_string()
}
#[allow(clippy::if_same_then_else)]
fn has_leading_timestamp(line: &str) -> bool {
let mut matched_chars = 0;
for (index, c) in line.chars().enumerate() {
if index == 0 && c == '#' {
matched_chars += 1;
} else if index > 0 && index < 11 && (c.is_digit(10)) {
matched_chars += 1;
} else if index > 11 {
break;
}
}
matched_chars == 11
}
pub fn bash_history_file_path() -> PathBuf {
let path =
PathBuf::from(env::var("HISTFILE").expect("McFly error: Please ensure HISTFILE is set for your shell."));
fs::canonicalize(&path).expect("McFly error: The contents of $HISTFILE appear invalid")
}
pub fn full_history(path: &PathBuf) -> Vec<String> {
let bash_history_contents = read_ignoring_utf_errors(&path);
bash_history_contents
.split('\n')
.filter(|line| !has_leading_timestamp(line) && !line.is_empty())
.map(String::from)
.collect::<Vec<String>>()
}
pub fn last_history_line(path: &PathBuf) -> Option<String> {
full_history(path).last().map(|s| s.trim().to_string())
}
pub fn delete_last_history_entry_if_search(path: &PathBuf) {
let bash_history_contents = read_ignoring_utf_errors(&path);
let mut lines = bash_history_contents
.split('\n')
.map(String::from)
.collect::<Vec<String>>();
if !lines.is_empty() && lines[lines.len() - 1].is_empty() {
lines.pop();
}
if lines.is_empty() || !lines[lines.len() - 1].starts_with("#mcfly:") {
return;
}
lines.pop();
if !lines.is_empty() && has_leading_timestamp(&lines[lines.len() - 1]) {
lines.pop();
}
lines.push(String::from(""));
fs::write(&path, lines.join("\n")).unwrap_or_else(|_| panic!("McFly error: Unable to update {:?}", &path));
}
pub fn delete_lines(path: &PathBuf, command: &str) {
let history_contents = read_ignoring_utf_errors(&path);
let lines = history_contents
.split('\n')
.map(String::from)
.filter(|cmd| !cmd.eq(command))
.collect::<Vec<String>>();
fs::write(&path, lines.join("\n")).unwrap_or_else(|_| panic!("McFly error: Unable to update {:?}", &path));
}
pub fn append_history_entry(command: &str, path: &PathBuf) {
let mut file = OpenOptions::new()
.write(true)
.append(true)
.open(path)
.expect("McFly error: please make sure HISTFILE exists.");
if let Err(e) = writeln!(file, "{}", command) {
eprintln!("Couldn't append to file {:?}: {}", &path, e);
}
}
#[cfg(test)]
mod tests {
use super::has_leading_timestamp;
#[test]
fn has_leading_timestamp_works() {
assert_eq!(false, has_leading_timestamp("abc"));
assert_eq!(false, has_leading_timestamp("#abc"));
assert_eq!(false, has_leading_timestamp("#123456"));
assert_eq!(true, has_leading_timestamp("#1234567890"));
assert_eq!(false, has_leading_timestamp("#123456789"));
assert_eq!(false, has_leading_timestamp("# 1234567890"));
assert_eq!(false, has_leading_timestamp("1234567890"));
assert_eq!(false, has_leading_timestamp("hello 1234567890"));
}
}