1use std::{
2 fs::Permissions,
3 os::unix::fs::PermissionsExt,
4 path::{Path, PathBuf},
5};
6
7#[derive(Debug, thiserror::Error)]
9pub enum FindScriptError {
10 #[error("Multiple({1}) scripts found with name '{0}'")]
12 MultipleFound(String, usize),
13 #[error(transparent)]
15 ListScriptsError(#[from] ListScriptsError),
16}
17
18#[derive(Debug, thiserror::Error)]
20pub enum ListScriptsError {
21 #[error("Path '{0:?}' is not a directory")]
23 NotADir(PathBuf),
24 #[error("Error reading directory: {0}")]
26 ReadDirFailure(std::io::Error),
27}
28
29pub fn find_script(root: &Path, name: String) -> Result<Option<PathBuf>, FindScriptError> {
39 let all_scripts = list_scripts(root)?;
40 let matching_scripts = all_scripts
41 .into_iter()
42 .filter(|(n, _)| *n == name)
43 .collect::<Vec<_>>();
44 match matching_scripts.as_slice() {
45 [(_, path)] => Ok(Some(path.to_path_buf())),
46 [] => Ok(None),
47 multiple => Err(FindScriptError::MultipleFound(name, multiple.len())),
48 }
49}
50
51pub fn list_scripts(root: &Path) -> Result<Vec<(String, PathBuf)>, ListScriptsError> {
68 let mut found_scripts = Vec::new();
69 let sub_dirs = root
70 .read_dir()
71 .map_err(ListScriptsError::ReadDirFailure)?
72 .filter_map(|f| f.ok())
73 .filter(|f| f.path().is_dir())
74 .collect::<Vec<_>>();
75 for sub_dir in sub_dirs {
76 let sub_scripts = list_scripts(&sub_dir.path())?;
77 found_scripts.extend(sub_scripts);
78 }
79 found_scripts.extend(list_scripts_at_directory(root)?);
80
81 Ok(found_scripts)
82}
83
84fn list_scripts_at_directory(dir: &Path) -> Result<Vec<(String, PathBuf)>, ListScriptsError> {
88 if !dir.is_dir() {
89 return Err(ListScriptsError::NotADir(dir.to_path_buf()));
90 }
91 let scripts = dir
92 .read_dir()
93 .map_err(ListScriptsError::ReadDirFailure)?
94 .filter_map(|f| f.ok())
95 .filter(|f| f.path().is_file())
96 .filter(|f| {
97 let Ok(m) = f.metadata() else {
98 return false;
99 };
100 is_path_executable(m.permissions())
101 })
102 .filter(|f| {
103 let Some(e) = f
104 .path()
105 .extension()
106 .map(|e| e.to_string_lossy().to_string())
107 else {
108 return false;
109 };
110 e == super::BASH_EXTENSION
111 })
112 .filter_map(|f| {
113 let name = f.path().file_stem()?.to_string_lossy().to_string();
114 Some((name, f.path()))
115 })
116 .collect::<Vec<_>>();
117
118 Ok(scripts)
119}
120
121pub(crate) fn is_path_executable(permissions: Permissions) -> bool {
123 permissions.mode() & 0o111 != 0
124}