fix(glob): Fix drive-letter glob expansion on Windows (#15871)

# Description
This PR fixes drive-letter glob expansion on Windows. It adds a bit of
pre-processing to play better with the wax crate.
This change is following suggestions from this thread on the wax repo:
https://github.com/olson-sean-k/wax/issues/34

fixes #15707 #7125
This commit is contained in:
Bertrand Bousquet 2025-06-04 12:28:49 -10:00 committed by GitHub
parent 74e0e4f092
commit ed7b2615c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -168,6 +168,10 @@ impl Command for Glob {
}), }),
}; };
// paths starting with drive letters must be escaped on Windows
#[cfg(windows)]
let glob_pattern = patch_windows_glob_pattern(glob_pattern, glob_span)?;
if glob_pattern.is_empty() { if glob_pattern.is_empty() {
return Err(ShellError::GenericError { return Err(ShellError::GenericError {
error: "glob pattern must not be empty".into(), error: "glob pattern must not be empty".into(),
@ -280,6 +284,26 @@ impl Command for Glob {
} }
} }
#[cfg(windows)]
fn patch_windows_glob_pattern(glob_pattern: String, glob_span: Span) -> Result<String, ShellError> {
let mut chars = glob_pattern.chars();
match (chars.next(), chars.next(), chars.next()) {
(Some(drive), Some(':'), Some('/' | '\\')) if drive.is_ascii_alphabetic() => {
Ok(format!("{drive}\\:/{}", chars.as_str()))
}
(Some(drive), Some(':'), Some(_)) if drive.is_ascii_alphabetic() => {
Err(ShellError::GenericError {
error: "invalid Windows path format".into(),
msg: "Windows paths with drive letters must include a path separator (/) after the colon".into(),
span: Some(glob_span),
help: Some("use format like 'C:/' instead of 'C:'".into()),
inner: vec![],
})
}
_ => Ok(glob_pattern),
}
}
fn convert_patterns(columns: &[Value]) -> Result<Vec<String>, ShellError> { fn convert_patterns(columns: &[Value]) -> Result<Vec<String>, ShellError> {
let res = columns let res = columns
.iter() .iter()
@ -325,3 +349,53 @@ fn glob_to_value(
ListStream::new(result, span, signals.clone()) ListStream::new(result, span, signals.clone())
} }
#[cfg(windows)]
#[cfg(test)]
mod windows_tests {
use super::*;
#[test]
fn glob_pattern_with_drive_letter() {
let pattern = "D:/*.mp4".to_string();
let result = patch_windows_glob_pattern(pattern, Span::test_data()).unwrap();
assert!(WaxGlob::new(&result).is_ok());
let pattern = "Z:/**/*.md".to_string();
let result = patch_windows_glob_pattern(pattern, Span::test_data()).unwrap();
assert!(WaxGlob::new(&result).is_ok());
let pattern = "C:/nested/**/escaped/path/<[_a-zA-Z\\-]>.md".to_string();
let result = patch_windows_glob_pattern(pattern, Span::test_data()).unwrap();
assert!(dbg!(WaxGlob::new(&result)).is_ok());
}
#[test]
fn glob_pattern_without_drive_letter() {
let pattern = "/usr/bin/*.sh".to_string();
let result = patch_windows_glob_pattern(pattern.clone(), Span::test_data()).unwrap();
assert_eq!(result, pattern);
assert!(WaxGlob::new(&result).is_ok());
let pattern = "a".to_string();
let result = patch_windows_glob_pattern(pattern.clone(), Span::test_data()).unwrap();
assert_eq!(result, pattern);
assert!(WaxGlob::new(&result).is_ok());
}
#[test]
fn invalid_path_format() {
let invalid = "C:lol".to_string();
let result = patch_windows_glob_pattern(invalid, Span::test_data());
assert!(result.is_err());
}
#[test]
fn unpatched_patterns() {
let unpatched = "C:/Users/*.txt".to_string();
assert!(WaxGlob::new(&unpatched).is_err());
let patched = patch_windows_glob_pattern(unpatched, Span::test_data()).unwrap();
assert!(WaxGlob::new(&patched).is_ok());
}
}