-
Notifications
You must be signed in to change notification settings - Fork 1
Implement SubcommandsProvider
#2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
artob
merged 2 commits into
dryrust:master
from
imunproductive:feat/subcommands_provider
Apr 12, 2025
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| // This is free and unencumbered software released into the public domain. | ||
|
|
||
| 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() | ||
| // Construct ExternalCommand. | ||
| .flat_map(|path| { | ||
| let name = path | ||
| .file_stem()? | ||
| .to_string_lossy() | ||
| .trim_start_matches(prefix) | ||
| .to_string(); | ||
|
|
||
| Some(Subcommand { name, path }) | ||
| }) | ||
| // Respect level. | ||
| .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() | ||
| } | ||
| } | ||
|
|
||
| #[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 { | ||
| // skip files with invalid names. | ||
| return false; | ||
| }; | ||
|
|
||
| if entry_name.starts_with(".") || entry_name.ends_with("~") { | ||
| // skip hidden and backup files. | ||
| return false; | ||
| } | ||
|
|
||
| if !entry_name.starts_with(prefix) { | ||
| // skip non-matching files. | ||
| return false; | ||
| } | ||
|
|
||
| let Ok(metadata) = std::fs::metadata(path) else { | ||
| // couldn't get metadata. | ||
| return false; | ||
| }; | ||
|
|
||
| if !metadata.is_file() || metadata.permissions().mode() & 0o111 == 0 { | ||
| // skip non-executable files. | ||
| return false; | ||
| } | ||
|
|
||
| true | ||
| } | ||
|
|
||
| fn collect_commands(prefix: &str) -> Vec<PathBuf> { | ||
| let Some(paths) = std::env::var_os("PATH") else { | ||
| // PATH variable is not set. | ||
| 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 { | ||
| // invalid entry. | ||
| 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 { | ||
| // PATH variable is not set. | ||
| 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 { | ||
| // PATHEXT variable is not set. | ||
| return None; | ||
| }; | ||
|
|
||
| // NOTE: I am not sure if std::env::split_paths should be applied here, | ||
| // since it also deals with '"' which seems to not be used in PATHEXT? | ||
| 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 { | ||
| // skip files with invalid names. | ||
| return false; | ||
| }; | ||
|
|
||
| if !entry_name.starts_with(prefix) { | ||
| // skip non-matching files. | ||
| return false; | ||
| } | ||
|
|
||
| if let Some(exts) = exts { | ||
| let Some(entry_ext) = path.extension().and_then(|ext| ext.to_str()) else { | ||
| // skip files without extensions. | ||
| return false; | ||
| }; | ||
|
|
||
| let entry_ext = entry_ext.to_lowercase(); | ||
| if !exts.contains(&entry_ext) { | ||
| // skip non-executable files | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| let Ok(metadata) = std::fs::metadata(path) else { | ||
| // couldn't get metadata. | ||
| return false; | ||
| }; | ||
|
|
||
| let is_hidden = metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0; | ||
| if is_hidden { | ||
| // skip hidden files | ||
| return false; | ||
| } | ||
|
|
||
| true | ||
| } | ||
|
|
||
| fn collect_commands(prefix: &str) -> Vec<PathBuf> { | ||
| let Some(paths) = std::env::var_os("PATH") else { | ||
| // PATH variable is not set. | ||
| return vec![]; | ||
| }; | ||
|
|
||
| let Some(exts) = Self::get_path_exts() else { | ||
| // PATHEXT variable is not set or invalid. | ||
| 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 { | ||
| // invalid entry. | ||
| 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 { | ||
| // PATH variable is not set. | ||
| return None; | ||
| }; | ||
|
|
||
| let Some(exts) = Self::get_path_exts() else { | ||
| // PATHEXT variable is not set or invalid. | ||
| return None; | ||
| }; | ||
|
|
||
| for path in std::env::split_paths(&paths) { | ||
| let mut path = path.join(command); | ||
|
|
||
| // Extension is provided. Just check if file exists. | ||
| if path.extension().is_some() { | ||
| match path.exists() { | ||
| true if Self::filter_file(prefix, &path, None) => return Some(path), | ||
| _ => continue, | ||
| } | ||
| } | ||
|
|
||
| // Iterate extensions and check if file exists. | ||
| for ext in &exts { | ||
| path.set_extension(ext); | ||
|
|
||
| match path.exists() { | ||
| true if Self::filter_file(prefix, &path, None) => return Some(path), | ||
| _ => continue, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
| } |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // This is free and unencumbered software released into the public domain. | ||
|
|
||
| use clientele::SubcommandsProvider; | ||
|
|
||
| mod subcommands_shared; | ||
| use subcommands_shared::{Result, TEST_FILES, TEST_PREFIX}; | ||
|
|
||
| #[test] | ||
| pub fn test_find() -> Result<()> { | ||
| let dir = subcommands_shared::init()?; | ||
|
|
||
| for file in TEST_FILES { | ||
| println!("{}: ", file.name); | ||
|
|
||
| let cd_name = file.name.trim_start_matches(TEST_PREFIX); | ||
| let cmd = SubcommandsProvider::find(TEST_PREFIX, cd_name); | ||
| let path = dir.child(file.full_name()); | ||
|
|
||
| // assert_eq!(cmd.is_some(), file.should_be_listed); | ||
|
|
||
| if let Some(cmd) = cmd { | ||
| assert_eq!(cmd.path, path); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // This is free and unencumbered software released into the public domain. | ||
|
|
||
| use clientele::SubcommandsProvider; | ||
|
|
||
| mod subcommands_shared; | ||
| use subcommands_shared::{Result, TEST_FILES, TEST_LEVEL, TEST_PREFIX}; | ||
|
|
||
| #[test] | ||
| pub fn test_list() -> Result<()> { | ||
| let dir = subcommands_shared::init()?; | ||
| let cmds = SubcommandsProvider::collect(TEST_PREFIX, TEST_LEVEL); | ||
|
|
||
| for file in TEST_FILES { | ||
| println!("{}: ", file.name); | ||
|
|
||
| let cd_name = file.name.trim_start_matches(TEST_PREFIX); | ||
| let cmd = cmds.iter().find(|cmd| cmd.name == cd_name); | ||
| let path = dir.child(file.full_name()); | ||
|
|
||
| assert_eq!(cmd.is_some(), file.should_be_listed); | ||
|
|
||
| if let Some(cmd) = cmd { | ||
| assert_eq!(cmd.path, path); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alphabetic ordering in these, always