diff --git a/crates/nu-command/src/path/relative_to.rs b/crates/nu-command/src/path/relative_to.rs index d63c3e2bb0..88575e0db4 100644 --- a/crates/nu-command/src/path/relative_to.rs +++ b/crates/nu-command/src/path/relative_to.rs @@ -144,17 +144,84 @@ path."# fn relative_to(path: &Path, span: Span, args: &Arguments) -> Value { let lhs = expand_to_real_path(path); let rhs = expand_to_real_path(&args.path.item); + match lhs.strip_prefix(&rhs) { Ok(p) => Value::string(p.to_string_lossy(), span), - Err(e) => Value::error( - ShellError::CantConvert { - to_type: e.to_string(), - from_type: "string".into(), + Err(e) => { + // On case-insensitive filesystems, try case-insensitive comparison + if is_case_insensitive_filesystem() { + 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, - 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 { + 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] + 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"), + } + } }