diff --git a/src/lib.rs b/src/lib.rs index 133a17a..402a6ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,6 +70,8 @@ extern crate doc_comment; #[cfg(test)] doctest!("../README.md"); +mod utils; + use std::cmp; use std::cmp::Ordering; use std::error::Error; @@ -179,6 +181,7 @@ pub fn glob(pattern: &str) -> Result { /// /// Paths are yielded in alphabetical order. pub fn glob_with(pattern: &str, options: MatchOptions) -> Result { + use utils::{get_home_dir, get_user_name}; #[cfg(windows)] fn check_windows_verbatim(p: &Path) -> bool { match p.components().next() { @@ -211,7 +214,42 @@ pub fn glob_with(pattern: &str, options: MatchOptions) -> Result v, + None => { + return Err(PatternError { + pos: 0, + msg: "Failed to determine home directory.", + }) + } + }; + if pattern == "~" || pattern.starts_with("~/") { + new_pattern = pattern.replacen("~", &home_dir, 1); + } else { + if let Some(user) = get_user_name() { + match pattern.strip_prefix("~").unwrap().strip_prefix(&user) { + Some(v) if v.starts_with("/") || v.is_empty() => { + home_dir.push_str(v); + new_pattern = home_dir; + } + _ => { + return Err(PatternError { + pos: 1, + msg: "'~' not followed by '/' or user name.", + }) + } + }; + } else { + return Err(PatternError { + pos: 1, + msg: "Failed to determine user name.", + }); + } + } + } + let pattern = new_pattern.as_str(); let mut components = Path::new(pattern).components().peekable(); loop { match components.peek() { @@ -1050,7 +1088,7 @@ fn chars_eq(a: char, b: char, case_sensitive: bool) -> bool { /// Configuration options to modify the behaviour of `Pattern::matches_with(..)`. #[allow(missing_copy_implementations)] -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MatchOptions { /// Whether or not patterns should be matched in a case-sensitive manner. /// This currently only considers upper/lower case relationships between @@ -1069,6 +1107,21 @@ pub struct MatchOptions { /// conventionally considered hidden on Unix systems and it might be /// desirable to skip them when listing files. pub require_literal_leading_dot: bool, + + /// Whether or not tilde expansion should be performed. if home directory + /// or user name cannot be determined, then no tilde expansion is performed. + pub glob_tilde_expansion: bool, +} + +impl Default for MatchOptions { + fn default() -> Self { + Self { + case_sensitive: true, + require_literal_separator: false, + require_literal_leading_dot: false, + glob_tilde_expansion: false, + } + } } impl MatchOptions { @@ -1083,19 +1136,14 @@ impl MatchOptions { /// case_sensitive: true, /// require_literal_separator: false, /// require_literal_leading_dot: false + /// glob_tilde_expansion: false, /// } /// ``` - /// /// # Note /// The behavior of this method doesn't match `default()`'s. This returns /// `case_sensitive` as `true` while `default()` does it as `false`. - // FIXME: Consider unity the behavior with `default()` in a next major release. pub fn new() -> Self { - Self { - case_sensitive: true, - require_literal_separator: false, - require_literal_leading_dot: false, - } + Self::default() } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..3eb238e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,12 @@ +#[inline(always)] +pub(crate) fn get_home_dir() -> Option { + #[allow(deprecated)] + std::env::home_dir().and_then(|v| v.to_str().map(String::from)) +} + +// This function is required when `glob_tilde_expansion` field of `glob::MatchOptions` is +// set `true` and pattern starts with `~` followed by any char expect `/` +pub(crate) fn get_user_name() -> Option { + let varname = if cfg!(windows) { "USERNAME" } else { "USER" }; + std::env::var(varname).ok() +}