use crate::help::{Command, Item};
fn reached(root: &'static Command, path: &[&str]) -> (&'static Command, Vec<&'static Item>) {
let (mut command, mut globals) = (root, Vec::new());
for name in path {
let Some(sub) = command.subcommands.iter().find(|sub| sub.name == *name) else {
break;
};
globals.extend(command.items.iter().filter(|item| item.global));
command = sub.command;
}
(command, globals)
}
pub(super) fn inherited(
root: &'static Command,
path: &[&str],
command: &Command,
) -> Vec<&'static Item> {
let same = |a: &Item, b: &Item| match (a.long, b.long) {
(Some(a), Some(b)) => a == b,
(None, None) => a.short.is_some() && a.short == b.short,
_ => false,
};
let own = |item: &Item| {
let mut flags = command.items.iter().filter(|own| !own.positional);
flags.any(|own| same(own, item))
};
let (_, globals) = reached(root, path);
let mut shown: Vec<&'static Item> = Vec::new();
for item in globals.into_iter().rev() {
if !item.hide && !own(item) && !shown.iter().any(|seen| same(seen, item)) {
shown.push(item);
}
}
shown.reverse();
shown
}
pub(super) fn flag(root: &'static Command, path: &[&str], typed: &str) -> Option<String> {
let typed = typed.strip_prefix("--")?;
let typed = typed.split_once('=').map_or(typed, |(name, _)| name);
let (command, globals) = reached(root, path);
let own = command.items.iter().filter(|item| !item.positional);
let names = own
.chain(globals)
.filter(|item| !item.hide)
.flat_map(|item| [item.long, item.negate])
.flatten()
.chain(command.help_flag.then_some("help"))
.chain(command.version.map(|_| "version"));
nearest(typed, names).map(|name| format!("--{name}"))
}
pub(super) fn subcommand(
root: &'static Command,
path: &[&str],
typed: &str,
) -> Option<&'static str> {
let (command, _) = reached(root, path);
let shown = command.subcommands.iter().filter(|sub| !sub.hide);
nearest(
typed,
shown.flat_map(|sub| [sub.name].into_iter().chain(sub.aliases.iter().copied())),
)
}
pub(super) fn nearest<'a>(typed: &str, names: impl Iterator<Item = &'a str>) -> Option<&'a str> {
let mut best = (0.7, None);
for name in names {
let likeness = jaro(typed, name);
if likeness > best.0 {
best = (likeness, Some(name));
}
}
best.1
}
fn jaro(a: &str, b: &str) -> f64 {
let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
if a.is_empty() || b.is_empty() {
return f64::from(u8::from(a == b));
}
let reach = (a.len().max(b.len()) / 2).saturating_sub(1);
let mut taken = vec![false; b.len()];
let mut matched = Vec::new();
for (at, letter) in a.iter().enumerate() {
let from = at.saturating_sub(reach);
let to = (at + reach + 1).min(b.len());
if let Some(found) = (from..to).find(|&i| !taken[i] && b[i] == *letter) {
taken[found] = true;
matched.push(*letter);
}
}
if matched.is_empty() {
return 0.0;
}
let theirs = b.iter().zip(&taken).filter(|(_, taken)| **taken);
let moved = matched
.iter()
.zip(theirs)
.filter(|(a, (b, _))| a != b)
.count();
let matches = matched.len() as f64;
(matches / a.len() as f64 + matches / b.len() as f64 + (matches - moved as f64 / 2.0) / matches)
/ 3.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_closest_name_is_found_when_one_is_close() {
let names = ["force", "format", "verbose"];
assert_eq!(nearest("fore", names.into_iter()), Some("force"));
assert_eq!(nearest("verbos", names.into_iter()), Some("verbose"));
assert_eq!(nearest("fromat", names.into_iter()), Some("format"));
assert_eq!(nearest("quiet", names.into_iter()), None);
assert_eq!(nearest("", names.into_iter()), None);
assert!((jaro("martha", "marhta") - 0.9444).abs() < 0.0001);
assert!((jaro("dixon", "dicksonx") - 0.7667).abs() < 0.0001);
assert_eq!(jaro("same", "same"), 1.0);
assert_eq!(jaro("abc", "xyz"), 0.0);
}
}