Skip to content
52 changes: 43 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -179,6 +181,7 @@ pub fn glob(pattern: &str) -> Result<Paths, PatternError> {
///
/// Paths are yielded in alphabetical order.
pub fn glob_with(pattern: &str, options: MatchOptions) -> Result<Paths, PatternError> {
use utils::{get_home_dir, get_user_name};
#[cfg(windows)]
fn check_windows_verbatim(p: &Path) -> bool {
match p.components().next() {
Expand Down Expand Up @@ -211,7 +214,28 @@ pub fn glob_with(pattern: &str, options: MatchOptions) -> Result<Paths, PatternE

// make sure that the pattern is valid first, else early return with error
let _ = Pattern::new(pattern)?;

let mut new_pattern = pattern.to_owned();
if options.glob_tilde_expansion {
let home_dir = get_home_dir();
if pattern == "~" || pattern.starts_with("~/") {
if let Some(home) = home_dir {
new_pattern = pattern.replacen("~", &home, 1);
}
} else if pattern.starts_with("~") {
if let Some(user) = get_user_name() {
match pattern.strip_prefix("~").unwrap().strip_prefix(&user) {
Some(v) if v.starts_with("/") || v.is_empty() => {
if let Some(mut p) = home_dir {
p.push_str(v);
new_pattern = p;
}
}
_ => {}
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the intent of this branch? AFAIK ~foo without the path shouldn't be a valid pattern

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ~ followed by user name then ~ and username are substituted by the home directory of that user.
  • If the username is invalid, or the home directory cannot be determined, then no substitution is performed.
  • if we want ~ followed by not username or not / or the home directory cannot be determined as an error. we have to add a another field to MatchOptions. if the fiend is set true the it will be return error. if the field set false, it ignore it.

(could i add an extra field to the MatcherOptiond ?)

https://man7.org/linux/man-pages/man3/glob.3.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed that in the glob manpage, thanks.

I think we should defer that part, and error if there is ~ not followed by /. Checking the env isn't always accurate: it needs getpwuid_r on Unix and GetUserProfileDirectoryA on Windows, which is more complexity than this crate should add.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright, i will update it.

}
let pattern = new_pattern.as_str();
let mut components = Path::new(pattern).components().peekable();
loop {
match components.peek() {
Expand Down Expand Up @@ -1050,7 +1074,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
Expand All @@ -1069,6 +1093,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 {
Expand All @@ -1083,19 +1122,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()
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#[inline(always)]
pub(crate) fn get_home_dir() -> Option<String> {
#[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<String> {
#[cfg(not(target_os = "windows"))]
return std::env::var("USER").ok();
#[cfg(target_os = "windows")]
std::env::var("USERNAME").ok()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will go away with #173 (comment) but for future reference, you should use cfg! rather than #[cfg(...)] where possible (i.e. when the types are the same).

let varname = if cfg!(windows) { "USERNAME" } else { "USER" };
env::var(varname).ok()

Makes no difference here but in general it avoids conditional compilation, so both branches get checked on all platforms. Also lets you use else.

Copy link

@reneleonhardt reneleonhardt Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, and even the merciless rust formatter doesn't bloat 1 line to 5 lines in this case at least 😍

env::var(if cfg!(windows) { "USERNAME" } else { "USER" }).ok()

}
Loading