Skip to content

Commit

Permalink
feat: Explicitly give subcommands
Browse files Browse the repository at this point in the history
  • Loading branch information
ysthakur committed Aug 11, 2023
1 parent 829b5ad commit 7146648
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 18 deletions.
38 changes: 28 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ struct Cli {
#[arg(short, long)]
out: PathBuf,

/// Shell(s) to generate completions for
#[arg(short, long, value_delimiter = ',', required = true)]
shells: Vec<Shell>,

/// Directories to exclude from search
#[arg(short = 'i', long = "ignore", value_delimiter = ',')]
dirs_exclude: Option<Vec<PathBuf>>,

/// Manpage sections to exclude (1-8)
#[arg(short = 'S', long, value_parser = section_num_parser, value_delimiter = ',')]
sections_exclude: Vec<u8>,

/// Particular commands to generate completions for. If omitted, generates
/// completions for all found commands.
#[arg(short, long)]
Expand All @@ -57,12 +57,30 @@ struct Cli {

/// Commands that should not be treated as subcommands. This is to help deal
/// with false positives when detecting subcommands.
#[arg(short, long, value_delimiter = ',')]
#[arg(long, value_delimiter = ',')]
not_subcmds: Vec<String>,

/// Shell(s) to generate completions for
#[arg(short, long, value_delimiter = ',', required = true)]
shells: Vec<Shell>,
/// Explicitly tell man-completions which man pages are for which
/// subcommands, in case it can't detect them. e.g. `git-commit=git
/// commit,foobar=foo bar`.
#[arg(long, value_delimiter = ',', value_parser=subcmd_map_parser)]
subcmds: Vec<(String, Vec<String>)>,

/// Manpage sections to exclude (1-8)
#[arg(short = 'S', long, value_parser = section_num_parser, value_delimiter = ',')]
sections_exclude: Vec<u8>,
}

fn subcmd_map_parser(
s: &str,
) -> core::result::Result<(String, Vec<String>), String> {
let Some((page_name, as_subcmd)) = s.split_once("=") else {
return Err(String::from(
"subcommand mapping should be in the form 'manpage-name=sub command'",
));
};
let as_subcmd = as_subcmd.split(" ").into_iter().map(String::from).collect();
Ok((String::from(page_name), as_subcmd))
}

fn section_num_parser(s: &str) -> core::result::Result<u8, String> {
Expand Down Expand Up @@ -107,11 +125,11 @@ fn main() -> Result<()> {
let mut cfg = ManParseConfig::new()
.exclude_dirs(args.dirs_exclude.unwrap_or_default())
.exclude_sections(args.sections_exclude)
.not_subcommands(args.not_subcmds);
.not_subcommands(args.not_subcmds)
.subcommands(args.subcmds);
if let Some(exclude_cmds) = args.exclude_cmds {
cfg = cfg.exclude_commands(exclude_cmds);
}

if let Some(cmds) = args.cmds {
cfg = cfg.restrict_to_commands(cmds);
}
Expand Down
41 changes: 33 additions & 8 deletions src/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub struct ManParseConfig {
include_commands: Option<Regex>,
exclude_commands: Option<Regex>,
not_subcommands: Vec<String>,
subcommand_map: HashMap<String, Vec<String>>,
}

impl ManParseConfig {
Expand All @@ -64,6 +65,7 @@ impl ManParseConfig {
include_commands: None,
exclude_commands: None,
not_subcommands: Vec::new(),
subcommand_map: HashMap::new(),
}
}

Expand Down Expand Up @@ -145,6 +147,19 @@ impl ManParseConfig {
self
}

/// Explicitly add some subcommand mappings. The keys are the man page file
/// name stems, and the values are the pieces of the subcommand, e.g.
/// `("git-commit", vec!["git", "commit"])`
pub fn subcommands<I>(mut self, subcmds: I) -> Self
where
I: IntoIterator<Item = (String, Vec<String>)>,
{
for (page_name, as_subcmd) in subcmds {
self.subcommand_map.insert(page_name, as_subcmd);
}
self
}

/// Actually do the parsing
pub fn parse(self) -> anyhow::Result<HashMap<String, CommandInfo>> {
let manpath = self.manpath.or_else(get_manpath).ok_or(anyhow!(
Expand Down Expand Up @@ -174,7 +189,7 @@ impl ManParseConfig {
&self.not_subcommands,
)?;

let parsed = parse_all_manpages(filtered);
let parsed = parse_all_manpages(filtered, self.subcommand_map);

Ok(parsed)
}
Expand All @@ -200,18 +215,24 @@ fn insert_cmd(
}
}

fn parse_all_manpages(manpages: Vec<(String, PathBuf)>) -> HashMap<String, CommandInfo> {
fn parse_all_manpages(
manpages: Vec<(String, PathBuf)>,
mut explicit_subcmds: HashMap<String, Vec<String>>,
) -> HashMap<String, CommandInfo> {
let mut res = HashMap::new();

for (cmd, manpage) in manpages {
if let Ok(text) = read_manpage(&manpage) {
let cmd_name = get_cmd_name(&manpage);
info!("Parsing man page for {} at {}", cmd_name, manpage.display());
match parse_manpage_text(&text) {
Some(parsed) => match detect_subcommand(&cmd_name, &text) {
Some(cmd_parts) => insert_cmd(&mut res, cmd_parts, parsed),
None => insert_cmd(&mut res, vec![cmd_name], parsed),
},
Some(parsed) => {
let as_subcmd = explicit_subcmds.remove(&cmd_name);
match as_subcmd.or_else(|| detect_subcommand(&cmd_name, &text)) {
Some(cmd_parts) => insert_cmd(&mut res, cmd_parts, parsed),
None => insert_cmd(&mut res, vec![cmd_name], parsed),
}
}
None => {
error!("Could not parse manpage for {}", cmd_name);
}
Expand Down Expand Up @@ -246,7 +267,8 @@ fn filter_pages(
} else {
// If it's a subcommand, then it might only match the start and
// have a hyphen after
mat.end() == cmd.len() || cmd.chars().nth(mat.end()).unwrap() == '-'
mat.end() == cmd.len()
|| cmd.chars().nth(mat.end()).unwrap() == '-'
}
}
_ => false,
Expand Down Expand Up @@ -348,7 +370,10 @@ fn detect_subcommand(cmd_name: &str, text: &str) -> Option<Vec<String>> {
}
}

fn all_possible_subcommands<'a>(hyphens: &[usize], cmd: &'a str) -> Vec<Vec<&'a str>> {
fn all_possible_subcommands<'a>(
hyphens: &[usize],
cmd: &'a str,
) -> Vec<Vec<&'a str>> {
if hyphens.len() == 2 {
Vec::new()
} else {
Expand Down

0 comments on commit 7146648

Please sign in to comment.