Skip to main content

shrub_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::env::{EnvironmentLookup, EnvironmentScope};
8use crate::sys::users;
9use crate::{ExecutionParameters, ShellFd, error, openfiles, pathsearch, variables};
10
11impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
12    /// Sets the shell's current working directory to the given path.
13    ///
14    /// # Arguments
15    ///
16    /// * `target_dir` - The path to set as the working directory.
17    ///
18    /// # Errors
19    ///
20    /// - [`crate::error::ErrorKind::NotADirectory`] if the path exists but is
21    ///   not a directory.
22    /// - [`crate::error::ErrorKind::IoError`] if the path's metadata cannot be
23    ///   read, for example because it does not exist.
24    /// - The error from updating `PWD` or `OLDPWD`, such as
25    ///   [`crate::error::ErrorKind::ReadonlyVariable`] when either is readonly.
26    pub fn set_working_dir(&mut self, target_dir: impl AsRef<Path>) -> Result<(), error::Error> {
27        let abs_path = self.absolute_path(target_dir.as_ref());
28
29        match std::fs::metadata(&abs_path) {
30            Ok(m) => {
31                if !m.is_dir() {
32                    return Err(error::ErrorKind::NotADirectory(abs_path).into());
33                }
34            }
35            Err(e) => {
36                return Err(e.into());
37            }
38        }
39
40        // Normalize the path (but don't canonicalize it).
41        let cleaned_path = abs_path.normalize();
42
43        let pwd = cleaned_path.to_string_lossy().to_string();
44
45        self.env.update_or_add(
46            "PWD",
47            variables::ShellValueLiteral::Scalar(pwd),
48            |_| Ok(()),
49            EnvironmentLookup::Anywhere,
50            EnvironmentScope::Global,
51        )?;
52        let oldpwd = std::mem::replace(self.working_dir_mut(), cleaned_path);
53
54        self.env.update_or_add(
55            "OLDPWD",
56            variables::ShellValueLiteral::Scalar(oldpwd.to_string_lossy().to_string()),
57            |_| Ok(()),
58            EnvironmentLookup::Anywhere,
59            EnvironmentScope::Global,
60        )?;
61
62        Ok(())
63    }
64
65    /// Tilde-shortens the given string, replacing the user's home directory
66    /// with a tilde.
67    ///
68    /// # Arguments
69    ///
70    /// * `s` - The string to shorten.
71    pub fn tilde_shorten(&self, s: String) -> String {
72        if let Some(home_dir) = self.home_dir()
73            && let Some(stripped) = s.strip_prefix(home_dir.to_string_lossy().as_ref())
74        {
75            return format!("~{stripped}");
76        }
77        s
78    }
79
80    /// Returns the shell's current home directory, if available.
81    pub(crate) fn home_dir(&self) -> Option<PathBuf> {
82        if let Some(home) = self.env.get_str("HOME", self) {
83            Some(PathBuf::from(home.to_string()))
84        } else {
85            // HOME isn't set, so let's sort it out ourselves.
86            users::get_current_user_home_dir()
87        }
88    }
89
90    /// Finds executables with the given name in the shell's current PATH,
91    /// yielding each match in search order.
92    ///
93    /// # Arguments
94    ///
95    /// * `filename` - The name of the executable to look for.
96    pub fn find_executables_in_path<'a>(
97        &'a self,
98        filename: &'a str,
99    ) -> impl Iterator<Item = PathBuf> + 'a {
100        let path_var = self.env.get_str("PATH", self).unwrap_or_default();
101        let paths = crate::sys::fs::split_paths(path_var.as_ref());
102
103        pathsearch::search_for_executable(paths, filename)
104    }
105
106    /// Finds executables in the shell's current default PATH, with filenames
107    /// matching the given prefix.
108    ///
109    /// # Arguments
110    ///
111    /// * `filename_prefix` - The prefix to match against executable filenames.
112    pub fn find_executables_in_path_with_prefix(
113        &self,
114        filename_prefix: &str,
115        case_insensitive: bool,
116    ) -> impl Iterator<Item = PathBuf> {
117        let path_var = self.env.get_str("PATH", self).unwrap_or_default();
118        let paths = crate::sys::fs::split_paths(path_var.as_ref());
119
120        pathsearch::search_for_executable_with_prefix(paths, filename_prefix, case_insensitive)
121    }
122
123    /// Determines whether the given filename is the name of an executable in
124    /// one of the directories in the shell's current PATH. If found,
125    /// returns the path.
126    ///
127    /// # Arguments
128    ///
129    /// * `candidate_name` - The name of the file to look for.
130    pub fn find_first_executable_in_path<S: AsRef<str>>(
131        &self,
132        candidate_name: S,
133    ) -> Option<PathBuf> {
134        self.find_executables_in_path(candidate_name.as_ref())
135            .next()
136    }
137
138    /// Uses the shell's hash-based path cache to check whether the given
139    /// filename is the name of an executable in one of the directories in
140    /// the shell's current PATH. If found, ensures the path is in the cache
141    /// and returns it.
142    ///
143    /// # Arguments
144    ///
145    /// * `candidate_name` - The name of the file to look for.
146    pub fn find_first_executable_in_path_using_cache<S: AsRef<str>>(
147        &mut self,
148        candidate_name: S,
149    ) -> Option<PathBuf>
150    where
151        String: From<S>,
152    {
153        if let Some(cached_path) = self.program_location_cache.get(&candidate_name) {
154            Some(cached_path)
155        } else if let Some(found_path) = self.find_first_executable_in_path(&candidate_name) {
156            self.program_location_cache
157                .set(candidate_name, found_path.clone());
158            Some(found_path)
159        } else {
160            None
161        }
162    }
163
164    /// Resolves a command name by searching the shell's current PATH.
165    ///
166    /// Unlike [`Self::find_first_executable_in_path`], a non-executable entry
167    /// in the PATH resolves as the command; the shell reports it as the
168    /// command and then fails to run it.
169    /// See [`pathsearch::resolve_command`].
170    ///
171    /// # Arguments
172    ///
173    /// * `candidate_name` - The name of the command to resolve.
174    pub fn resolve_command_in_path<S: AsRef<str>>(&self, candidate_name: S) -> Option<PathBuf> {
175        let path_var = self.env.get_str("PATH", self).unwrap_or_default();
176        let paths = crate::sys::fs::split_paths(path_var.as_ref());
177        pathsearch::resolve_command(paths, candidate_name.as_ref())
178    }
179
180    /// Like [`Self::resolve_command_in_path`], but consults the shell's
181    /// hash-based path cache first and caches whatever a search turns up.
182    ///
183    /// # Arguments
184    ///
185    /// * `candidate_name` - The name of the command to resolve.
186    pub fn resolve_command_in_path_using_cache<S: AsRef<str>>(
187        &mut self,
188        candidate_name: S,
189    ) -> Option<PathBuf>
190    where
191        String: From<S>,
192    {
193        if let Some(cached_path) = self.program_location_cache.get(&candidate_name) {
194            return Some(cached_path);
195        }
196
197        let found_path = self.resolve_command_in_path(candidate_name.as_ref())?;
198        self.program_location_cache
199            .set(candidate_name, found_path.clone());
200
201        Some(found_path)
202    }
203
204    /// Gets the absolute form of the given path.
205    ///
206    /// # Arguments
207    ///
208    /// * `path` - The path to get the absolute form of.
209    pub fn absolute_path(&self, path: impl AsRef<Path>) -> PathBuf {
210        let path = path.as_ref();
211        if path.as_os_str().is_empty() || path.is_absolute() {
212            path.to_owned()
213        } else {
214            self.working_dir().join(path)
215        }
216    }
217
218    /// Opens the given file, using the context of this shell and the provided
219    /// execution parameters.
220    ///
221    /// # Arguments
222    ///
223    /// * `options` - The options to use opening the file.
224    /// * `path` - The path to the file to open; may be relative to the shell's
225    ///   working directory.
226    /// * `params` - Execution parameters.
227    pub(crate) fn open_file(
228        &self,
229        options: &std::fs::OpenOptions,
230        path: impl AsRef<Path>,
231        params: &ExecutionParameters,
232    ) -> Result<openfiles::OpenFile, std::io::Error> {
233        // Give platform-specific code a chance to handle special files
234        // (e.g. /dev/null on Windows, which needs to open NUL instead).
235        // This is checked before absolute_path so that paths like /dev/null
236        // are intercepted on platforms where they aren't valid native paths.
237        if let Some(result) = crate::sys::fs::try_open_special_file(path.as_ref()) {
238            return result.map(openfiles::OpenFile::from);
239        }
240
241        let path_to_open = self.absolute_path(path.as_ref());
242
243        // See if this is a reference to a file descriptor. These paths should
244        // reflect the shell's current execution fds, which can differ from the
245        // host process fds after redirections like here-docs.
246        if let Some(fd_num) = shell_fd_path_to_fd(&path_to_open)
247            && let Some(open_file) = params.try_fd(self, fd_num)
248        {
249            return Ok(open_file);
250        }
251
252        Ok(options.open(path_to_open)?.into())
253    }
254
255    /// Replaces the shell's currently configured open files with the given set.
256    /// Typically only used by exec-like builtins.
257    ///
258    /// # Arguments
259    ///
260    /// * `open_files` - The new set of open files to use.
261    pub fn replace_open_files(
262        &mut self,
263        open_fds: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
264    ) {
265        self.open_files = openfiles::OpenFiles::from(open_fds);
266    }
267
268    pub(crate) const fn persistent_open_files(&self) -> &openfiles::OpenFiles {
269        &self.open_files
270    }
271}
272
273fn shell_fd_path_to_fd(path: &Path) -> Option<ShellFd> {
274    match path.to_str()? {
275        "/dev/stdin" => return Some(openfiles::OpenFiles::STDIN_FD),
276        "/dev/stdout" => return Some(openfiles::OpenFiles::STDOUT_FD),
277        "/dev/stderr" => return Some(openfiles::OpenFiles::STDERR_FD),
278        _ => {}
279    }
280
281    if let Some(parent) = path.parent()
282        && parent == Path::new("/dev/fd")
283        && let Some(filename) = path.file_name()
284    {
285        filename.to_string_lossy().parse::<ShellFd>().ok()
286    } else {
287        None
288    }
289}