use crate::models::HelpFormat;
use crate::scanner::man_page::{
extract_man_examples, extract_man_options, extract_man_summary, extract_man_synopsis,
is_man_page,
};
use crate::scanner::parsers::gnu::detect_structured_output;
use crate::scanner::parsers::positional_args::extract_args_from_usage_line;
use crate::scanner::protocol::{CliParser, ParsedHelp};
fn documentation_name(tool_name: &str) -> &str {
std::path::Path::new(tool_name)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(tool_name)
}
pub struct ManHelpParser;
impl CliParser for ManHelpParser {
fn name(&self) -> &str {
"man"
}
fn priority(&self) -> u32 {
90
}
fn can_parse(&self, help_text: &str, _tool_name: &str) -> bool {
is_man_page(help_text)
}
fn parse(&self, help_text: &str, tool_name: &str) -> anyhow::Result<ParsedHelp> {
let flags = extract_man_options(help_text);
let positional_args = extract_args_from_usage_line(&extract_man_synopsis(help_text));
let structured_output = detect_structured_output(&flags, help_text);
Ok(ParsedHelp {
description: extract_man_summary(help_text),
flags,
positional_args,
subcommand_names: Vec::new(),
examples: extract_man_examples(help_text, documentation_name(tool_name)),
structured_output,
help_format: HelpFormat::Man,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const GIT_LOG_MAN: &str = "\
GIT-LOG(1) Git Manual GIT-LOG(1)
NAME
git-log - Show commit logs
SYNOPSIS
git log [<options>] [<revision-range>] [[--] <path>...]
DESCRIPTION
Shows the commit logs.
OPTIONS
--follow
Continue listing the history of a file beyond renames.
-n <number>, --max-count=<number>
Limit the number of commits to output.
--oneline
This is a shorthand for \"--pretty=oneline --abbrev-commit\".
GIT
Part of the git(1) suite
";
fn parse(text: &str) -> ParsedHelp {
ManHelpParser
.parse(text, "git")
.expect("parse should succeed")
}
#[test]
fn test_man_parser_claims_a_man_page() {
assert!(ManHelpParser.can_parse(GIT_LOG_MAN, "git"));
}
#[test]
fn test_man_parser_declines_ordinary_help_output() {
let gnu = "Usage: tool [OPTIONS]\n\nOptions:\n -v, --verbose Be verbose\n";
assert!(!ManHelpParser.can_parse(gnu, "tool"));
assert!(!ManHelpParser.can_parse("see the SYNOPSIS above", "tool"));
}
#[test]
fn test_man_parser_extracts_the_subcommands_own_flags() {
let parsed = parse(GIT_LOG_MAN);
let names: Vec<&str> = parsed
.flags
.iter()
.filter_map(|f| f.long_name.as_deref())
.collect();
assert!(names.contains(&"--follow"), "{names:?}");
assert!(names.contains(&"--max-count"), "{names:?}");
assert!(names.contains(&"--oneline"), "{names:?}");
}
#[test]
fn test_man_parser_carries_the_synopsis_operands() {
let parsed = parse(GIT_LOG_MAN);
let names: Vec<&str> = parsed
.positional_args
.iter()
.map(|a| a.name.as_str())
.collect();
assert!(names.contains(&"revision-range"), "{names:?}");
assert!(names.contains(&"path"), "{names:?}");
assert!(
!names.contains(&"options"),
"the option group is not an operand: {names:?}"
);
}
#[test]
fn test_man_parser_uses_the_name_section_as_the_description() {
let parsed = parse(GIT_LOG_MAN);
assert_eq!(parsed.description, "Show commit logs");
}
#[test]
fn test_man_parser_reports_its_format() {
assert_eq!(parse(GIT_LOG_MAN).help_format, HelpFormat::Man);
}
#[test]
fn test_man_parser_finds_examples_when_scanned_by_absolute_path() {
const WITH_EXAMPLES: &str = "\
GIT-LOG(1) Git Manual GIT-LOG(1)
NAME
git-log - Show commit logs
SYNOPSIS
git log [<options>] [<revision-range>] [[--] <path>...]
EXAMPLES
git log --no-merges
Show the whole commit history, but skip any merges.
";
let by_name = ManHelpParser.parse(WITH_EXAMPLES, "git").unwrap();
let by_path = ManHelpParser.parse(WITH_EXAMPLES, "/usr/bin/git").unwrap();
assert_eq!(by_name.examples, vec!["git log --no-merges"]);
assert_eq!(
by_path.examples, by_name.examples,
"a path and a bare name must yield the same contract"
);
}
#[test]
fn test_man_parser_finds_no_subcommands() {
assert!(parse(GIT_LOG_MAN).subcommand_names.is_empty());
}
}