Skip to main content

aster/config/
search_path.rs

1use std::{
2    env::{self},
3    ffi::{OsStr, OsString},
4    path::PathBuf,
5};
6
7use anyhow::{Context, Result};
8
9use crate::config::Config;
10
11pub struct SearchPaths {
12    paths: Vec<PathBuf>,
13}
14
15impl SearchPaths {
16    pub fn builder() -> Self {
17        let mut paths = Config::global()
18            .get_aster_search_paths()
19            .unwrap_or_default();
20
21        paths.push("~/.local/bin".into());
22
23        #[cfg(unix)]
24        {
25            paths.push("/usr/local/bin".into());
26        }
27
28        if cfg!(target_os = "macos") {
29            paths.push("/opt/homebrew/bin".into());
30            paths.push("/opt/local/bin".into());
31        }
32
33        Self {
34            paths: paths
35                .into_iter()
36                .map(|s| PathBuf::from(shellexpand::tilde(&s).as_ref()))
37                .collect(),
38        }
39    }
40
41    pub fn with_npm(mut self) -> Self {
42        if cfg!(windows) {
43            if let Some(appdata) = dirs::data_dir() {
44                self.paths.push(appdata.join("npm"));
45            }
46        } else if let Some(home) = dirs::home_dir() {
47            self.paths.push(home.join(".npm-global/bin"));
48        }
49        self
50    }
51
52    pub fn path(self) -> Result<OsString> {
53        env::join_paths(
54            self.paths.into_iter().chain(
55                env::var_os("PATH")
56                    .as_ref()
57                    .map(env::split_paths)
58                    .into_iter()
59                    .flatten(),
60            ),
61        )
62        .map_err(Into::into)
63    }
64
65    pub fn resolve<N>(self, name: N) -> Result<PathBuf>
66    where
67        N: AsRef<OsStr>,
68    {
69        which::which_in_global(name.as_ref(), Some(self.path()?))?
70            .next()
71            .with_context(|| {
72                format!(
73                    "could not resolve command '{}': file does not exist",
74                    name.as_ref().to_string_lossy()
75                )
76            })
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_path_preserves_existing_path() {
86        let search_paths = SearchPaths::builder();
87        let combined_path = search_paths.path().unwrap();
88
89        if let Some(existing_path) = env::var_os("PATH") {
90            let combined_str = combined_path.to_string_lossy();
91            let existing_str = existing_path.to_string_lossy();
92
93            assert!(combined_str.contains(&existing_str.to_string()));
94        }
95    }
96
97    #[test]
98    fn test_resolve_nonexistent_executable() {
99        let search_paths = SearchPaths::builder();
100
101        let result = search_paths.resolve("nonexistent_executable_12345_abcdef");
102
103        assert!(
104            result.is_err(),
105            "Resolving nonexistent executable should return an error"
106        );
107    }
108
109    #[test]
110    fn test_resolve_common_executable() {
111        let search_paths = SearchPaths::builder();
112
113        #[cfg(unix)]
114        let test_executable = "sh";
115
116        #[cfg(windows)]
117        let test_executable = "cmd";
118
119        search_paths
120            .resolve(test_executable)
121            .expect("should resolve sh (or cmd on Windows)");
122    }
123}