use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Subcommand {
pub name: String,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct SubcommandsProvider {
commands: Vec<Subcommand>,
}
impl SubcommandsProvider {
pub fn collect(prefix: &str, level: usize) -> SubcommandsProvider {
let commands = Self::collect_commands(prefix)
.into_iter()
.flat_map(|path| {
let name = path
.file_stem()?
.to_string_lossy()
.trim_start_matches(prefix)
.to_string();
Some(Subcommand { name, path })
})
.filter(|cmd| {
let count = cmd.name.chars().filter(|&c| c == '-').count();
count < level
})
.collect();
SubcommandsProvider { commands }
}
pub fn find(prefix: &str, name: &str) -> Option<Subcommand> {
let name = format!("{}{}", prefix, name);
let path = Self::resolve_command(prefix, &name);
path.map(|path| Subcommand { name, path })
}
}
impl SubcommandsProvider {
pub fn iter(&self) -> impl Iterator<Item = &Subcommand> {
self.commands.iter()
}
pub fn into_iter(self) -> impl Iterator<Item = Subcommand> {
self.commands.into_iter()
}
pub fn get_commands(&self) -> &Vec<Subcommand> {
&self.commands
}
pub fn into_commands(self) -> Vec<Subcommand> {
self.commands
}
}
#[cfg(unix)]
impl SubcommandsProvider {
fn filter_file(prefix: &str, path: &Path) -> bool {
use std::os::unix::prelude::*;
let file_name = path.file_name();
let Some(entry_name) = file_name.and_then(|name| name.to_str()) else {
return false;
};
if entry_name.starts_with(".") || entry_name.ends_with("~") {
return false;
}
if !entry_name.starts_with(prefix) {
return false;
}
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
if !metadata.is_file() || metadata.permissions().mode() & 0o111 == 0 {
return false;
}
true
}
fn collect_commands(prefix: &str) -> Vec<PathBuf> {
let Some(paths) = std::env::var_os("PATH") else {
return vec![];
};
let mut result = vec![];
for path in std::env::split_paths(&paths) {
let Ok(dir) = std::fs::read_dir(path) else {
continue;
};
for entry in dir {
let Ok(entry) = entry else {
continue;
};
let path = entry.path();
if Self::filter_file(prefix, &path) {
result.push(path);
}
}
}
result
}
fn resolve_command(prefix: &str, command: &str) -> Option<PathBuf> {
let Some(paths) = std::env::var_os("PATH") else {
return None;
};
for path in std::env::split_paths(&paths) {
let path = path.join(command);
if !path.exists() {
continue;
}
if !Self::filter_file(prefix, &path) {
continue;
}
return Some(path);
}
None
}
}
#[cfg(windows)]
impl SubcommandsProvider {
fn get_path_exts() -> Option<Vec<String>> {
let Ok(exts) = std::env::var("PATHEXT") else {
return None;
};
return Some(
exts.split(';')
.map(|ext| ext[1..].to_lowercase())
.collect::<Vec<_>>(),
);
}
fn filter_file(prefix: &str, path: &Path, exts: Option<&[String]>) -> bool {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002;
let file_name = path.file_name();
let Some(entry_name) = file_name.and_then(|name| name.to_str()) else {
return false;
};
if !entry_name.starts_with(prefix) {
return false;
}
if let Some(exts) = exts {
let Some(entry_ext) = path.extension().and_then(|ext| ext.to_str()) else {
return false;
};
let entry_ext = entry_ext.to_lowercase();
if !exts.contains(&entry_ext) {
return false;
}
}
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
let is_hidden = metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0;
if is_hidden {
return false;
}
true
}
fn collect_commands(prefix: &str) -> Vec<PathBuf> {
let Some(paths) = std::env::var_os("PATH") else {
return vec![];
};
let Some(exts) = Self::get_path_exts() else {
return vec![];
};
let mut result = vec![];
for path in std::env::split_paths(&paths) {
let Ok(dir) = std::fs::read_dir(path) else {
continue;
};
for entry in dir {
let Ok(entry) = entry else {
continue;
};
let path = entry.path();
if Self::filter_file(prefix, &path, Some(&exts)) {
result.push(path);
}
}
}
result
}
fn resolve_command(prefix: &str, command: &str) -> Option<PathBuf> {
let Some(paths) = std::env::var_os("PATH") else {
return None;
};
let Some(exts) = Self::get_path_exts() else {
return None;
};
for path in std::env::split_paths(&paths) {
let mut path = path.join(command);
if path.extension().is_some() {
match path.exists() {
true if Self::filter_file(prefix, &path, None) => return Some(path),
_ => continue,
}
}
for ext in &exts {
path.set_extension(ext);
match path.exists() {
true if Self::filter_file(prefix, &path, None) => return Some(path),
_ => continue,
}
}
}
None
}
}