Skip to main content

runmat_runtime/builtins/common/
fs.rs

1//! Filesystem helper utilities shared across REPL-facing builtins.
2//!
3//! These helpers centralize path normalization, wildcard detection,
4//! and platform-aware sorting so builtins such as `ls` and `dir`
5//! can focus on their user-facing semantics.
6
7#[cfg(not(windows))]
8use crate::builtins::common::env as runtime_env;
9use std::cmp::Ordering;
10#[cfg(windows)]
11use std::env;
12use std::path::{Path, PathBuf};
13
14/// Expand a user-relative path (e.g., `~` or `~/Documents`) into an absolute
15/// filesystem path string. Returns the original string when no expansion is
16/// required.
17pub fn expand_user_path(raw: &str, error_prefix: &str) -> Result<String, String> {
18    if raw == "~" {
19        return home_directory()
20            .map(|path| path_to_string(&path))
21            .ok_or_else(|| format!("{error_prefix}: unable to resolve home directory"));
22    }
23
24    if let Some(stripped) = raw.strip_prefix("~/").or_else(|| raw.strip_prefix("~\\")) {
25        let home = home_directory()
26            .ok_or_else(|| format!("{error_prefix}: unable to resolve home directory"))?;
27        let mut buf = home;
28        if !stripped.is_empty() {
29            buf.push(stripped);
30        }
31        return Ok(path_to_string(&buf));
32    }
33
34    Ok(raw.to_string())
35}
36
37/// Return the user's home directory if it can be determined.
38pub fn home_directory() -> Option<PathBuf> {
39    #[cfg(windows)]
40    {
41        if let Ok(user_profile) = env::var("USERPROFILE") {
42            return Some(PathBuf::from(user_profile));
43        }
44        if let (Ok(drive), Ok(path)) = (env::var("HOMEDRIVE"), env::var("HOMEPATH")) {
45            return Some(PathBuf::from(format!("{drive}{path}")));
46        }
47        None
48    }
49    #[cfg(not(windows))]
50    {
51        runtime_env::var("HOME").map(PathBuf::from).ok()
52    }
53}
54
55/// Convert a path into an owned string using lossless conversion semantics.
56pub fn path_to_string(path: &Path) -> String {
57    path.to_string_lossy().into_owned()
58}
59
60/// Return `true` when the text contains glob-style wildcards (`*` or `?`).
61pub fn contains_wildcards(text: &str) -> bool {
62    text.contains('*') || text.contains('?')
63}
64
65/// Sort entries in-place using MATLAB-compatible ordering.
66pub fn sort_entries(entries: &mut [String]) {
67    entries.sort_by(|a, b| compare_names(a, b));
68}
69
70/// Compare two file names using case-insensitive ordering on Windows and
71/// case-sensitive ordering elsewhere, matching MATLAB's behaviour.
72pub fn compare_names(a: &str, b: &str) -> Ordering {
73    #[cfg(windows)]
74    {
75        let lower_a = a.to_ascii_lowercase();
76        let lower_b = b.to_ascii_lowercase();
77        match lower_a.cmp(&lower_b) {
78            Ordering::Equal => a.cmp(b),
79            other => other,
80        }
81    }
82    #[cfg(not(windows))]
83    {
84        a.cmp(b)
85    }
86}