1use std::collections::HashMap;
8use std::fs::{File, OpenOptions};
9use std::io::{self, BufRead, BufReader, Write};
10use std::path::Path;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13#[derive(Clone, Debug)]
15pub struct HistEntry {
16 pub histnum: i64, pub text: String, pub words: Vec<(usize, usize)>, pub stim: i64, pub ftim: i64, pub flags: u32, }
23
24pub mod hist_flags {
26 pub const OLD: u32 = 1; pub const DUP: u32 = 2; pub const FOREIGN: u32 = 4; pub const TMPSTORE: u32 = 8; pub const NOWRITE: u32 = 16; }
32
33impl HistEntry {
34 pub fn new(histnum: i64, text: String) -> Self {
35 let now = SystemTime::now()
36 .duration_since(UNIX_EPOCH)
37 .map(|d| d.as_secs() as i64)
38 .unwrap_or(0);
39
40 HistEntry {
41 histnum,
42 text,
43 words: Vec::new(),
44 stim: now,
45 ftim: now,
46 flags: 0,
47 }
48 }
49
50 pub fn get_word(&self, index: usize) -> Option<&str> {
52 self.words
53 .get(index)
54 .map(|(start, end)| &self.text[*start..*end])
55 }
56
57 pub fn num_words(&self) -> usize {
59 self.words.len()
60 }
61}
62
63pub const HA_ACTIVE: u32 = 1; pub const HA_NOINC: u32 = 2; pub const HA_INWORD: u32 = 4; pub struct History {
70 entries: HashMap<i64, HistEntry>,
72 ring: Vec<i64>,
74 pub curhist: i64,
76 pub histlinect: i64,
78 pub histsiz: i64,
80 pub savehistsiz: i64,
82 pub histactive: u32,
84 pub stophist: i32,
86 pub histdone: i32,
88 pub hist_skip_flags: i32,
90 pub hist_ignore_all_dups: bool,
92 pub curline: Option<HistEntry>,
94 pub hsubl: Option<String>,
96 pub hsubr: Option<String>,
97 pub bangchar: char,
99 pub histfile: Option<String>,
101}
102
103impl Default for History {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl History {
110 pub fn new() -> Self {
111 History {
112 entries: HashMap::new(),
113 ring: Vec::new(),
114 curhist: 0,
115 histlinect: 0,
116 histsiz: 1000,
117 savehistsiz: 1000,
118 histactive: 0,
119 stophist: 0,
120 histdone: 0,
121 hist_skip_flags: 0,
122 hist_ignore_all_dups: false,
123 curline: None,
124 hsubl: None,
125 hsubr: None,
126 bangchar: '!',
127 histfile: None,
128 }
129 }
130
131 pub fn init(&mut self) {
133 self.curhist = 0;
134 self.histlinect = 0;
135 }
136
137 pub fn hbegin(&mut self, interactive: bool) {
139 if (self.histactive & HA_ACTIVE) != 0 {
140 return;
141 }
142
143 self.histactive = HA_ACTIVE;
144 self.histdone = 0;
145
146 if interactive {
147 self.curhist += 1;
148 self.curline = Some(HistEntry::new(self.curhist, String::new()));
149 }
150 }
151
152 pub fn hend(&mut self, text: Option<String>) -> bool {
154 if (self.histactive & HA_ACTIVE) == 0 {
155 return false;
156 }
157
158 self.histactive = 0;
159
160 if let Some(mut entry) = self.curline.take() {
161 if let Some(t) = text {
162 entry.text = t;
163 }
164
165 if entry.text.trim().is_empty() {
167 self.curhist -= 1;
168 return false;
169 }
170
171 if self.hist_ignore_all_dups {
173 let dup = self.entries.values().any(|e| e.text == entry.text);
174 if dup {
175 self.curhist -= 1;
176 return false;
177 }
178 }
179
180 self.add_entry(entry);
182 return true;
183 }
184
185 false
186 }
187
188 fn add_entry(&mut self, entry: HistEntry) {
190 let num = entry.histnum;
191
192 while self.histlinect >= self.histsiz && !self.ring.is_empty() {
194 let oldest = self.ring.pop().unwrap();
195 self.entries.remove(&oldest);
196 self.histlinect -= 1;
197 }
198
199 self.entries.insert(num, entry);
200 self.ring.insert(0, num);
201 self.histlinect += 1;
202 }
203
204 pub fn get(&self, num: i64) -> Option<&HistEntry> {
206 self.entries.get(&num)
207 }
208
209 pub fn latest(&self) -> Option<&HistEntry> {
211 self.ring.first().and_then(|n| self.entries.get(n))
212 }
213
214 pub fn recent(&self, n: usize) -> Option<&HistEntry> {
216 self.ring.get(n).and_then(|num| self.entries.get(num))
217 }
218
219 pub fn search_back(&self, pattern: &str, start: i64) -> Option<&HistEntry> {
221 for num in self.ring.iter() {
222 if *num >= start {
223 continue;
224 }
225 if let Some(entry) = self.entries.get(num) {
226 if entry.text.contains(pattern) {
227 return Some(entry);
228 }
229 }
230 }
231 None
232 }
233
234 pub fn search_forward(&self, pattern: &str, start: i64) -> Option<&HistEntry> {
236 for num in self.ring.iter().rev() {
237 if *num <= start {
238 continue;
239 }
240 if let Some(entry) = self.entries.get(num) {
241 if entry.text.contains(pattern) {
242 return Some(entry);
243 }
244 }
245 }
246 None
247 }
248
249 pub fn expand(&mut self, line: &str) -> Result<String, String> {
251 let mut result = String::new();
252 let mut chars = line.chars().peekable();
253 let bang = self.bangchar;
254
255 while let Some(c) = chars.next() {
256 if c == bang {
257 match chars.peek() {
258 Some(&'!') => {
259 chars.next();
261 if let Some(entry) = self.latest() {
262 result.push_str(&entry.text);
263 } else {
264 return Err("No previous command".to_string());
265 }
266 }
267 Some(&'-') | Some(&('0'..='9')) => {
268 let mut numstr = String::new();
270 if chars.peek() == Some(&'-') {
271 numstr.push(chars.next().unwrap());
272 }
273 while let Some(&c) = chars.peek() {
274 if c.is_ascii_digit() {
275 numstr.push(chars.next().unwrap());
276 } else {
277 break;
278 }
279 }
280 if let Ok(n) = numstr.parse::<i64>() {
281 let target = if n < 0 { self.curhist + n } else { n };
282 if let Some(entry) = self.get(target) {
283 result.push_str(&entry.text);
284 } else {
285 return Err(format!("!{}: event not found", numstr));
286 }
287 }
288 }
289 Some(&'?') => {
290 chars.next();
292 let mut pattern = String::new();
293 while let Some(&c) = chars.peek() {
294 if c == '?' {
295 chars.next();
296 break;
297 }
298 pattern.push(chars.next().unwrap());
299 }
300 if let Some(entry) = self.search_back(&pattern, self.curhist) {
301 result.push_str(&entry.text);
302 } else {
303 return Err(format!("!?{}: event not found", pattern));
304 }
305 }
306 Some(&'^') | Some(&'$') | Some(&'*') | Some(&':') => {
307 if let Some(entry) = self.latest() {
309 let words: Vec<&str> = entry.text.split_whitespace().collect();
310 match chars.next() {
311 Some('^') => {
312 if let Some(w) = words.get(1) {
313 result.push_str(w);
314 }
315 }
316 Some('$') => {
317 if let Some(w) = words.last() {
318 result.push_str(w);
319 }
320 }
321 Some('*') => {
322 result.push_str(&words[1..].join(" "));
323 }
324 _ => {}
325 }
326 }
327 }
328 Some(c) if c.is_alphabetic() => {
329 let mut pattern = String::new();
331 while let Some(&c) = chars.peek() {
332 if c.is_alphanumeric() || c == '_' {
333 pattern.push(chars.next().unwrap());
334 } else {
335 break;
336 }
337 }
338 let found = self.ring.iter().find_map(|num| {
339 self.entries
340 .get(num)
341 .filter(|e| e.text.starts_with(&pattern))
342 });
343 if let Some(entry) = found {
344 result.push_str(&entry.text);
345 } else {
346 return Err(format!("!{}: event not found", pattern));
347 }
348 }
349 _ => result.push(bang),
350 }
351 } else if c == '^' && result.is_empty() {
352 let mut old = String::new();
354 let mut new = String::new();
355 let mut in_new = false;
356
357 while let Some(c) = chars.next() {
358 if c == '^' {
359 if in_new {
360 break;
361 }
362 in_new = true;
363 } else if in_new {
364 new.push(c);
365 } else {
366 old.push(c);
367 }
368 }
369
370 if let Some(entry) = self.latest() {
371 result = entry.text.replacen(&old, &new, 1);
372 self.hsubl = Some(old);
373 self.hsubr = Some(new);
374 } else {
375 return Err("No previous command".to_string());
376 }
377 } else {
378 result.push(c);
379 }
380 }
381
382 Ok(result)
383 }
384
385 pub fn read_file(&mut self, path: &Path) -> io::Result<()> {
387 let file = File::open(path)?;
388 let reader = BufReader::new(file);
389
390 for line in reader.lines() {
391 let line = line?;
392
393 if line.starts_with(':') {
395 let parts: Vec<&str> = line.splitn(2, ';').collect();
397 if parts.len() == 2 {
398 let text = parts[1].to_string();
399 let mut entry = HistEntry::new(self.curhist + 1, text);
400
401 if let Some(ts_part) = parts[0].strip_prefix(": ") {
403 if let Some(ts_str) = ts_part.split(':').next() {
404 if let Ok(ts) = ts_str.parse::<i64>() {
405 entry.stim = ts;
406 entry.ftim = ts;
407 }
408 }
409 }
410
411 entry.flags |= hist_flags::OLD;
412 self.curhist += 1;
413 self.add_entry(entry);
414 }
415 } else if !line.is_empty() {
416 self.curhist += 1;
418 let mut entry = HistEntry::new(self.curhist, line);
419 entry.flags |= hist_flags::OLD;
420 self.add_entry(entry);
421 }
422 }
423
424 Ok(())
425 }
426
427 pub fn write_file(&self, path: &Path, append: bool) -> io::Result<()> {
429 let mut file = OpenOptions::new()
430 .write(true)
431 .create(true)
432 .truncate(!append)
433 .append(append)
434 .open(path)?;
435
436 for num in self.ring.iter().rev() {
437 if let Some(entry) = self.entries.get(num) {
438 if (entry.flags & hist_flags::NOWRITE) != 0 {
439 continue;
440 }
441 writeln!(file, ": {}:0;{}", entry.stim, entry.text)?;
443 }
444 }
445
446 Ok(())
447 }
448
449 pub fn clear(&mut self) {
451 self.entries.clear();
452 self.ring.clear();
453 self.histlinect = 0;
454 }
455
456 pub fn all_entries(&self) -> Vec<&HistEntry> {
458 self.ring
459 .iter()
460 .filter_map(|n| self.entries.get(n))
461 .collect()
462 }
463
464 pub fn len(&self) -> usize {
466 self.entries.len()
467 }
468
469 pub fn is_empty(&self) -> bool {
471 self.entries.is_empty()
472 }
473}
474
475#[derive(Clone)]
477pub struct HistStack {
478 pub histactive: u32,
479 pub histdone: i32,
480 pub stophist: i32,
481 pub chline: Option<String>,
482 pub hptr: usize,
483 pub chwords: Vec<(usize, usize)>,
484 pub hlinesz: usize,
485 pub defev: i64,
486 pub hist_keep_comment: bool,
487}
488
489impl Default for HistStack {
490 fn default() -> Self {
491 HistStack {
492 histactive: 0,
493 histdone: 0,
494 stophist: 0,
495 chline: None,
496 hptr: 0,
497 chwords: Vec::new(),
498 hlinesz: 0,
499 defev: 0,
500 hist_keep_comment: false,
501 }
502 }
503}
504
505pub const HISTFLAG_DONE: i32 = 1;
507pub const HISTFLAG_NOEXEC: i32 = 2;
508pub const HISTFLAG_RECALL: i32 = 4;
509pub const HISTFLAG_SETTY: i32 = 8;
510
511#[derive(Clone, Copy, Debug, PartialEq)]
513pub enum CaseMod {
514 Lower,
515 Upper,
516 Caps,
517}
518
519pub fn casemodify(s: &str, how: CaseMod) -> String {
521 let mut result = String::with_capacity(s.len());
522 let mut nextupper = true;
523
524 for c in s.chars() {
525 let modified = match how {
526 CaseMod::Lower => c.to_lowercase().collect::<String>(),
527 CaseMod::Upper => c.to_uppercase().collect::<String>(),
528 CaseMod::Caps => {
529 if !c.is_alphanumeric() {
530 nextupper = true;
531 c.to_string()
532 } else if nextupper {
533 nextupper = false;
534 c.to_uppercase().collect::<String>()
535 } else {
536 c.to_lowercase().collect::<String>()
537 }
538 }
539 };
540 result.push_str(&modified);
541 }
542
543 result
544}
545
546pub fn remtpath(s: &str, count: i32) -> String {
548 let s = s.trim_end_matches('/');
549
550 if s.is_empty() {
551 return "/".to_string();
552 }
553
554 if count == 0 {
555 if let Some(pos) = s.rfind('/') {
556 if pos == 0 {
557 return "/".to_string();
558 }
559 return s[..pos].trim_end_matches('/').to_string();
560 }
561 return ".".to_string();
562 }
563
564 let parts: Vec<&str> = s.split('/').filter(|p| !p.is_empty()).collect();
565 if count as usize >= parts.len() {
566 return s.to_string();
567 }
568
569 let leading_slash = s.starts_with('/');
570 let result: String = parts
571 .iter()
572 .take(count as usize)
573 .map(|s| *s)
574 .collect::<Vec<&str>>()
575 .join("/");
576
577 if leading_slash {
578 format!("/{}", result)
579 } else {
580 result
581 }
582}
583
584pub fn remlpaths(s: &str, count: i32) -> String {
586 let s = s.trim_end_matches('/');
587
588 if s.is_empty() {
589 return String::new();
590 }
591
592 let parts: Vec<&str> = s.split('/').filter(|p| !p.is_empty()).collect();
593
594 if count == 0 {
595 if let Some(last) = parts.last() {
596 return last.to_string();
597 }
598 return String::new();
599 }
600
601 if count as usize >= parts.len() {
602 return s.to_string();
603 }
604
605 parts
606 .iter()
607 .rev()
608 .take(count as usize)
609 .rev()
610 .map(|s| *s)
611 .collect::<Vec<&str>>()
612 .join("/")
613}
614
615pub fn remtext(s: &str) -> String {
617 if let Some(slash_pos) = s.rfind('/') {
618 let after_slash = &s[slash_pos + 1..];
619 if let Some(dot_pos) = after_slash.rfind('.') {
620 if dot_pos > 0 {
621 return format!("{}/{}", &s[..slash_pos], &after_slash[..dot_pos]);
622 }
623 }
624 return s.to_string();
625 }
626
627 if let Some(dot_pos) = s.rfind('.') {
628 if dot_pos > 0 {
629 return s[..dot_pos].to_string();
630 }
631 }
632 s.to_string()
633}
634
635pub fn rembutext(s: &str) -> String {
637 if let Some(slash_pos) = s.rfind('/') {
638 let after_slash = &s[slash_pos + 1..];
639 if let Some(dot_pos) = after_slash.rfind('.') {
640 return after_slash[dot_pos + 1..].to_string();
641 }
642 return String::new();
643 }
644
645 if let Some(dot_pos) = s.rfind('.') {
646 return s[dot_pos + 1..].to_string();
647 }
648 String::new()
649}
650
651pub fn chabspath(s: &str) -> std::io::Result<String> {
653 if s.is_empty() {
654 return Ok(String::new());
655 }
656
657 let path = if !s.starts_with('/') {
658 let cwd = std::env::current_dir()?;
659 format!("{}/{}", cwd.display(), s)
660 } else {
661 s.to_string()
662 };
663
664 let mut result = Vec::new();
665 for component in path.split('/') {
666 match component {
667 "" | "." => continue,
668 ".." => {
669 if !result.is_empty() && result.last() != Some(&"..") {
670 result.pop();
671 } else if result.is_empty() && !path.starts_with('/') {
672 result.push("..");
673 }
674 }
675 c => result.push(c),
676 }
677 }
678
679 if path.starts_with('/') {
680 Ok(format!("/{}", result.join("/")))
681 } else if result.is_empty() {
682 Ok(".".to_string())
683 } else {
684 Ok(result.join("/"))
685 }
686}
687
688pub fn quote(s: &str) -> String {
690 let mut result = String::with_capacity(s.len() + 10);
691 result.push('\'');
692
693 for c in s.chars() {
694 if c == '\'' {
695 result.push_str("'\\''");
696 } else {
697 result.push(c);
698 }
699 }
700
701 result.push('\'');
702 result
703}
704
705pub fn quotebreak(s: &str) -> String {
707 let mut result = String::with_capacity(s.len() + 10);
708 result.push('\'');
709
710 for c in s.chars() {
711 if c == '\'' {
712 result.push_str("'\\''");
713 } else if c.is_whitespace() {
714 result.push('\'');
715 result.push(c);
716 result.push('\'');
717 } else {
718 result.push(c);
719 }
720 }
721
722 result.push('\'');
723 result
724}
725
726pub fn subst(s: &str, in_pattern: &str, out_pattern: &str, global: bool) -> String {
728 if in_pattern.is_empty() {
729 return s.to_string();
730 }
731
732 let out_expanded = convamps(out_pattern, in_pattern);
733
734 if global {
735 s.replace(in_pattern, &out_expanded)
736 } else {
737 s.replacen(in_pattern, &out_expanded, 1)
738 }
739}
740
741fn convamps(out: &str, in_pattern: &str) -> String {
743 let mut result = String::with_capacity(out.len());
744 let mut chars = out.chars().peekable();
745
746 while let Some(c) = chars.next() {
747 if c == '\\' {
748 if let Some(&next) = chars.peek() {
749 result.push(next);
750 chars.next();
751 }
752 } else if c == '&' {
753 result.push_str(in_pattern);
754 } else {
755 result.push(c);
756 }
757 }
758
759 result
760}
761
762pub fn getargspec(argc: usize, c: char, marg: Option<usize>, evset: bool) -> Option<usize> {
764 match c {
765 '0' => Some(0),
766 '1'..='9' => Some(c.to_digit(10).unwrap() as usize),
767 '^' => Some(1),
768 '$' => Some(argc),
769 '%' => {
770 if evset {
771 return None;
772 }
773 marg
774 }
775 _ => None,
776 }
777}
778
779impl History {
781 pub fn hconsearch(&self, pattern: &str) -> Option<(i64, usize)> {
782 for num in &self.ring {
783 if let Some(entry) = self.entries.get(num) {
784 if let Some(pos) = entry.text.find(pattern) {
785 let words: Vec<&str> = entry.text.split_whitespace().collect();
786 let mut word_idx = 0;
787 let mut char_count = 0;
788 for (i, word) in words.iter().enumerate() {
789 if char_count + word.len() > pos {
790 word_idx = i;
791 break;
792 }
793 char_count += word.len() + 1;
794 }
795 return Some((entry.histnum, word_idx));
796 }
797 }
798 }
799 None
800 }
801
802 pub fn hcomsearch(&self, prefix: &str) -> Option<i64> {
804 for num in &self.ring {
805 if let Some(entry) = self.entries.get(num) {
806 if entry.text.starts_with(prefix) {
807 return Some(entry.histnum);
808 }
809 }
810 }
811 None
812 }
813
814 pub fn getargs(&self, ev: i64, arg1: usize, arg2: usize) -> Option<String> {
816 let entry = self.entries.get(&ev)?;
817 let words: Vec<&str> = entry.text.split_whitespace().collect();
818
819 if arg2 < arg1 || arg1 >= words.len() || arg2 >= words.len() {
820 return None;
821 }
822
823 if arg1 == 0 && arg2 == words.len() - 1 {
824 return Some(entry.text.clone());
825 }
826
827 Some(words[arg1..=arg2].join(" "))
828 }
829
830 pub fn save_context(&self) -> HistStack {
832 HistStack {
833 histactive: self.histactive,
834 histdone: self.histdone,
835 stophist: self.stophist,
836 chline: self.curline.as_ref().map(|e| e.text.clone()),
837 hptr: 0,
838 chwords: Vec::new(),
839 hlinesz: 0,
840 defev: self.curhist - 1,
841 hist_keep_comment: false,
842 }
843 }
844
845 pub fn restore_context(&mut self, ctx: &HistStack) {
847 self.histactive = ctx.histactive;
848 self.histdone = ctx.histdone;
849 self.stophist = ctx.stophist;
850 }
851
852 pub fn hist_in_word(&mut self, yesno: bool) {
854 if yesno {
855 self.histactive |= HA_INWORD;
856 } else {
857 self.histactive &= !HA_INWORD;
858 }
859 }
860
861 pub fn hist_is_in_word(&self) -> bool {
863 (self.histactive & HA_INWORD) != 0
864 }
865
866 pub fn addhistnum(&self, hl: i64, n: i64) -> i64 {
868 let target = hl + n;
869 if target < 1 {
870 0
871 } else if target > self.curhist {
872 self.curhist + 1
873 } else {
874 target
875 }
876 }
877
878 pub fn histreduceblanks(line: &str, words: &[(usize, usize)]) -> String {
880 if words.is_empty() {
881 return line.to_string();
882 }
883
884 let mut result = String::new();
885 let chars: Vec<char> = line.chars().collect();
886
887 for (i, (start, end)) in words.iter().enumerate() {
888 if i > 0 {
889 result.push(' ');
890 }
891 for j in *start..*end {
892 if j < chars.len() {
893 result.push(chars[j]);
894 }
895 }
896 }
897
898 result
899 }
900
901 pub fn resizehistents(&mut self) {
903 while self.histlinect > self.histsiz {
904 if let Some(oldest) = self.ring.pop() {
905 self.entries.remove(&oldest);
906 self.histlinect -= 1;
907 } else {
908 break;
909 }
910 }
911 }
912
913 pub fn readhistfile(&mut self, filename: &str, err: bool) -> io::Result<usize> {
915 let file = File::open(filename)?;
916 let reader = BufReader::new(file);
917 let mut count = 0;
918
919 for line in reader.lines() {
920 let line = line?;
921 if line.is_empty() {
922 continue;
923 }
924
925 if line.starts_with(": ") {
927 let rest = &line[2..];
928 if let Some(semi) = rest.find(';') {
929 let time_part = &rest[..semi];
930 let cmd_part = &rest[semi + 1..];
931
932 let stim = if let Some(colon) = time_part.find(':') {
933 time_part[..colon].parse::<i64>().unwrap_or(0)
934 } else {
935 time_part.parse::<i64>().unwrap_or(0)
936 };
937
938 if !cmd_part.trim().is_empty() {
939 self.curhist += 1;
940 let mut entry = HistEntry::new(self.curhist, cmd_part.to_string());
941 entry.stim = stim;
942 entry.flags = hist_flags::OLD;
943 self.add_entry(entry);
944 count += 1;
945 }
946 }
947 } else {
948 if !line.trim().is_empty() {
950 self.curhist += 1;
951 let mut entry = HistEntry::new(self.curhist, line);
952 entry.flags = hist_flags::OLD;
953 self.add_entry(entry);
954 count += 1;
955 }
956 }
957 }
958
959 if err && count == 0 {
960 return Err(io::Error::new(
961 io::ErrorKind::InvalidData,
962 "No history entries",
963 ));
964 }
965
966 Ok(count)
967 }
968
969 pub fn savehistfile(&self, filename: &str, mode: WriteMode) -> io::Result<usize> {
971 let file = match mode {
972 WriteMode::Overwrite => File::create(filename)?,
973 WriteMode::Append => OpenOptions::new()
974 .create(true)
975 .append(true)
976 .open(filename)?,
977 };
978 let mut writer = io::BufWriter::new(file);
979 let mut count = 0;
980
981 for num in self.ring.iter().rev() {
982 if let Some(entry) = self.entries.get(num) {
983 if (entry.flags & hist_flags::NOWRITE) != 0 {
984 continue;
985 }
986
987 writeln!(writer, ": {}:0;{}", entry.stim, entry.text)?;
989 count += 1;
990 }
991 }
992
993 writer.flush()?;
994 Ok(count)
995 }
996
997 pub fn lockhistfile(&self, filename: &str, _excl: bool) -> io::Result<()> {
999 let lockfile = format!("{}.lock", filename);
1000 File::create(&lockfile)?;
1001 Ok(())
1002 }
1003
1004 pub fn unlockhistfile(&self, filename: &str) -> io::Result<()> {
1006 let lockfile = format!("{}.lock", filename);
1007 std::fs::remove_file(&lockfile).ok();
1008 Ok(())
1009 }
1010
1011 pub fn quotestring(s: &str) -> String {
1013 let mut result = String::with_capacity(s.len() + 10);
1014 result.push('\'');
1015
1016 for c in s.chars() {
1017 if c == '\'' {
1018 result.push_str("'\\''");
1019 } else {
1020 result.push(c);
1021 }
1022 }
1023
1024 result.push('\'');
1025 result
1026 }
1027
1028 pub fn get_history_word(line: &str, idx: usize) -> Option<&str> {
1030 line.split_whitespace().nth(idx)
1031 }
1032
1033 pub fn histword_count(line: &str) -> usize {
1035 line.split_whitespace().count()
1036 }
1037}
1038
1039pub enum WriteMode {
1041 Overwrite,
1042 Append,
1043}
1044
1045pub fn apply_word_designator(text: &str, designator: &str) -> Option<String> {
1062 let words: Vec<&str> = text.split_whitespace().collect();
1063 if words.is_empty() {
1064 return None;
1065 }
1066
1067 match designator {
1068 "0" => Some(words[0].to_string()),
1069 "^" => words.get(1).map(|s| s.to_string()),
1070 "$" => words.last().map(|s| s.to_string()),
1071 "*" => {
1072 if words.len() > 1 {
1073 Some(words[1..].join(" "))
1074 } else {
1075 Some(String::new())
1076 }
1077 }
1078 s if s.contains('-') => {
1079 let parts: Vec<&str> = s.splitn(2, '-').collect();
1080 let start: usize = if parts[0].is_empty() {
1081 0
1082 } else {
1083 parts[0].parse().ok()?
1084 };
1085 let end: usize = if parts[1].is_empty() {
1086 words.len() - 2 } else {
1088 parts[1].parse().ok()?
1089 };
1090 if start <= end && end < words.len() {
1091 Some(words[start..=end].join(" "))
1092 } else {
1093 None
1094 }
1095 }
1096 s if s.ends_with('*') => {
1097 let start: usize = s[..s.len() - 1].parse().ok()?;
1098 if start < words.len() {
1099 Some(words[start..].join(" "))
1100 } else {
1101 None
1102 }
1103 }
1104 s => {
1105 let idx: usize = s.parse().ok()?;
1106 words.get(idx).map(|s| s.to_string())
1107 }
1108 }
1109}
1110
1111pub fn apply_hist_modifier(
1113 text: &str,
1114 modifier: char,
1115 global: bool,
1116 subst_state: &mut (String, String),
1117) -> String {
1118 match modifier {
1119 'h' => {
1120 if let Some(pos) = text.rfind('/') {
1122 if pos == 0 {
1123 "/".to_string()
1124 } else {
1125 text[..pos].to_string()
1126 }
1127 } else {
1128 ".".to_string()
1129 }
1130 }
1131 't' => {
1132 text.rsplit('/').next().unwrap_or(text).to_string()
1134 }
1135 'r' => {
1136 if let Some(dot) = text.rfind('.') {
1138 if dot > text.rfind('/').unwrap_or(0) {
1139 return text[..dot].to_string();
1140 }
1141 }
1142 text.to_string()
1143 }
1144 'e' => {
1145 if let Some(dot) = text.rfind('.') {
1147 if dot > text.rfind('/').unwrap_or(0) {
1148 return text[dot + 1..].to_string();
1149 }
1150 }
1151 String::new()
1152 }
1153 'l' => text.to_lowercase(),
1154 'u' => text.to_uppercase(),
1155 'q' => {
1156 format!("'{}'", text.replace('\'', "'\\''"))
1158 }
1159 'Q' => {
1160 let s = text.strip_prefix('\'').and_then(|s| s.strip_suffix('\''));
1162 match s {
1163 Some(inner) => inner.replace("'\\''", "'"),
1164 None => {
1165 let s = text.strip_prefix('"').and_then(|s| s.strip_suffix('"'));
1166 match s {
1167 Some(inner) => inner.to_string(),
1168 None => text.to_string(),
1169 }
1170 }
1171 }
1172 }
1173 'x' => {
1174 text.split_whitespace()
1176 .map(|w| format!("'{}'", w.replace('\'', "'\\''")))
1177 .collect::<Vec<_>>()
1178 .join(" ")
1179 }
1180 'a' => {
1181 if text.starts_with('/') {
1183 text.to_string()
1184 } else if let Ok(cwd) = std::env::current_dir() {
1185 cwd.join(text).to_string_lossy().to_string()
1186 } else {
1187 text.to_string()
1188 }
1189 }
1190 's' | '&' => {
1191 if modifier == '&' {
1193 let (ref old, ref new) = *subst_state;
1195 if old.is_empty() {
1196 return text.to_string();
1197 }
1198 if global {
1199 text.replace(old.as_str(), new.as_str())
1200 } else {
1201 text.replacen(old.as_str(), new.as_str(), 1)
1202 }
1203 } else {
1204 text.to_string() }
1206 }
1207 'p' => text.to_string(), _ => text.to_string(),
1209 }
1210}
1211
1212pub fn histremovedups(entries: &mut Vec<HistEntry>) {
1214 let mut seen = std::collections::HashSet::new();
1215 entries.retain(|e| seen.insert(e.text.clone()));
1216}
1217
1218pub fn histreduceblanks(text: &str) -> String {
1220 let mut result = String::with_capacity(text.len());
1221 let mut prev_space = false;
1222 for c in text.chars() {
1223 if c.is_whitespace() {
1224 if !prev_space {
1225 result.push(' ');
1226 prev_space = true;
1227 }
1228 } else {
1229 result.push(c);
1230 prev_space = false;
1231 }
1232 }
1233 result.trim().to_string()
1234}
1235
1236pub fn hgetline(entry: &HistEntry) -> String {
1238 entry.text.clone()
1239}
1240
1241pub fn hwrep(entry: &HistEntry, replacement: &str, word_idx: usize) -> String {
1243 let words: Vec<&str> = entry.text.split_whitespace().collect();
1244 if word_idx >= words.len() {
1245 return entry.text.clone();
1246 }
1247 let mut new_words: Vec<String> = words.iter().map(|s| s.to_string()).collect();
1248 new_words[word_idx] = replacement.to_string();
1249 new_words.join(" ")
1250}
1251
1252pub fn addhistnum(base: i64, n: i64) -> i64 {
1254 base + n
1255}
1256
1257pub fn should_ignore_line(
1259 text: &str,
1260 ignorespace: bool,
1261 ignoredups: bool,
1262 last: Option<&str>,
1263) -> bool {
1264 if ignorespace && text.starts_with(' ') {
1265 return true;
1266 }
1267 if ignoredups {
1268 if let Some(prev) = last {
1269 if prev == text {
1270 return true;
1271 }
1272 }
1273 }
1274 false
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279 use super::*;
1280
1281 #[test]
1282 fn test_history_add() {
1283 let mut hist = History::new();
1284 hist.hbegin(true);
1285 hist.hend(Some("echo hello".to_string()));
1286
1287 assert_eq!(hist.len(), 1);
1288 assert_eq!(hist.latest().unwrap().text, "echo hello");
1289 }
1290
1291 #[test]
1292 fn test_history_expand_bang_bang() {
1293 let mut hist = History::new();
1294 hist.hbegin(true);
1295 hist.hend(Some("ls -la".to_string()));
1296
1297 let result = hist.expand("!! | grep foo").unwrap();
1298 assert_eq!(result, "ls -la | grep foo");
1299 }
1300
1301 #[test]
1302 fn test_history_expand_caret() {
1303 let mut hist = History::new();
1304 hist.hbegin(true);
1305 hist.hend(Some("echo hello".to_string()));
1306
1307 let result = hist.expand("^hello^world").unwrap();
1308 assert_eq!(result, "echo world");
1309 }
1310
1311 #[test]
1312 fn test_history_search() {
1313 let mut hist = History::new();
1314
1315 hist.hbegin(true);
1316 hist.hend(Some("cd /tmp".to_string()));
1317
1318 hist.hbegin(true);
1319 hist.hend(Some("echo hello".to_string()));
1320
1321 hist.hbegin(true);
1322 hist.hend(Some("ls -la".to_string()));
1323
1324 let result = hist.search_back("echo", hist.curhist + 1);
1325 assert!(result.is_some());
1326 assert_eq!(result.unwrap().text, "echo hello");
1327 }
1328
1329 #[test]
1330 fn test_history_capacity() {
1331 let mut hist = History::new();
1332 hist.histsiz = 3;
1333
1334 for i in 0..5 {
1335 hist.hbegin(true);
1336 hist.hend(Some(format!("cmd{}", i)));
1337 }
1338
1339 assert_eq!(hist.len(), 3);
1340 assert!(hist.get(1).is_none());
1341 assert!(hist.get(2).is_none());
1342 }
1343}
1344
1345pub struct HistInputStack {
1351 stack: Vec<HistInputState>,
1352}
1353
1354struct HistInputState {
1355 dohist: bool,
1356}
1357
1358impl Default for HistInputStack {
1359 fn default() -> Self {
1360 Self::new()
1361 }
1362}
1363
1364impl HistInputStack {
1365 pub fn new() -> Self {
1366 HistInputStack { stack: Vec::new() }
1367 }
1368
1369 pub fn strinbeg(&mut self, dohist: bool) {
1371 self.stack.push(HistInputState { dohist });
1372 }
1373
1374 pub fn strinend(&mut self) {
1376 self.stack.pop();
1377 }
1378
1379 pub fn doing_hist(&self) -> bool {
1381 self.stack.last().map(|s| s.dohist).unwrap_or(false)
1382 }
1383}
1384
1385pub struct HistLineLink {
1387 pub linked: bool,
1388 pub line: String,
1389}
1390
1391impl HistLineLink {
1392 pub fn new() -> Self {
1393 HistLineLink {
1394 linked: false,
1395 line: String::new(),
1396 }
1397 }
1398
1399 pub fn linkcurline(&mut self, line: &str) {
1401 self.line = line.to_string();
1402 self.linked = true;
1403 }
1404
1405 pub fn unlinkcurline(&mut self) {
1407 self.linked = false;
1408 self.line.clear();
1409 }
1410}
1411
1412impl Default for HistLineLink {
1413 fn default() -> Self {
1414 Self::new()
1415 }
1416}
1417
1418impl History {
1420 pub fn movehistent(&self, start: i64, n: i64) -> Option<&HistEntry> {
1422 let target = start + n;
1423 self.get(target)
1424 }
1425
1426 pub fn up_histent(&self, current: i64) -> Option<&HistEntry> {
1428 self.get(current - 1)
1429 }
1430
1431 pub fn down_histent(&self, current: i64) -> Option<&HistEntry> {
1433 self.get(current + 1)
1434 }
1435
1436 pub fn gethistent(&self, ev: i64, near_match: bool) -> Option<&HistEntry> {
1438 if let Some(entry) = self.get(ev) {
1439 return Some(entry);
1440 }
1441 if !near_match {
1442 return None;
1443 }
1444 let mut best = None;
1446 let mut best_dist = i64::MAX;
1447 for (num, entry) in &self.entries {
1448 let dist = (*num - ev).abs();
1449 if dist < best_dist {
1450 best_dist = dist;
1451 best = Some(entry);
1452 }
1453 }
1454 best
1455 }
1456
1457 pub fn prepnexthistent(&mut self) -> i64 {
1459 self.curhist + 1
1460 }
1461}
1462
1463pub struct HistWordBuffer {
1465 buf: String,
1466 active: bool,
1467}
1468
1469impl Default for HistWordBuffer {
1470 fn default() -> Self {
1471 Self::new()
1472 }
1473}
1474
1475impl HistWordBuffer {
1476 pub fn new() -> Self {
1477 HistWordBuffer {
1478 buf: String::new(),
1479 active: false,
1480 }
1481 }
1482
1483 pub fn ihwbegin(&mut self) {
1485 self.buf.clear();
1486 self.active = true;
1487 }
1488
1489 pub fn ihwabort(&mut self) {
1491 self.active = false;
1492 self.buf.clear();
1493 }
1494
1495 pub fn ihwend(&mut self) -> Option<String> {
1497 if self.active {
1498 self.active = false;
1499 Some(std::mem::take(&mut self.buf))
1500 } else {
1501 None
1502 }
1503 }
1504
1505 pub fn add(&mut self, c: char) {
1507 if self.active {
1508 self.buf.push(c);
1509 }
1510 }
1511
1512 pub fn hwget(&self) -> &str {
1514 &self.buf
1515 }
1516}
1517
1518pub fn histbackword(line: &str, pos: usize) -> usize {
1520 if pos == 0 {
1521 return 0;
1522 }
1523 let bytes = line.as_bytes();
1524 let mut p = pos.min(bytes.len());
1525
1526 while p > 0 && bytes[p - 1].is_ascii_whitespace() {
1528 p -= 1;
1529 }
1530 while p > 0 && !bytes[p - 1].is_ascii_whitespace() {
1532 p -= 1;
1533 }
1534 p
1535}
1536
1537pub struct HistUnget {
1539 chars: Vec<char>,
1540}
1541
1542impl Default for HistUnget {
1543 fn default() -> Self {
1544 Self::new()
1545 }
1546}
1547
1548impl HistUnget {
1549 pub fn new() -> Self {
1550 HistUnget { chars: Vec::new() }
1551 }
1552
1553 pub fn ihungetc(&mut self, c: char) {
1555 self.chars.push(c);
1556 }
1557
1558 pub fn ihgetc(&mut self) -> Option<char> {
1560 self.chars.pop()
1561 }
1562
1563 pub fn has_chars(&self) -> bool {
1564 !self.chars.is_empty()
1565 }
1566}
1567
1568pub fn ihwaddc(hwbuf: &mut HistWordBuffer, c: char) {
1574 hwbuf.add(c);
1575}
1576
1577pub fn iaddtoline(line: &mut String, c: char) {
1579 line.push(c);
1580}
1581
1582pub fn safeinungetc(unget: &mut HistUnget, c: char) {
1584 unget.ihungetc(c);
1585}
1586
1587pub fn herrflush() {
1589 }
1591
1592pub fn getsubsargs(line: &str) -> Option<(String, String, bool)> {
1595 if line.len() < 2 {
1596 return None;
1597 }
1598 let sep = line.chars().next()?;
1599 let rest = &line[sep.len_utf8()..];
1600
1601 let mut old = String::new();
1602 let mut new = String::new();
1603 let mut in_new = false;
1604 let mut global = false;
1605
1606 for c in rest.chars() {
1607 if c == sep {
1608 if in_new {
1609 break;
1610 }
1611 in_new = true;
1612 continue;
1613 }
1614 if in_new {
1615 new.push(c);
1616 } else {
1617 old.push(c);
1618 }
1619 }
1620
1621 if rest.ends_with('g') && rest.len() > old.len() + new.len() + 2 {
1623 global = true;
1624 }
1625
1626 if old.is_empty() {
1627 None
1628 } else {
1629 Some((old, new, global))
1630 }
1631}
1632
1633pub fn getargc(entry: &HistEntry) -> usize {
1635 entry.num_words()
1636}
1637
1638pub fn substfailed() -> String {
1640 "substitution failed".to_string()
1641}
1642
1643pub fn digitcount(s: &str) -> usize {
1645 s.chars().take_while(|c| c.is_ascii_digit()).count()
1646}
1647
1648pub fn nohw(_c: char) {}
1650
1651pub fn nohwabort() {}
1653
1654pub fn nohwe() {}
1656
1657pub fn putoldhistentryontop(hist: &mut History) -> bool {
1659 if let Some(oldest_num) = hist.ring.first().copied() {
1661 if let Some(entry) = hist.entries.remove(&oldest_num) {
1662 hist.ring.remove(0);
1663 let new_num = hist.curhist + 1;
1664 hist.entries.insert(new_num, entry);
1665 hist.ring.push(new_num);
1666 return true;
1667 }
1668 }
1669 false
1670}
1671
1672pub fn checkcurline(hist: &History, line: &str) -> bool {
1674 hist.latest().map(|e| e.text == line).unwrap_or(false)
1675}
1676
1677pub fn quietgethist(hist: &History, ev: i64) -> Option<&HistEntry> {
1679 hist.get(ev)
1680}
1681
1682pub fn hdynread(_hist: &History) -> Option<String> {
1684 None
1687}
1688
1689pub fn inithist() -> History {
1691 History::new()
1692}
1693
1694pub fn readhistline(line: &str) -> Option<HistEntry> {
1696 let line = line.trim();
1697 if line.is_empty() {
1698 return None;
1699 }
1700 if line.starts_with(": ") {
1702 let rest = &line[2..];
1703 if let Some(semi) = rest.find(';') {
1704 let meta = &rest[..semi];
1705 let cmd = &rest[semi + 1..];
1706 let parts: Vec<&str> = meta.splitn(2, ':').collect();
1707 let timestamp = parts
1708 .first()
1709 .and_then(|s| s.parse::<i64>().ok())
1710 .unwrap_or(0);
1711 let mut entry = HistEntry::new(0, cmd.to_string());
1712 entry.stim = timestamp;
1713 return Some(entry);
1714 }
1715 }
1716 Some(HistEntry::new(0, line.to_string()))
1717}
1718
1719pub fn flockhistfile(path: &str) -> bool {
1721 #[cfg(unix)]
1722 {
1723 use std::os::unix::io::AsRawFd;
1724 if let Ok(file) = std::fs::OpenOptions::new()
1725 .write(true)
1726 .create(true)
1727 .open(format!("{}.lock", path))
1728 {
1729 let fd = file.as_raw_fd();
1730 unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) == 0 }
1731 } else {
1732 false
1733 }
1734 }
1735 #[cfg(not(unix))]
1736 {
1737 true
1738 }
1739}
1740
1741pub fn checklocktime(path: &str, max_age_secs: u64) -> bool {
1743 let lockfile = format!("{}.lock", path);
1744 if let Ok(meta) = std::fs::metadata(&lockfile) {
1745 if let Ok(modified) = meta.modified() {
1746 if let Ok(age) = modified.elapsed() {
1747 return age.as_secs() < max_age_secs;
1748 }
1749 }
1750 }
1751 false
1752}
1753
1754pub fn histsplitwords(line: &str) -> Vec<(usize, usize)> {
1756 let mut words = Vec::new();
1757 let mut in_word = false;
1758 let mut word_start = 0;
1759 let mut in_quote = false;
1760 let mut quote_char = '\0';
1761
1762 for (i, c) in line.char_indices() {
1763 if in_quote {
1764 if c == quote_char {
1765 in_quote = false;
1766 }
1767 continue;
1768 }
1769 if c == '\'' || c == '"' {
1770 in_quote = true;
1771 quote_char = c;
1772 if !in_word {
1773 word_start = i;
1774 in_word = true;
1775 }
1776 continue;
1777 }
1778 if c.is_ascii_whitespace() {
1779 if in_word {
1780 words.push((word_start, i));
1781 in_word = false;
1782 }
1783 } else if !in_word {
1784 word_start = i;
1785 in_word = true;
1786 }
1787 }
1788 if in_word {
1789 words.push((word_start, line.len()));
1790 }
1791 words
1792}
1793
1794pub struct HistStackManager {
1796 stack: Vec<HistStackFrame>,
1797}
1798
1799struct HistStackFrame {
1800 curhist: i64,
1801 histsiz: usize,
1802 histactive: u32,
1803}
1804
1805impl Default for HistStackManager {
1806 fn default() -> Self {
1807 Self::new()
1808 }
1809}
1810
1811impl HistStackManager {
1812 pub fn new() -> Self {
1813 HistStackManager { stack: Vec::new() }
1814 }
1815
1816 pub fn pushhiststack(&mut self, hist: &History) {
1818 self.stack.push(HistStackFrame {
1819 curhist: hist.curhist,
1820 histsiz: hist.histsiz as usize,
1821 histactive: hist.histactive,
1822 });
1823 }
1824
1825 pub fn pophiststack(&mut self, hist: &mut History) {
1827 if let Some(frame) = self.stack.pop() {
1828 hist.curhist = frame.curhist;
1829 hist.histsiz = frame.histsiz as i64;
1830 hist.histactive = frame.histactive;
1831 }
1832 }
1833
1834 pub fn saveandpophiststack(&mut self, hist: &mut History) {
1836 self.pophiststack(hist);
1837 }
1838}
1839
1840pub fn chrealpath(path: &str) -> Option<String> {
1842 std::fs::canonicalize(path)
1843 .ok()
1844 .map(|p| p.to_string_lossy().to_string())
1845}
1846
1847pub fn bufferwords(line: &str, cursor_pos: usize) -> (Vec<String>, usize) {
1849 let words: Vec<String> = line.split_whitespace().map(String::from).collect();
1850 let mut pos = 0;
1852 let mut word_idx = 0;
1853 for (i, word) in line.split_whitespace().enumerate() {
1854 if let Some(start) = line[pos..].find(word) {
1855 let wstart = pos + start;
1856 let wend = wstart + word.len();
1857 if cursor_pos >= wstart && cursor_pos <= wend {
1858 word_idx = i;
1859 break;
1860 }
1861 pos = wend;
1862 }
1863 }
1864 (words, word_idx)
1865}