shellhist_core/
powershell.rs1use crate::{HistoryEntry, Shell};
14
15fn ends_with_continuation(line: &str) -> bool {
16 line.bytes().rev().take_while(|&b| b == b'`').count() % 2 == 1
19}
20
21#[must_use]
23pub fn parse(data: &[u8]) -> Vec<HistoryEntry> {
24 let text = String::from_utf8_lossy(crate::strip_bom(data));
25 let mut entries = Vec::new();
26 let mut current = String::new();
27 let mut continuing = false;
28
29 for line in text.split('\n') {
30 if continuing {
31 current.push('\n');
32 current.push_str(line);
33 } else {
34 current = line.to_string();
35 }
36 if ends_with_continuation(line) {
37 current.pop(); continuing = true;
39 } else {
40 if !current.is_empty() {
41 entries.push(HistoryEntry::plain(
42 Shell::PowerShell,
43 std::mem::take(&mut current),
44 ));
45 }
46 current.clear();
47 continuing = false;
48 }
49 }
50 if continuing && !current.is_empty() {
51 entries.push(HistoryEntry::plain(Shell::PowerShell, current));
52 }
53 entries
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn plain_lines_become_entries_without_timestamps() {
62 let e = parse(b"Get-Process\nGet-ChildItem\n");
63 assert_eq!(e.len(), 2);
64 assert_eq!(e[0].command, "Get-Process");
65 assert!(e.iter().all(|x| x.timestamp.is_none()));
66 assert_eq!(e[0].shell, Shell::PowerShell);
67 }
68
69 #[test]
70 fn leading_bom_is_stripped() {
71 let e = parse(b"\xEF\xBB\xBFGet-Process\n");
72 assert_eq!(e.len(), 1);
73 assert_eq!(e[0].command, "Get-Process");
74 }
75
76 #[test]
77 fn backtick_continuation_rejoins_a_multiline_command() {
78 let e = parse(b"Get-Process |`\nWhere-Object CPU -gt 1\nls\n");
79 assert_eq!(e.len(), 2);
80 assert_eq!(e[0].command, "Get-Process |\nWhere-Object CPU -gt 1");
81 assert_eq!(e[1].command, "ls");
82 }
83
84 #[test]
85 fn empty_input_yields_no_entries() {
86 assert!(parse(b"").is_empty());
87 }
88}