Skip to main content

brush_core/shell/
fs.rs

1//! Filesystem interaction in the shell.
2
3use std::path::{Path, PathBuf};
4
5use normalize_path::NormalizePath as _;
6
7use crate::{
8    ExecutionParameters, ShellFd,
9    env::{EnvironmentLookup, EnvironmentScope},
10    error, openfiles, pathsearch,
11    sys::{fs::PathExt as _, users},
12    variables,
13};
14
15impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
16    /// Sets the shell's current working directory to the given path.
17    ///
18    /// # Arguments
19    ///
20    /// * `target_dir` - The path to set as the working directory.
21    pub fn set_working_dir(&mut self, target_dir: impl AsRef<Path>) -> Result<(), error::Error> {
22        let abs_path = self.absolute_path(target_dir.as_ref());
23
24        match std::fs::metadata(&abs_path) {
25            Ok(m) => {
26                if !m.is_dir() {
27                    return Err(error::ErrorKind::NotADirectory(abs_path).into());
28                }
29            }
30            Err(e) => {
31                return Err(e.into());
32            }
33        }
34
35        // Normalize the path (but don't canonicalize it).
36        let cleaned_path = abs_path.normalize();
37
38        let pwd = cleaned_path.to_string_lossy().to_string();
39
40        self.env.update_or_add(
41            "PWD",
42            variables::ShellValueLiteral::Scalar(pwd),
43            |_| Ok(()),
44            EnvironmentLookup::Anywhere,
45            EnvironmentScope::Global,
46        )?;
47        let oldpwd = std::mem::replace(self.working_dir_mut(), cleaned_path);
48
49        self.env.update_or_add(
50            "OLDPWD",
51            variables::ShellValueLiteral::Scalar(oldpwd.to_string_lossy().to_string()),
52            |_| Ok(()),
53            EnvironmentLookup::Anywhere,
54            EnvironmentScope::Global,
55        )?;
56
57        Ok(())
58    }
59
60    /// Tilde-shortens the given string, replacing the user's home directory with a tilde.
61    ///
62    /// # Arguments
63    ///
64    /// * `s` - The string to shorten.
65    pub fn tilde_shorten(&self, s: String) -> String {
66        if let Some(home_dir) = self.home_dir()
67            && let Some(stripped) = s.strip_prefix(home_dir.to_string_lossy().as_ref())
68        {
69            return format!("~{stripped}");
70        }
71        s
72    }
73
74    /// Returns the shell's current home directory, if available.
75    pub(crate) fn home_dir(&self) -> Option<PathBuf> {
76        if let Some(home) = self.env.get_str("HOME", self) {
77            Some(PathBuf::from(home.to_string()))
78        } else {
79            // HOME isn't set, so let's sort it out ourselves.
80            users::get_current_user_home_dir()
81        }
82    }
83
84    /// Finds executables in the shell's current default PATH, matching the given glob pattern.
85    ///
86    /// # Arguments
87    ///
88    /// * `required_glob_pattern` - The glob pattern to match against.
89    pub fn find_executables_in_path<'a>(
90        &'a self,
91        filename: &'a str,
92    ) -> impl Iterator<Item = PathBuf> + 'a {
93        let path_var = self.env.get_str("PATH", self).unwrap_or_default();
94        let paths = crate::sys::fs::split_paths(path_var.as_ref());
95
96        pathsearch::search_for_executable(paths, filename)
97    }
98
99    /// Finds executables in the shell's current default PATH, with filenames matching the
100    /// given prefix.
101    ///
102    /// # Arguments
103    ///
104    /// * `filename_prefix` - The prefix to match against executable filenames.
105    pub fn find_executables_in_path_with_prefix(
106        &self,
107        filename_prefix: &str,
108        case_insensitive: bool,
109    ) -> impl Iterator<Item = PathBuf> {
110        let path_var = self.env.get_str("PATH", self).unwrap_or_default();
111        let paths = crate::sys::fs::split_paths(path_var.as_ref());
112
113        pathsearch::search_for_executable_with_prefix(paths, filename_prefix, case_insensitive)
114    }
115
116    /// Determines whether the given filename is the name of an executable in one of the
117    /// directories in the shell's current PATH. If found, returns the path.
118    ///
119    /// # Arguments
120    ///
121    /// * `candidate_name` - The name of the file to look for.
122    pub fn find_first_executable_in_path<S: AsRef<str>>(
123        &self,
124        candidate_name: S,
125    ) -> Option<PathBuf> {
126        let path = self.env_str("PATH").unwrap_or_default();
127        for one_dir in crate::sys::fs::split_paths(path.as_ref()) {
128            let candidate_path = one_dir.join(candidate_name.as_ref());
129            if candidate_path.executable() {
130                return Some(candidate_path);
131            }
132        }
133        None
134    }
135
136    /// Uses the shell's hash-based path cache to check whether the given filename is the name
137    /// of an executable in one of the directories in the shell's current PATH. If found,
138    /// ensures the path is in the cache and returns it.
139    ///
140    /// # Arguments
141    ///
142    /// * `candidate_name` - The name of the file to look for.
143    pub fn find_first_executable_in_path_using_cache<S: AsRef<str>>(
144        &mut self,
145        candidate_name: S,
146    ) -> Option<PathBuf>
147    where
148        String: From<S>,
149    {
150        if let Some(cached_path) = self.program_location_cache.get(&candidate_name) {
151            Some(cached_path)
152        } else if let Some(found_path) = self.find_first_executable_in_path(&candidate_name) {
153            self.program_location_cache
154                .set(candidate_name, found_path.clone());
155            Some(found_path)
156        } else {
157            None
158        }
159    }
160
161    /// Gets the absolute form of the given path.
162    ///
163    /// # Arguments
164    ///
165    /// * `path` - The path to get the absolute form of.
166    pub fn absolute_path(&self, path: impl AsRef<Path>) -> PathBuf {
167        let path = path.as_ref();
168        if path.as_os_str().is_empty() || path.is_absolute() {
169            path.to_owned()
170        } else {
171            self.working_dir().join(path)
172        }
173    }
174
175    /// Opens the given file, using the context of this shell and the provided execution parameters.
176    ///
177    /// # Arguments
178    ///
179    /// * `options` - The options to use opening the file.
180    /// * `path` - The path to the file to open; may be relative to the shell's working directory.
181    /// * `params` - Execution parameters.
182    pub(crate) fn open_file(
183        &self,
184        options: &std::fs::OpenOptions,
185        path: impl AsRef<Path>,
186        params: &ExecutionParameters,
187    ) -> Result<openfiles::OpenFile, std::io::Error> {
188        // Give platform-specific code a chance to handle special files
189        // (e.g. /dev/null on Windows, which needs to open NUL instead).
190        // This is checked before absolute_path so that paths like /dev/null
191        // are intercepted on platforms where they aren't valid native paths.
192        if let Some(result) = crate::sys::fs::try_open_special_file(path.as_ref()) {
193            return result.map(openfiles::OpenFile::from);
194        }
195
196        let path_to_open = self.absolute_path(path.as_ref());
197
198        // See if this is a reference to a file descriptor, in which case the actual
199        // /dev/fd* file path for this process may not match with what's in the execution
200        // parameters.
201        if let Some(parent) = path_to_open.parent()
202            && parent == Path::new("/dev/fd")
203            && let Some(filename) = path_to_open.file_name()
204            && let Ok(fd_num) = filename.to_string_lossy().to_string().parse::<ShellFd>()
205            && let Some(open_file) = params.try_fd(self, fd_num)
206        {
207            return open_file.try_clone();
208        }
209
210        Ok(options.open(path_to_open)?.into())
211    }
212
213    /// Replaces the shell's currently configured open files with the given set.
214    /// Typically only used by exec-like builtins.
215    ///
216    /// # Arguments
217    ///
218    /// * `open_files` - The new set of open files to use.
219    pub fn replace_open_files(
220        &mut self,
221        open_fds: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
222    ) {
223        self.open_files = openfiles::OpenFiles::from(open_fds);
224    }
225
226    pub(crate) const fn persistent_open_files(&self) -> &openfiles::OpenFiles {
227        &self.open_files
228    }
229}