use crate::commands::COMMANDS;
use exfiltrate_internal::args::{ArgKind, ArgSpec, usage_block};
use exfiltrate_internal::command::{Command, Response};
pub struct Help;
static ARGS: &[ArgSpec] =
&[ArgSpec::positional("command", "the command to describe", ArgKind::String).required()];
impl Command for Help {
fn name(&self) -> &'static str {
"help"
}
fn short_description(&self) -> &'static str {
"Provides detailed help for a command. Use this to learn more about an individual command."
}
fn full_description(&self) -> &'static str {
"Provides detailed help for a command."
}
fn args(&self) -> &'static [ArgSpec] {
ARGS
}
fn execute(&self, args: Vec<String>) -> Result<Response, Response> {
let parsed = exfiltrate_internal::args::ParsedArgs::parse(self.args(), args)
.map_err(|error| Response::String(format!("{error}\n{}", usage_line())))?;
let requested = parsed.get("command").expect("`command` is required");
let registry = COMMANDS.lock_sync_read();
let Some(command) = registry.get(requested) else {
let mut message = format!("No such command: {requested}\n\nAvailable commands:\n");
for name in registry.keys() {
message.push_str(" ");
message.push_str(name);
message.push('\n');
}
return Err(message.into());
};
Ok(describe(command.as_ref()).into())
}
}
fn usage_line() -> String {
exfiltrate_internal::args::usage_line("help", ARGS)
}
pub(crate) fn describe(command: &dyn Command) -> String {
let mut out = command.full_description().trim_end().to_string();
if !command.args().is_empty() {
out.push_str("\n\n");
out.push_str(&usage_block(command.name(), command.args()));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
static DECLARED_ARGS: &[ArgSpec] = &[
ArgSpec::positional("path", "which file", ArgKind::Path).required(),
ArgSpec::flag("lines", "how many lines", ArgKind::Integer),
];
struct Declared;
impl Command for Declared {
fn name(&self) -> &'static str {
"declared"
}
fn short_description(&self) -> &'static str {
"a command with a schema"
}
fn full_description(&self) -> &'static str {
"Does a thing."
}
fn args(&self) -> &'static [ArgSpec] {
DECLARED_ARGS
}
fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
Ok("".into())
}
}
struct Undeclared;
impl Command for Undeclared {
fn name(&self) -> &'static str {
"undeclared"
}
fn short_description(&self) -> &'static str {
"a command without a schema"
}
fn full_description(&self) -> &'static str {
"Does a thing.\nUsage: exfiltrate undeclared [WHATEVER]"
}
fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
Ok("".into())
}
}
#[test]
fn a_declared_command_gets_a_generated_usage_block() {
let text = describe(&Declared);
assert!(text.starts_with("Does a thing."), "{text}");
assert!(
text.contains("Usage: exfiltrate declared <PATH> [--lines <INTEGER>]"),
"{text}"
);
assert!(text.contains("which file"), "{text}");
assert!(text.contains("how many lines"), "{text}");
}
#[test]
fn an_undeclared_command_keeps_its_hand_written_prose_untouched() {
let text = describe(&Undeclared);
assert_eq!(
text,
"Does a thing.\nUsage: exfiltrate undeclared [WHATEVER]"
);
}
#[test]
fn asking_for_nothing_reports_the_usage_rather_than_an_empty_answer() {
let error = Help.execute(Vec::new()).unwrap_err().to_string();
assert!(
error.contains("Usage: exfiltrate help <COMMAND>"),
"{error}"
);
}
#[test]
fn an_unknown_command_lists_the_ones_that_do_exist() {
crate::commands::insert(Box::new(Declared)).ok();
let error = Help
.execute(vec!["not_a_command".to_string()])
.unwrap_err()
.to_string();
assert!(error.contains("No such command: not_a_command"), "{error}");
assert!(error.contains("declared"), "{error}");
}
}