use std::marker::PhantomData;
use crate::command::{BaseCommand, Command};
use crate::command_set::CommandSet;
use crate::error::ShiError;
use crate::parser::CommandType;
use crate::shell::Shell;
use crate::Result;
#[derive(Debug)]
pub struct HelpCommand<'a, S> {
phantom: &'a PhantomData<S>,
}
impl<'a, S> Default for HelpCommand<'a, S> {
fn default() -> Self {
Self::new()
}
}
impl<'a, S> HelpCommand<'a, S> {
pub fn new() -> HelpCommand<'a, S> {
HelpCommand {
phantom: &PhantomData,
}
}
fn execute_no_args(&self, shell: &mut Shell<S>) -> String {
let mut help_lines: Vec<String> =
Vec::with_capacity(shell.cmds.borrow().len() + shell.builtins.len() + 2);
help_lines.push(String::from("Normal commands:"));
for cmd in shell.cmds.borrow().iter() {
help_lines.push(format!("\t'{}' - {}", cmd.name(), cmd.help()));
}
help_lines.push(String::from("Built-in commands:"));
for builtin in shell.builtins.iter() {
help_lines.push(format!("\t'{}' - {}", builtin.name(), builtin.help()))
}
help_lines.join("\n")
}
fn help_breakdown<T>(
&self,
cmd_path: Vec<&str>,
invocation_args: Vec<&str>,
cmds: &CommandSet<T>,
) -> Result<String> {
let mut lines = Vec::with_capacity(cmd_path.len() + 1);
let mut current_cmds = cmds;
for (indent, segment) in cmd_path.iter().enumerate() {
match current_cmds.get(segment) {
Some(cmd) => {
let cmd_name = cmd.name();
let help_msg = cmd.help();
lines.push(format!(
"{}└─ {} - {}",
" ".repeat(indent), cmd_name,
help_msg
));
match &**cmd {
Command::Parent(parent) => current_cmds = parent.sub_commands(),
Command::Leaf(_) => {
let mut called_with_msg =
format!("Called with args: [{}]", invocation_args.join(", "));
if invocation_args.is_empty() {
called_with_msg = String::from("Called with no args")
}
lines.push(format!(
"{}└─ {}",
" ".repeat(indent + 1),
called_with_msg,
));
}
};
}
None => {
return Err(ShiError::UnrecognizedCommand {
got: segment.to_string(),
})
}
}
}
Ok(lines.join("\n"))
}
fn execute_with_args(&self, shell: &mut Shell<S>, args: &[String]) -> Result<String> {
let invocation = args.join(" ");
let outcome = shell.parse(&invocation);
return match outcome.cmd_type {
CommandType::Custom => {
self.help_breakdown(outcome.cmd_path, outcome.remaining, &shell.cmds.borrow())
}
CommandType::Builtin => {
self.help_breakdown(outcome.cmd_path, outcome.remaining, &shell.builtins)
}
CommandType::Unknown => Err(outcome
.error()
.expect("unknown command type, but could not produce error")),
};
}
}
impl<'a, S> BaseCommand for HelpCommand<'a, S> {
type State = Shell<'a, S>;
fn name(&self) -> &str {
"help"
}
fn validate_args(&self, _: &[String]) -> Result<()> {
Ok(())
}
fn execute(&self, shell: &mut Shell<S>, args: &[String]) -> Result<String> {
if args.is_empty() {
Ok(self.execute_no_args(shell))
} else {
self.execute_with_args(shell, args)
}
}
fn help(&self) -> String {
String::from("Prints help info for root commands or explains a given command invocation")
}
}
#[cfg(test)]
mod test {
use super::HelpCommand;
use crate::command::BaseCommand;
use crate::shell::Shell;
use crate::Result;
use crate::{leaf, parent};
use pretty_assertions::assert_eq;
use std::marker::PhantomData;
#[derive(Debug)]
struct TestCommand<'a, S> {
name: &'a str,
help: &'a str,
phantom: PhantomData<S>,
}
impl<'a, S> TestCommand<'a, S> {
fn new(name: &'a str, help: &'a str) -> TestCommand<'a, S> {
TestCommand {
name,
help,
phantom: PhantomData,
}
}
}
impl<'a, S> BaseCommand for TestCommand<'a, S> {
type State = S;
fn name(&self) -> &str {
self.name
}
#[cfg(not(tarpaulin_include))]
fn validate_args(&self, _: &[String]) -> Result<()> {
Ok(())
}
#[cfg(not(tarpaulin_include))]
fn execute(&self, _: &mut S, _: &[String]) -> Result<String> {
Ok(String::from(""))
}
fn help(&self) -> String {
self.help.to_string()
}
}
fn run_help_test(args: Vec<String>, expected: String) -> Result<()> {
let mut shell = Shell::new("");
shell.register(leaf!(TestCommand::new("leaf", "1")))?;
shell.register(parent!(
"foo",
"2",
leaf!(TestCommand::new("bar", "2.1")),
leaf!(TestCommand::new("baz", "2.2")),
parent!(
"qux",
"2.3",
leaf!(TestCommand::new("quuz", "2.3.1")),
leaf!(TestCommand::new("corge", "2.3.2")),
),
leaf!(TestCommand::new("quux", "2.4")),
))?;
verify_help_output(&mut shell, args, expected);
Ok(())
}
fn run_help_test_no_cmds(args: Vec<String>, expected: String) {
let mut shell = Shell::new("");
verify_help_output(&mut shell, args, expected);
}
fn verify_help_output(shell: &mut Shell<()>, args: Vec<String>, expected: String) {
let help_cmd = HelpCommand::new();
match help_cmd.execute(shell, &args) {
Ok(help_output) => {
println!("{}", help_output);
assert_eq!(help_output, expected);
}
Err(err) => {
assert_eq!(format!("{}", err), expected)
}
};
}
#[test]
fn help_with_no_args_gives_list() -> Result<()> {
run_help_test(
vec![],
String::from(
"\
Normal commands:\n\t\
\'leaf\' - 1\n\t\
\'foo\' - 2\n\
Built-in commands:\n\t\
\'help\' - Prints help info for root commands or explains a given command invocation\n\t\
\'helptree\' - Prints a tree depiction of all commands in this shell\n\t\
\'exit\' - Exits the shell session\n\t\
\'history\' - Prints the history of commands",
),
)
}
#[test]
fn help_with_no_args_and_no_cmds() {
run_help_test_no_cmds(
vec![],
String::from(
"\
Normal commands:\n\
Built-in commands:\n\t\
\'help\' - Prints help info for root commands or explains a given command invocation\n\t\
\'helptree\' - Prints a tree depiction of all commands in this shell\n\t\
\'exit\' - Exits the shell session\n\t\
\'history\' - Prints the history of commands\
"),
)
}
#[test]
fn help_on_root_leaf_cmd() -> Result<()> {
run_help_test(
vec![String::from("leaf")],
String::from("└─ leaf - 1\n └─ Called with no args"),
)
}
#[test]
fn help_on_root_parent_cmd() -> Result<()> {
run_help_test(vec![String::from("foo")], String::from("└─ foo - 2"))
}
#[test]
fn help_on_depth_2() -> Result<()> {
run_help_test(
vec![String::from("foo"), String::from("bar")],
String::from("└─ foo - 2\n └─ bar - 2.1\n └─ Called with no args"),
)
}
#[test]
fn help_on_depth_3() -> Result<()> {
run_help_test(
vec![
String::from("foo"),
String::from("qux"),
String::from("quuz"),
],
String::from(
"└─ foo - 2\n └─ qux - 2.3\n └─ quuz - 2.3.1\n └─ Called with no args",
),
)
}
#[test]
fn help_on_depth_2_with_1_leaf_arg() -> Result<()> {
run_help_test(
vec![
String::from("foo"),
String::from("bar"),
String::from("hello"),
],
String::from("└─ foo - 2\n └─ bar - 2.1\n └─ Called with args: [hello]"),
)
}
#[test]
fn help_on_depth_2_with_2_leaf_args() -> Result<()> {
run_help_test(
vec![
String::from("foo"),
String::from("bar"),
String::from("hello"),
String::from("world"),
],
String::from("└─ foo - 2\n └─ bar - 2.1\n └─ Called with args: [hello, world]"),
)
}
#[test]
fn invalid_command_invocation() -> Result<()> {
run_help_test(
vec![String::from("DNE")],
String::from(
"\
command failed to parse: \'DNE\' is not a recognized command.\n\n\t \
=> expected one of \'leaf\' or \'foo\'.\n\n\
Run \'helptree\' for more info on the entire command tree.\n\
",
),
)
}
}