mirror of
https://github.com/nushell/nushell.git
synced 2025-08-11 13:04:39 +02:00
Fix path relative-to for case-insensitive filesystems (#16310)
fixes #16205 # Description 1. **Adds fallback**: On case-insensitive filesystems (Windows, macOS), falls back to case-insensitive comparison when the standard comparison fails 2. **Maintains filesystem semantics**: Only uses case-insensitive comparison on platforms where it's appropriate ## Before: ```console $> "/etc" | path relative-to "/Etc" Error: nu:🐚:cant_convert × Can't convert to prefix not found. ╭─[entry #33:1:1] 1 │ "/etc" | path relative-to "/Etc" · ───┬── · ╰── can't convert string to prefix not found ╰──── ``` ## After: For Windows and macOS: ```console $> "/etc" | path relative-to "/Etc" | debug -v "" ```
This commit is contained in:
@ -144,17 +144,84 @@ path."#
|
|||||||
fn relative_to(path: &Path, span: Span, args: &Arguments) -> Value {
|
fn relative_to(path: &Path, span: Span, args: &Arguments) -> Value {
|
||||||
let lhs = expand_to_real_path(path);
|
let lhs = expand_to_real_path(path);
|
||||||
let rhs = expand_to_real_path(&args.path.item);
|
let rhs = expand_to_real_path(&args.path.item);
|
||||||
|
|
||||||
match lhs.strip_prefix(&rhs) {
|
match lhs.strip_prefix(&rhs) {
|
||||||
Ok(p) => Value::string(p.to_string_lossy(), span),
|
Ok(p) => Value::string(p.to_string_lossy(), span),
|
||||||
Err(e) => Value::error(
|
Err(e) => {
|
||||||
ShellError::CantConvert {
|
// On case-insensitive filesystems, try case-insensitive comparison
|
||||||
to_type: e.to_string(),
|
if is_case_insensitive_filesystem() {
|
||||||
from_type: "string".into(),
|
if let Some(relative_path) = try_case_insensitive_strip_prefix(&lhs, &rhs) {
|
||||||
|
return Value::string(relative_path.to_string_lossy(), span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::error(
|
||||||
|
ShellError::CantConvert {
|
||||||
|
to_type: e.to_string(),
|
||||||
|
from_type: "string".into(),
|
||||||
|
span,
|
||||||
|
help: None,
|
||||||
|
},
|
||||||
span,
|
span,
|
||||||
help: None,
|
)
|
||||||
},
|
}
|
||||||
span,
|
}
|
||||||
),
|
}
|
||||||
|
|
||||||
|
/// Check if the current filesystem is typically case-insensitive
|
||||||
|
fn is_case_insensitive_filesystem() -> bool {
|
||||||
|
// Windows and macOS typically have case-insensitive filesystems
|
||||||
|
cfg!(any(target_os = "windows", target_os = "macos"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to strip prefix in a case-insensitive manner
|
||||||
|
fn try_case_insensitive_strip_prefix(lhs: &Path, rhs: &Path) -> Option<std::path::PathBuf> {
|
||||||
|
let mut lhs_components = lhs.components();
|
||||||
|
let mut rhs_components = rhs.components();
|
||||||
|
|
||||||
|
// Compare components case-insensitively
|
||||||
|
loop {
|
||||||
|
match (lhs_components.next(), rhs_components.next()) {
|
||||||
|
(Some(lhs_comp), Some(rhs_comp)) => {
|
||||||
|
match (lhs_comp, rhs_comp) {
|
||||||
|
(
|
||||||
|
std::path::Component::Normal(lhs_name),
|
||||||
|
std::path::Component::Normal(rhs_name),
|
||||||
|
) => {
|
||||||
|
if lhs_name.to_string_lossy().to_lowercase()
|
||||||
|
!= rhs_name.to_string_lossy().to_lowercase()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-Normal components must match exactly
|
||||||
|
_ if lhs_comp != rhs_comp => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(lhs_comp), None) => {
|
||||||
|
// rhs is fully consumed, but lhs has more components
|
||||||
|
// This means rhs is a prefix of lhs, collect remaining lhs components
|
||||||
|
let mut result = std::path::PathBuf::new();
|
||||||
|
// Add the current lhs component that wasn't matched
|
||||||
|
result.push(lhs_comp);
|
||||||
|
// Add all remaining lhs components
|
||||||
|
for component in lhs_components {
|
||||||
|
result.push(component);
|
||||||
|
}
|
||||||
|
return Some(result);
|
||||||
|
}
|
||||||
|
(None, Some(_)) => {
|
||||||
|
// lhs is shorter than rhs, so rhs cannot be a prefix of lhs
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
// Both paths have the same components, relative path is empty
|
||||||
|
return Some(std::path::PathBuf::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,4 +235,89 @@ mod tests {
|
|||||||
|
|
||||||
test_examples(PathRelativeTo {})
|
test_examples(PathRelativeTo {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_case_insensitive_filesystem() {
|
||||||
|
use nu_protocol::{Span, Value};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let args = Arguments {
|
||||||
|
path: Spanned {
|
||||||
|
item: "/Etc".to_string(),
|
||||||
|
span: Span::test_data(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = relative_to(Path::new("/etc"), Span::test_data(), &args);
|
||||||
|
|
||||||
|
// On case-insensitive filesystems (Windows, macOS), this should work
|
||||||
|
// On case-sensitive filesystems (Linux, FreeBSD), this should fail
|
||||||
|
if is_case_insensitive_filesystem() {
|
||||||
|
match result {
|
||||||
|
Value::String { val, .. } => {
|
||||||
|
assert_eq!(val, "");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected string result on case-insensitive filesystem"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match result {
|
||||||
|
Value::Error { .. } => {
|
||||||
|
// Expected on case-sensitive filesystems
|
||||||
|
}
|
||||||
|
_ => panic!("Expected error on case-sensitive filesystem"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_case_insensitive_with_subpath() {
|
||||||
|
use nu_protocol::{Span, Value};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let args = Arguments {
|
||||||
|
path: Spanned {
|
||||||
|
item: "/Home/User".to_string(),
|
||||||
|
span: Span::test_data(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = relative_to(Path::new("/home/user/documents"), Span::test_data(), &args);
|
||||||
|
|
||||||
|
if is_case_insensitive_filesystem() {
|
||||||
|
match result {
|
||||||
|
Value::String { val, .. } => {
|
||||||
|
assert_eq!(val, "documents");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected string result on case-insensitive filesystem"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match result {
|
||||||
|
Value::Error { .. } => {
|
||||||
|
// Expected on case-sensitive filesystems
|
||||||
|
}
|
||||||
|
_ => panic!("Expected error on case-sensitive filesystem"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truly_different_paths() {
|
||||||
|
use nu_protocol::{Span, Value};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let args = Arguments {
|
||||||
|
path: Spanned {
|
||||||
|
item: "/Different/Path".to_string(),
|
||||||
|
span: Span::test_data(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = relative_to(Path::new("/home/user"), Span::test_data(), &args);
|
||||||
|
|
||||||
|
// This should fail on all filesystems since paths are truly different
|
||||||
|
match result {
|
||||||
|
Value::Error { .. } => {}
|
||||||
|
_ => panic!("Expected error for truly different paths"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user