#[derive(Debug, Clone, PartialEq)]
pub enum Head<'a> {
Bare,
Sub { sub: &'a str, rest: Vec<&'a str> },
}
pub fn head<'a>(input: &'a str, name: &str) -> Option<Head<'a>> {
let mut tokens = input.split_whitespace();
let first = tokens.next()?;
let expected = format!("/{name}");
if !first.eq_ignore_ascii_case(&expected) {
return None;
}
match tokens.next() {
None => Some(Head::Bare),
Some(sub) => Some(Head::Sub {
sub,
rest: tokens.collect(),
}),
}
}
pub fn argument(tokens: &[&str]) -> Option<String> {
let joined = tokens.join(" ");
let arg = joined.trim().trim_matches(|c| c == '"' || c == '\'').trim();
(!arg.is_empty()).then(|| arg.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognizes_the_command_and_its_subcommand() {
assert_eq!(head("/project", "project"), Some(Head::Bare));
assert_eq!(
head("/PROJECT Attach D:/x", "project"),
Some(Head::Sub {
sub: "Attach",
rest: vec!["D:/x"]
})
);
}
#[test]
fn a_longer_word_is_not_the_command() {
assert_eq!(head("/projects", "project"), None);
assert_eq!(head("project attach x", "project"), None);
assert_eq!(head("tell me about /project", "project"), None);
}
#[test]
fn an_argument_keeps_spaces_and_loses_quotes() {
assert_eq!(
argument(&["\"C:\\My", "Projects\\app\""]),
Some("C:\\My Projects\\app".to_string())
);
assert_eq!(argument(&[]), None);
assert_eq!(argument(&[" "]), None);
}
}