asimov_cli/
subcommands_provider.rs1use std::path::{Path, PathBuf};
4
5#[derive(Debug, PartialEq, Eq, Clone)]
6pub struct Subcommand {
7 pub name: String,
8 pub path: PathBuf,
9}
10
11#[derive(Debug, Clone)]
12pub struct SubcommandsProvider {
13 commands: Vec<Subcommand>,
14}
15
16impl SubcommandsProvider {
17 pub fn collect(prefix: &str, level: usize) -> SubcommandsProvider {
18 let commands = Self::collect_commands(prefix)
19 .into_iter()
20 .flat_map(|path| {
22 let name = path
23 .file_stem()?
24 .to_string_lossy()
25 .trim_start_matches(prefix)
26 .to_string();
27
28 Some(Subcommand { name, path })
29 })
30 .filter(|cmd| {
32 let count = cmd.name.chars().filter(|&c| c == '-').count();
33 count < level
34 })
35 .collect();
36
37 SubcommandsProvider { commands }
38 }
39
40 pub fn find(prefix: &str, name: &str) -> Option<Subcommand> {
41 let name = format!("{}{}", prefix, name);
42 let path = Self::resolve_command(prefix, &name);
43 path.map(|path| Subcommand { name, path })
44 }
45}
46
47impl SubcommandsProvider {
48 pub fn iter(&self) -> impl Iterator<Item = &Subcommand> {
49 self.commands.iter()
50 }
51}
52
53#[cfg(unix)]
54impl SubcommandsProvider {
55 fn filter_file(prefix: &str, path: &Path) -> bool {
56 use std::os::unix::prelude::*;
57
58 let file_name = path.file_name();
59 let Some(entry_name) = file_name.and_then(|name| name.to_str()) else {
60 return false;
62 };
63
64 if entry_name.starts_with(".") || entry_name.ends_with("~") {
65 return false;
67 }
68
69 if !entry_name.starts_with(prefix) {
70 return false;
72 }
73
74 let Ok(metadata) = std::fs::metadata(path) else {
75 return false;
77 };
78
79 if !metadata.is_file() || metadata.permissions().mode() & 0o111 == 0 {
80 return false;
82 }
83
84 true
85 }
86
87 fn collect_commands(prefix: &str) -> Vec<PathBuf> {
88 let Some(paths) = std::env::var_os("PATH") else {
89 return vec![];
91 };
92
93 let mut result = vec![];
94 for path in std::env::split_paths(&paths) {
95 let Ok(dir) = std::fs::read_dir(path) else {
96 continue;
97 };
98
99 for entry in dir {
100 let Ok(entry) = entry else {
101 continue;
103 };
104
105 let path = entry.path();
106 if Self::filter_file(prefix, &path) {
107 result.push(path);
108 }
109 }
110 }
111
112 result
113 }
114
115 fn resolve_command(prefix: &str, command: &str) -> Option<PathBuf> {
116 let Some(paths) = std::env::var_os("PATH") else {
117 return None;
119 };
120
121 for path in std::env::split_paths(&paths) {
122 let path = path.join(command);
123
124 if !path.exists() {
125 continue;
126 }
127
128 if !Self::filter_file(prefix, &path) {
129 continue;
130 }
131
132 return Some(path);
133 }
134
135 None
136 }
137}
138
139#[cfg(windows)]
140impl SubcommandsProvider {
141 fn get_path_exts() -> Option<Vec<String>> {
142 let Ok(exts) = std::env::var("PATHEXT") else {
143 return None;
145 };
146
147 return Some(
150 exts.split(';')
151 .map(|ext| ext[1..].to_lowercase())
152 .collect::<Vec<_>>(),
153 );
154 }
155
156 fn filter_file(prefix: &str, path: &Path, exts: Option<&[String]>) -> bool {
157 use std::os::windows::fs::MetadataExt;
158 const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002;
159
160 let file_name = path.file_name();
161 let Some(entry_name) = file_name.and_then(|name| name.to_str()) else {
162 return false;
164 };
165
166 if !entry_name.starts_with(prefix) {
167 return false;
169 }
170
171 if let Some(exts) = exts {
172 let Some(entry_ext) = path.extension().and_then(|ext| ext.to_str()) else {
173 return false;
175 };
176
177 let entry_ext = entry_ext.to_lowercase();
178 if !exts.contains(&entry_ext) {
179 return false;
181 }
182 }
183
184 let Ok(metadata) = std::fs::metadata(path) else {
185 return false;
187 };
188
189 let is_hidden = metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0;
190 if is_hidden {
191 return false;
193 }
194
195 true
196 }
197
198 fn collect_commands(prefix: &str) -> Vec<PathBuf> {
199 let Some(paths) = std::env::var_os("PATH") else {
200 return vec![];
202 };
203
204 let Some(exts) = Self::get_path_exts() else {
205 return vec![];
207 };
208
209 let mut result = vec![];
210 for path in std::env::split_paths(&paths) {
211 let Ok(dir) = std::fs::read_dir(path) else {
212 continue;
213 };
214
215 for entry in dir {
216 let Ok(entry) = entry else {
217 continue;
219 };
220
221 let path = entry.path();
222 if Self::filter_file(prefix, &path, Some(&exts)) {
223 result.push(path);
224 }
225 }
226 }
227
228 result
229 }
230
231 fn resolve_command(prefix: &str, command: &str) -> Option<PathBuf> {
232 let Some(paths) = std::env::var_os("PATH") else {
233 return None;
235 };
236
237 let Some(exts) = Self::get_path_exts() else {
238 return None;
240 };
241
242 for path in std::env::split_paths(&paths) {
243 let mut path = path.join(command);
244
245 if path.extension().is_some() {
247 match path.exists() {
248 true if Self::filter_file(prefix, &path, None) => return Some(path),
249 _ => continue,
250 }
251 }
252
253 for ext in &exts {
255 path.set_extension(ext);
256
257 match path.exists() {
258 true if Self::filter_file(prefix, &path, None) => return Some(path),
259 _ => continue,
260 }
261 }
262 }
263
264 None
265 }
266}