use crate::command::{Command, Completion};
use crate::command_set::CommandSet;
use crate::error::ShiError;
use crate::shell::Shell;
use crate::tokenizer::{DefaultTokenizer, Tokenization, Tokenizer};
pub struct Parser {
tokenizer: DefaultTokenizer,
}
#[derive(Debug, PartialEq)]
pub enum CommandType {
Builtin,
Custom,
Unknown,
}
#[derive(Debug, PartialEq)]
pub struct Outcome<'a> {
pub cmd_path: Vec<&'a str>,
pub remaining: Vec<&'a str>,
pub cmd_type: CommandType,
pub possibilities: Vec<String>,
pub leaf_completion: Option<Completion>,
pub complete: bool,
}
impl<'a> Outcome<'a> {
pub fn error(&self) -> Option<ShiError> {
if !self.complete {
Some(ShiError::ParseError {
msg: self.error_msg(),
cmd_path: self.cmd_path.iter().map(|s| s.to_string()).collect(),
remaining: self.remaining.iter().map(|s| s.to_string()).collect(),
possibilities: self.possibilities.clone(),
})
} else {
None
}
}
pub fn error_msg(&self) -> String {
if self.complete {
return String::from("");
}
let mut msg = String::new();
if self.cmd_path.is_empty() && self.remaining.is_empty() {
msg += "Empty string could not be parsed as a command.";
} else if self.cmd_path.is_empty() {
if let Some(first_remaining_word) = self.remaining.get(0) {
msg.push_str(&format!(
"'{}' is not a recognized command.",
first_remaining_word
));
} else {
unreachable!("remaining unparsed tokens cannot be empty at this point")
}
} else {
msg += "Failed to parse fully:\n";
msg += "\n";
let valid_prefix = self.cmd_path.join(" ");
let invalid_suffix = self.remaining.join(" ");
msg += "\t (spaces trimmed)\n";
if self.remaining.is_empty() {
msg += &format!("\t => '{} '\n", valid_prefix);
} else {
msg += &format!("\t => '{} {}'\n", valid_prefix, invalid_suffix);
}
msg += &format!("\t {}^\n", " ".repeat(valid_prefix.len() + 1));
msg += "expected a valid subcommand\n";
msg += "instead, got: ";
if let Some(first_remaining_word) = self.remaining.get(0) {
msg += &format!("'{}';\n", first_remaining_word);
} else {
msg += "nothing;\n"
}
msg += "\n";
msg.push_str(&format!(
"Run '{} help' for more info on the command.",
valid_prefix
));
}
if !self.possibilities.is_empty() {
msg += "\n\n";
msg.push_str(&format!(
"\t => expected one of {}.\n",
self.possibilities
.iter()
.map(|s| format!("'{}'", s))
.collect::<Vec<String>>()
.join(" or ")
))
}
msg += "\n";
msg += "Run 'helptree' for more info on the entire command tree.\n";
msg
}
}
impl Parser {
pub fn new() -> Parser {
Parser {
tokenizer: DefaultTokenizer::new(vec!['\'', '"']),
}
}
fn parse_tokens_with_set<'a, T>(
&self,
tokenization: &Tokenization<'a>,
cmd_type: CommandType,
set: &CommandSet<T>,
) -> Outcome<'a> {
let mut cmd_path: Vec<&str> = Vec::new();
let mut current_set = set;
for (i, token) in tokenization.tokens.iter().enumerate() {
let looked_up_cmd = match current_set.get(token) {
Some(cmd) => {
cmd_path.push(token);
cmd
}
None => {
return Outcome {
cmd_path,
remaining: tokenization.tokens.get(i..).unwrap().to_vec(),
cmd_type: if i == 0 {
CommandType::Unknown
} else {
cmd_type
},
possibilities: current_set.names(),
leaf_completion: None,
complete: false,
};
}
};
match &**looked_up_cmd {
Command::Leaf(cmd) => {
return Outcome {
cmd_path,
remaining: tokenization.tokens.get(i + 1..).unwrap().to_vec(),
cmd_type,
possibilities: Vec::new(),
leaf_completion: Some(cmd.autocomplete(
tokenization.tokens.get(i + 1..).unwrap().to_vec(),
tokenization.trailing_space,
)),
complete: true,
};
}
Command::Parent(cmd) => {
current_set = cmd.sub_commands();
}
}
}
Outcome {
cmd_path,
remaining: Vec::new(), cmd_type: if tokenization.tokens.is_empty() {
CommandType::Unknown
} else {
cmd_type
},
possibilities: current_set.names(),
leaf_completion: None,
complete: false,
}
}
fn parse_tokens<'a, S>(
&self,
tokenization: &Tokenization<'a>,
cmds: &CommandSet<S>,
builtins: &CommandSet<Shell<S>>,
) -> Outcome<'a> {
let cmd_outcome = self.parse_tokens_with_set(tokenization, CommandType::Custom, cmds);
if cmd_outcome.complete {
return cmd_outcome;
}
let builtin_outcome =
self.parse_tokens_with_set(tokenization, CommandType::Builtin, builtins);
if builtin_outcome.complete {
return builtin_outcome;
}
cmd_outcome
}
pub fn parse<'a, S>(
&self,
line: &'a str,
cmds: &CommandSet<S>,
builtins: &CommandSet<Shell<S>>,
) -> Outcome<'a> {
let tokenization = self.tokenizer.tokenize(line);
self.parse_tokens(&tokenization, cmds, builtins)
}
}
#[cfg(test)]
pub mod test {
use super::*;
use crate::command::BaseCommand;
use crate::Result;
use pretty_assertions::assert_eq;
use std::marker::PhantomData;
#[derive(Debug)]
struct ParseTestCommand<'a, S> {
name: &'a str,
autocompletions: Vec<&'a str>,
phantom: PhantomData<S>,
}
impl<'a, S> ParseTestCommand<'a, S> {
fn new(name: &str) -> ParseTestCommand<S> {
ParseTestCommand {
name,
autocompletions: Vec::new(),
phantom: PhantomData,
}
}
fn new_with_completions(
name: &'a str,
completions: Vec<&'a str>,
) -> ParseTestCommand<'a, S> {
ParseTestCommand {
name,
autocompletions: completions,
phantom: PhantomData,
}
}
}
impl<'a, S> BaseCommand for ParseTestCommand<'a, S> {
type State = S;
fn name(&self) -> &str {
self.name
}
#[cfg(not(tarpaulin_include))]
fn validate_args(&self, _: &[String]) -> Result<()> {
Ok(())
}
fn autocomplete(&self, args: Vec<&str>, _: bool) -> Completion {
if self.autocompletions.is_empty() {
return Completion::Nothing;
}
match args.last() {
Some(last) => {
if self.autocompletions.iter().filter(|s| s == &last).count() > 0 {
Completion::Nothing
} else {
let prefix_matches: Vec<String> = self
.autocompletions
.iter()
.filter(|s| s.starts_with(last))
.map(|s| s.to_string())
.collect();
if prefix_matches.is_empty() {
return Completion::Nothing;
}
Completion::PartialArgCompletion(prefix_matches)
}
}
None => Completion::Possibilities(
self.autocompletions.iter().map(|s| s.to_string()).collect(),
),
}
}
#[cfg(not(tarpaulin_include))]
fn execute(&self, _: &mut S, _: &[String]) -> Result<String> {
Ok(String::from(""))
}
}
pub fn make_parser_cmds<'a>() -> (CommandSet<'a, ()>, CommandSet<'a, Shell<'a, ()>>) {
(
CommandSet::new_from_vec(vec![
Command::new_parent(
"foo-c",
vec![
Command::new_leaf(ParseTestCommand::new("bar-c")),
Command::new_leaf(ParseTestCommand::new("baz-c")),
Command::new_parent(
"qux-c",
vec![
Command::new_leaf(ParseTestCommand::new("quux-c")),
Command::new_leaf(ParseTestCommand::new("corge-c")),
],
),
],
),
Command::new_leaf(ParseTestCommand::new("grault-c")),
Command::new_leaf(ParseTestCommand::new("conflict-tie")),
Command::new_leaf(ParseTestCommand::new(
"conflict-builtin-longer-match-but-still-loses",
)),
Command::new_parent(
"conflict-custom-wins",
vec![Command::new_leaf(ParseTestCommand::new("child"))],
),
]),
CommandSet::new_from_vec(vec![
Command::new_parent(
"foo-b",
vec![Command::new_leaf(ParseTestCommand::new_with_completions(
"bar-b",
vec!["ho", "he", "bum"],
))],
),
Command::new_leaf(ParseTestCommand::new("conflict-tie")),
Command::new_leaf(ParseTestCommand::new("conflict-custom-wins")),
Command::new_parent(
"conflict-builtin-longer-match-but-still-loses",
vec![Command::new_leaf(ParseTestCommand::new("child"))],
),
]),
)
}
#[test]
fn nesting() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-c bar-c he", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-c", "bar-c"],
remaining: vec!["he"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn no_nesting_no_args() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-c bar-c", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-c", "bar-c"],
remaining: vec![],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn end_with_no_args_but_is_parent() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-c qux-c", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-c", "qux-c"],
remaining: vec![],
cmd_type: CommandType::Custom,
possibilities: vec![String::from("quux-c"), String::from("corge-c")],
leaf_completion: None,
complete: false,
}
);
}
#[test]
fn builtin_nesting() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-b bar-b he", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-b", "bar-b"],
remaining: vec!["he"],
cmd_type: CommandType::Builtin,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn empty() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec![],
remaining: vec![],
cmd_type: CommandType::Unknown,
possibilities: vec![
String::from("foo-c"),
String::from("grault-c"),
String::from("conflict-tie"),
String::from("conflict-builtin-longer-match-but-still-loses"),
String::from("conflict-custom-wins"),
],
leaf_completion: None,
complete: false,
}
);
}
#[test]
fn invalid_subcmd() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-c he", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-c"],
remaining: vec!["he"],
cmd_type: CommandType::Custom,
possibilities: vec![
String::from("bar-c"),
String::from("baz-c"),
String::from("qux-c"),
],
leaf_completion: None,
complete: false,
}
);
}
#[test]
fn no_nesting() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("grault-c la la", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["grault-c"],
remaining: vec!["la", "la"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn no_args_no_nesting() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("grault-c", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["grault-c"],
remaining: vec![],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn cmd_has_args_that_match_other_cmds() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("grault-c foo-c bar-c", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["grault-c"],
remaining: vec!["foo-c", "bar-c"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn nonexistent_cmd() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("notacmd", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec![],
remaining: vec!["notacmd"],
cmd_type: CommandType::Unknown,
possibilities: vec![
String::from("foo-c"),
String::from("grault-c"),
String::from("conflict-tie"),
String::from("conflict-builtin-longer-match-but-still-loses"),
String::from("conflict-custom-wins"),
],
leaf_completion: None,
complete: false,
}
);
}
#[test]
fn args_with_nonexistent_cmd() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("notacmd la la", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec![],
remaining: vec!["notacmd", "la", "la"],
cmd_type: CommandType::Unknown,
possibilities: vec![
String::from("foo-c"),
String::from("grault-c"),
String::from("conflict-tie"),
String::from("conflict-builtin-longer-match-but-still-loses"),
String::from("conflict-custom-wins"),
],
leaf_completion: None,
complete: false,
}
);
}
#[test]
fn three_levels_deep() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-c qux-c quux-c la la", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-c", "qux-c", "quux-c"],
remaining: vec!["la", "la"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn perfect_tie_custom_wins_tie_breaker() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("conflict-tie ha ha", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["conflict-tie"],
remaining: vec!["ha", "ha"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn conflict_but_builtin_has_longer_match() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse(
"conflict-builtin-longer-match-but-still-loses child ha",
&cmds.0,
&cmds.1
),
Outcome {
cmd_path: vec!["conflict-builtin-longer-match-but-still-loses"],
remaining: vec!["child", "ha"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn conflict_but_custom_has_longer_match() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("conflict-custom-wins child ha", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["conflict-custom-wins", "child"],
remaining: vec!["ha"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn cmd_level_partial_autocompletion_multiple_choices() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-b bar-b h", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-b", "bar-b"],
remaining: vec!["h"],
cmd_type: CommandType::Builtin,
possibilities: Vec::new(),
leaf_completion: Some(Completion::PartialArgCompletion(vec![
String::from("ho"),
String::from("he")
])),
complete: true,
}
);
}
#[test]
fn cmd_level_partial_autocompletion_single_choice() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-b bar-b b", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-b", "bar-b"],
remaining: vec!["b"],
cmd_type: CommandType::Builtin,
possibilities: Vec::new(),
leaf_completion: Some(Completion::PartialArgCompletion(vec![String::from("bum"),])),
complete: true,
}
);
}
#[test]
fn cmd_level_completion_all_options() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-b bar-b", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-b", "bar-b"],
remaining: vec![],
cmd_type: CommandType::Builtin,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Possibilities(vec![
String::from("ho"),
String::from("he"),
String::from("bum"),
])),
complete: true,
}
);
}
#[test]
fn cmd_level_completion_no_matches() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-b bar-b z", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-b", "bar-b"],
remaining: vec!["z"],
cmd_type: CommandType::Builtin,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
#[test]
fn cmd_level_completion_already_complete() {
let cmds = make_parser_cmds();
assert_eq!(
Parser::new().parse("foo-b bar-b bum", &cmds.0, &cmds.1),
Outcome {
cmd_path: vec!["foo-b", "bar-b"],
remaining: vec!["bum"],
cmd_type: CommandType::Builtin,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
}
);
}
mod outcome {
use super::{CommandType, Completion, Outcome};
use pretty_assertions::assert_eq;
#[test]
fn outcome_error_msg() {
let outcome = Outcome {
cmd_path: vec!["foo", "bar"],
remaining: vec!["la", "la"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: None,
complete: false,
};
assert_eq!(
outcome.error_msg(),
vec![
"Failed to parse fully:\n",
"\n",
"\t (spaces trimmed)\n",
"\t => 'foo bar la la'\n",
"\t ^\n",
"expected a valid subcommand\n",
"instead, got: 'la';\n",
"\n",
"Run 'foo bar help' for more info on the command.\n",
"Run 'helptree' for more info on the entire command tree.\n",
]
.join(""),
);
}
#[test]
fn empty_remaining_in_outcome() {
let outcome = Outcome {
cmd_path: vec!["foo", "bar"],
remaining: vec![],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: None,
complete: false,
};
assert_eq!(
outcome.error_msg(),
vec![
"Failed to parse fully:\n",
"\n",
"\t (spaces trimmed)\n",
"\t => 'foo bar '\n",
"\t ^\n",
"expected a valid subcommand\n",
"instead, got: nothing;\n",
"\n",
"Run 'foo bar help' for more info on the command.\n",
"Run 'helptree' for more info on the entire command tree.\n",
]
.join(""),
);
}
#[test]
fn empty() {
let outcome = Outcome {
cmd_path: vec![],
remaining: vec![],
cmd_type: CommandType::Custom,
possibilities: vec![
String::from("conflict-tie"),
String::from("conflict-builtin-longer-match-but-still-loses"),
String::from("conflict-custom-wins"),
String::from("foo-c"),
String::from("grault-c"),
],
leaf_completion: None,
complete: false,
};
assert_eq!(
outcome.error_msg(),
vec![
"Empty string could not be parsed as a command.\n",
"\n",
"\t => expected one of 'conflict-tie' or 'conflict-builtin-longer-match-but-still-loses' or 'conflict-custom-wins' or 'foo-c' or 'grault-c'.",
"\n",
"\n",
"Run 'helptree' for more info on the entire command tree.\n",
]
.join(""),
);
}
#[test]
fn unrecognized_first_cmd() {
let outcome = Outcome {
cmd_path: vec![],
remaining: vec!["notfound", "la"],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: None,
complete: false,
};
assert_eq!(
outcome.error_msg(),
vec![
"'notfound' is not a recognized command.\n",
"Run 'helptree' for more info on the entire command tree.\n",
]
.join(""),
);
}
#[test]
fn error_msg_is_blank_for_complete_parse() {
let outcome = Outcome {
cmd_path: vec![],
remaining: vec![],
cmd_type: CommandType::Custom,
possibilities: Vec::new(),
leaf_completion: Some(Completion::Nothing),
complete: true,
};
assert_eq!(outcome.error_msg(), String::from(""));
}
}
}