riir
This commit is contained in:
parent
da4bc139eb
commit
a87b5eb3bd
44 changed files with 7087 additions and 5426 deletions
144
src/parsers/help.rs
Normal file
144
src/parsers/help.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
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<Param<'a>>), (&'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<Subcommand<'a>>) -> Vec<Subcommand<'a>> {
|
||||
let mut by_name: HashMap<String, Subcommand<'a>> = HashMap::new();
|
||||
let mut order: Vec<String> = 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<ParseResult<'a>>) -> HelpResult<'a> {
|
||||
let mut entries = Vec::new();
|
||||
let mut raw_subcommands: Vec<Subcommand<'a>> = 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<ParseResult<'a>>)|
|
||||
build_help_result(original, results)
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue