use std::path::Path;
use super::{ExtensionDef, ExtensionRegistry, MatchSpec, MatchToken};
#[derive(Debug, Clone)]
pub enum ExtensionMatch<'a> {
Suffix {
ext: &'a ExtensionDef,
path: &'a str,
},
Prefix {
ext: &'a ExtensionDef,
args_after_prefix: &'a [String],
},
PrefixSuffix {
ext: &'a ExtensionDef,
path: &'a str,
args_after_prefix: &'a [String],
},
}
impl<'a> ExtensionMatch<'a> {
#[must_use]
pub fn extension(&self) -> &'a ExtensionDef {
match self {
Self::Suffix { ext, .. }
| Self::Prefix { ext, .. }
| Self::PrefixSuffix { ext, .. } => ext,
}
}
#[must_use]
pub fn path(&self) -> Option<&'a str> {
match self {
Self::Suffix { path, .. } | Self::PrefixSuffix { path, .. } => Some(*path),
Self::Prefix { .. } => None,
}
}
#[must_use]
pub fn args_after_prefix(&self) -> &'a [String] {
match self {
Self::Prefix {
args_after_prefix, ..
}
| Self::PrefixSuffix {
args_after_prefix, ..
} => args_after_prefix,
Self::Suffix { .. } => &[],
}
}
}
impl ExtensionDef {
pub(super) fn match_spec_argv<'a>(&'a self, argv: &'a [String]) -> Option<ExtensionMatch<'a>> {
let spec = &self.match_spec;
if let Some(prefix) = &spec.argv_prefix {
if argv.len() < prefix.len()
|| !prefix
.iter()
.zip(argv.iter())
.all(|(expected, got)| expected.as_str() == got)
{
return None;
}
let rest = &argv[prefix.len()..];
if let Some(suffix) = &spec.arg_suffix {
let path = rest
.iter()
.find(|token| ends_with_suffix(token, suffix.as_str()))?;
return Some(ExtensionMatch::PrefixSuffix {
ext: self,
path: path.as_str(),
args_after_prefix: rest,
});
}
return Some(ExtensionMatch::Prefix {
ext: self,
args_after_prefix: rest,
});
}
if argv.len() != 1 {
return None;
}
let token = argv[0].as_str();
if let Some(filename) = &spec.filename {
let base = Path::new(token).file_name()?.to_str()?;
if base == filename.as_str() {
return Some(ExtensionMatch::Suffix {
ext: self,
path: token,
});
}
return None;
}
if let Some(suffix) = &spec.positional_suffix {
if ends_with_suffix(token, suffix.as_str()) {
return Some(ExtensionMatch::Suffix {
ext: self,
path: token,
});
}
}
None
}
}
pub(crate) fn ends_with_suffix(token: &str, suffix: &str) -> bool {
let Some(start) = token.len().checked_sub(suffix.len()) else {
return false;
};
token
.get(start..)
.is_some_and(|tail| tail.eq_ignore_ascii_case(suffix))
}
#[must_use]
pub fn match_extension_help<'a>(
registry: &'a ExtensionRegistry,
argv: &'a [String],
) -> Option<&'a ExtensionDef> {
if let Some(ext) = match_prefix_help(registry, argv) {
return Some(ext);
}
match_suffix_help(registry, argv)
}
fn match_prefix_help<'a>(
registry: &'a ExtensionRegistry,
argv: &'a [String],
) -> Option<&'a ExtensionDef> {
let mut best: Option<(&'a ExtensionDef, usize)> = None;
for ext in registry.extensions() {
let Some(prefix) = &ext.match_spec.argv_prefix else {
continue;
};
if prefix.is_empty() || argv.len() < prefix.len() {
continue;
}
if !prefix
.iter()
.zip(argv.iter())
.all(|(expected, got)| expected.as_str() == got)
{
continue;
}
if !is_help_only_tokens(&argv[prefix.len()..]) {
continue;
}
let len = prefix.len();
if best.is_none_or(|(_, best_len)| len > best_len) {
best = Some((ext, len));
}
}
best.map(|(ext, _)| ext)
}
fn match_suffix_help<'a>(
registry: &'a ExtensionRegistry,
argv: &'a [String],
) -> Option<&'a ExtensionDef> {
if argv.len() < 2 || !is_help_only_tokens(&argv[1..]) {
return None;
}
let path = argv[0].as_str();
registry
.extensions()
.iter()
.find(|ext| ext.match_spec.argv_prefix.is_none() && matches_suffix_or_filename(ext, path))
}
fn matches_suffix_or_filename(ext: &ExtensionDef, token: &str) -> bool {
let spec = &ext.match_spec;
if let Some(filename) = &spec.filename {
return Path::new(token)
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|base| base == filename.as_str());
}
spec.positional_suffix
.as_ref()
.is_some_and(|suffix| ends_with_suffix(token, suffix.as_str()))
}
#[must_use]
pub fn is_help_only_tokens(tokens: &[String]) -> bool {
!tokens.is_empty()
&& tokens
.iter()
.all(|token| token == "--help" || token == "-h")
}
#[must_use]
pub fn match_kind_summary(spec: &MatchSpec) -> String {
if let Some(prefix) = &spec.argv_prefix {
let prefix_s = prefix
.iter()
.map(MatchToken::as_str)
.collect::<Vec<_>>()
.join(" ");
if let Some(suffix) = &spec.arg_suffix {
return format!("prefix+suffix: {prefix_s} {suffix}");
}
return format!("prefix: {prefix_s}");
}
if let Some(filename) = &spec.filename {
return format!("filename: {filename}");
}
if let Some(suffix) = &spec.positional_suffix {
return format!("suffix: {suffix}");
}
"match: (none)".to_string()
}