mod description; mod helpers; mod options; mod positionals; mod subcommands; pub use options::{param_parser, parse_usage_flags, switch_parser}; pub use positionals::{ extract_cli11_positionals, extract_usage_positionals, parse_usage_args, skip_command_name, }; use std::collections::HashMap; use crate::{ parsers::help::{ description::description, helpers::{eol, get_indent, rest_of_line}, subcommands::subcommand_entry, }, types::*, }; use nom::{ IResult, Parser, branch::alt, bytes::complete::tag_no_case, character::complete::{char, line_ending, space0}, combinator::{opt, peek, rest, value, verify}, multi::many0, sequence::{delimited, terminated}, }; use crate::make_parser; /// parse a single flag entry: indent + switch + optional param + description. make_parser!(entry -> OptionEntry<'_>, ( space0, (switch_parser, opt(param_parser)), description, ) => |(_, (switch, param), (first, cont)) : (_, (Switch<'a>, Option>), (&'a str, Vec<&'a str>))| { let mut desc: Vec<&str> = Vec::with_capacity(1 + cont.len()); if !first.trim().is_empty() { desc.push(first); } desc.extend(cont.into_iter().filter(|l| !l.trim().is_empty())); OptionEntry { switch, param, desc } } ); enum ParseResult<'a> { OptionEntry(OptionEntry<'a>), SectionHeader, Subcommand(Subcommand<'a>), NonOptionLine, } /// fallback: consume the current line + line_ending unconditionally. used /// after entry / section_header / subcommand_entry have all failed. make_parser!(skip_non_option_line -> (), value((), terminated(rest_of_line, line_ending))); make_parser!(is_arg_section -> (), value((), alt(( tag_no_case("positional arguments"), tag_no_case("arguments"), tag_no_case("positionals"), tag_no_case("args"), )), ) ); make_parser!(section_header -> (), value((), delimited( verify(space0, |ss: &str| get_indent(ss).1 <= 4), is_arg_section, (char(':'), rest_of_line, eol) ) ) ); /// dedup raw subcommands by case-insensitive name, keeping the entry with /// the longest description. preserves first-seen ordering. fn dedup_subcommands<'a>(raw: Vec>) -> Vec> { let mut by_name: HashMap> = HashMap::new(); let mut order: Vec = Vec::new(); for sc in raw { let key = sc.name.to_ascii_lowercase(); match by_name.get(&key) { Some(prev) if prev.desc.len() >= sc.desc.len() => {} _ => { if !by_name.contains_key(&key) { order.push(key.clone()); } by_name.insert(key, sc); } } } order.into_iter().map(|k| by_name.remove(&k).unwrap()).collect() } /// build the final HelpResult from the parse outputs + a copy of the /// original input (for whole-input positional extraction). fn build_help_result<'a>(original: &'a str, results: Vec>) -> HelpResult<'a> { let mut entries = Vec::new(); let mut raw_subcommands: Vec> = Vec::new(); for res in results { use ParseResult::*; // TODO: track in_arg_sec to filter subcommands under positional sections match res { OptionEntry(e) => entries.push(e), Subcommand(e) => raw_subcommands.push(e), _ => (), } } let subcommands = dedup_subcommands(raw_subcommands); // cli11 positional section takes priority over the usage-line scan // when both are present — cli11 carries types and optionality. let positionals = match extract_cli11_positionals(original) { Ok((_, p)) if !p.is_empty() => p, _ => extract_usage_positionals(original).map(|(_, p)| p).unwrap_or_default(), }; HelpResult { entries, subcommands, positionals, desc: "" } } /// top-level help parser. `peek(rest)` captures the original input slice /// so build_help_result can run the positional extractors over the whole /// thing while many0 still parses from the same position. make_parser!(pub help_parser -> HelpResult<'a>, ( peek(rest), many0(alt(( entry.map(ParseResult::OptionEntry), section_header.map(|_| ParseResult::SectionHeader), subcommand_entry.map(ParseResult::Subcommand), skip_non_option_line.map(|_| ParseResult::NonOptionLine), ))), ) => |(original, results): (&'a str, Vec>)| build_help_result(original, results) );