use once_cell::sync::Lazy;
use regex::Regex;
pub(crate) fn match_prompt_line(line: &str) -> Option<(&str, &str)> {
for (re, has_cmd) in PROMPT_PATTERNS.iter() {
if let Some(cap) = re.captures(line) {
let prompt = cap.name("prompt").map(|m| m.as_str()).unwrap_or_default();
return if *has_cmd {
let cmd = cap.name("cmd").map(|m| m.as_str()).unwrap_or_default();
Some((prompt, cmd))
} else {
Some((prompt, ""))
};
}
}
None
}
#[rustfmt::skip]
pub(crate) static PROMPT_PATTERNS: Lazy<Vec<(Regex, bool)>> = Lazy::new(|| vec![
(Regex::new(r"^(?P<prompt>.+?[#\$]) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>.+?[#\$])$").unwrap(), false),
(Regex::new(r"^(?P<prompt>➜ ~) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>➜ ~)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>git:\(.+\)(?: ✗)?)\s+(?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>git:\(.+\)(?: ✗)?)\s*$").unwrap(), false),
(Regex::new(r"^(?P<prompt>PS [A-Z]:\\[^>]+>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>PS [A-Z]:\\[^>]+>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>[A-Z]:\\>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>[A-Z]:\\>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>>>>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>>>>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>>)(?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>mysql>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>mysql>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>sqlite>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>sqlite>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>psql=#) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>psql=#)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>SQL>|[A-Z]+@[A-Z0-9]+>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>SQL>|[A-Z]+@[A-Z0-9]+>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>\S+[>#]) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>\S+[>#])$").unwrap(), false),
(Regex::new(r"^(?P<prompt><(Huawei|H3C)>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt><(Huawei|H3C)>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>\[.*\]) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>\[.*\])$").unwrap(), false),
(Regex::new(r"^(?P<prompt>HP-iLO>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>HP-iLO>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>AS400>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>AS400>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>UCSM#) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>UCSM#)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>uemcli>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>uemcli>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>symcli>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>symcli>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>CLI>|3PAR>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>CLI>|3PAR>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>HNAS>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>HNAS>)$").unwrap(), false),
(Regex::new(r"^(?P<prompt>cluster::>|.*\*>) (?P<cmd>.+)$").unwrap(), true), (Regex::new(r"^(?P<prompt>cluster::>|.*\*>)$").unwrap(), false),
]);
#[cfg(test)]
mod tests {
use super::match_prompt_line;
#[test]
fn test_bash_prompt() {
let line = "user@localhost:~$ echo hello";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("user@localhost:~$", "echo hello")));
}
#[test]
fn test_root_prompt() {
let line = "root@localhost:/etc# cat hosts";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("root@localhost:/etc#", "cat hosts")));
}
#[test]
fn test_oh_my_zsh() {
let line = "➜ ~ cargo run";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("➜ ~", "cargo run")));
}
#[test]
fn test_git_branch_zsh() {
let line = "git:(main) ✗ git commit -m 'fix'";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("git:(main) ✗", "git commit -m 'fix'")));
}
#[test]
fn test_powershell() {
let line = "PS C:\\Users\\Admin> Get-Process";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("PS C:\\Users\\Admin>", "Get-Process")));
}
#[test]
fn test_cmd() {
let line = "C:\\> dir";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("C:\\>", "dir")));
}
#[test]
fn test_python_prompt() {
let line = ">>> print(\"hello\")";
let matched = match_prompt_line(line);
assert_eq!(matched, Some((">>>", "print(\"hello\")")));
}
#[test]
fn test_mysql_prompt() {
let line = "mysql> SELECT * FROM users;";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("mysql>", "SELECT * FROM users;")));
}
#[test]
fn test_postgres_prompt() {
let line = "psql=# SELECT now();";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("psql=#", "SELECT now();")));
}
#[test]
fn test_cisco_prompt() {
let line = "Router# show ip interface brief";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("Router#", "show ip interface brief")));
}
#[test]
fn test_3par_prompt() {
let line = "3PAR> showvv";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("3PAR>", "showvv")));
}
#[test]
fn test_netapp_prompt() {
let line = "cluster::> volume show";
let matched = match_prompt_line(line);
assert_eq!(matched, Some(("cluster::>", "volume show")));
}
#[test]
fn test_prompt_only_lines() {
let cases = [
("root@localhost:~#", "root@localhost:~#"),
("user@host:~$", "user@host:~$"),
("[root@host:/etc]#", "[root@host:/etc]#"),
("➜ ~", "➜ ~"),
("git:(main)", "git:(main)"),
("git:(dev ✗)", "git:(dev ✗)"),
("PS C:\\Users\\admin>", "PS C:\\Users\\admin>"),
("C:\\>", "C:\\>"),
(">>>", ">>>"),
(">", ">"),
("mysql>", "mysql>"),
("sqlite>", "sqlite>"),
("psql=#", "psql=#"),
("SQL>", "SQL>"),
("SYS@XE>", "SYS@XE>"),
("Router#", "Router#"),
("Switch>", "Switch>"),
("<Huawei>", "<Huawei>"),
("<H3C>", "<H3C>"),
("[Huawei]", "[Huawei]"),
("[GigabitEthernet0/0]", "[GigabitEthernet0/0]"),
("HP-iLO>", "HP-iLO>"),
("AS400>", "AS400>"),
("UCSM#", "UCSM#"),
("uemcli>", "uemcli>"),
("symcli>", "symcli>"),
("CLI>", "CLI>"),
("3PAR>", "3PAR>"),
("HNAS>", "HNAS>"),
("cluster::*>", "cluster::*>"),
];
for (input, expected_prompt) in cases {
let matched = match_prompt_line(input);
assert_eq!(
matched,
Some((expected_prompt, "")),
"Failed to match prompt-only line: {}",
input
);
}
}
}