@@ -144,17 +144,84 @@ path."#
144144fn relative_to ( path : & Path , span : Span , args : & Arguments ) -> Value {
145145 let lhs = expand_to_real_path ( path) ;
146146 let rhs = expand_to_real_path ( & args. path . item ) ;
147+
147148 match lhs. strip_prefix ( & rhs) {
148149 Ok ( p) => Value :: string ( p. to_string_lossy ( ) , span) ,
149- Err ( e) => Value :: error (
150- ShellError :: CantConvert {
151- to_type : e. to_string ( ) ,
152- from_type : "string" . into ( ) ,
150+ Err ( e) => {
151+ // On case-insensitive filesystems, try case-insensitive comparison
152+ if is_case_insensitive_filesystem ( ) {
153+ if let Some ( relative_path) = try_case_insensitive_strip_prefix ( & lhs, & rhs) {
154+ return Value :: string ( relative_path. to_string_lossy ( ) , span) ;
155+ }
156+ }
157+
158+ Value :: error (
159+ ShellError :: CantConvert {
160+ to_type : e. to_string ( ) ,
161+ from_type : "string" . into ( ) ,
162+ span,
163+ help : None ,
164+ } ,
153165 span,
154- help : None ,
155- } ,
156- span,
157- ) ,
166+ )
167+ }
168+ }
169+ }
170+
171+ /// Check if the current filesystem is typically case-insensitive
172+ fn is_case_insensitive_filesystem ( ) -> bool {
173+ // Windows and macOS typically have case-insensitive filesystems
174+ cfg ! ( any( target_os = "windows" , target_os = "macos" ) )
175+ }
176+
177+ /// Try to strip prefix in a case-insensitive manner
178+ fn try_case_insensitive_strip_prefix ( lhs : & Path , rhs : & Path ) -> Option < std:: path:: PathBuf > {
179+ let mut lhs_components = lhs. components ( ) ;
180+ let mut rhs_components = rhs. components ( ) ;
181+
182+ // Compare components case-insensitively
183+ loop {
184+ match ( lhs_components. next ( ) , rhs_components. next ( ) ) {
185+ ( Some ( lhs_comp) , Some ( rhs_comp) ) => {
186+ match ( lhs_comp, rhs_comp) {
187+ (
188+ std:: path:: Component :: Normal ( lhs_name) ,
189+ std:: path:: Component :: Normal ( rhs_name) ,
190+ ) => {
191+ if lhs_name. to_string_lossy ( ) . to_lowercase ( )
192+ != rhs_name. to_string_lossy ( ) . to_lowercase ( )
193+ {
194+ return None ;
195+ }
196+ }
197+ // Non-Normal components must match exactly
198+ _ if lhs_comp != rhs_comp => {
199+ return None ;
200+ }
201+ _ => { }
202+ }
203+ }
204+ ( Some ( lhs_comp) , None ) => {
205+ // rhs is fully consumed, but lhs has more components
206+ // This means rhs is a prefix of lhs, collect remaining lhs components
207+ let mut result = std:: path:: PathBuf :: new ( ) ;
208+ // Add the current lhs component that wasn't matched
209+ result. push ( lhs_comp) ;
210+ // Add all remaining lhs components
211+ for component in lhs_components {
212+ result. push ( component) ;
213+ }
214+ return Some ( result) ;
215+ }
216+ ( None , Some ( _) ) => {
217+ // lhs is shorter than rhs, so rhs cannot be a prefix of lhs
218+ return None ;
219+ }
220+ ( None , None ) => {
221+ // Both paths have the same components, relative path is empty
222+ return Some ( std:: path:: PathBuf :: new ( ) ) ;
223+ }
224+ }
158225 }
159226}
160227
@@ -168,4 +235,89 @@ mod tests {
168235
169236 test_examples ( PathRelativeTo { } )
170237 }
238+
239+ #[ test]
240+ fn test_case_insensitive_filesystem ( ) {
241+ use nu_protocol:: { Span , Value } ;
242+ use std:: path:: Path ;
243+
244+ let args = Arguments {
245+ path : Spanned {
246+ item : "/Etc" . to_string ( ) ,
247+ span : Span :: test_data ( ) ,
248+ } ,
249+ } ;
250+
251+ let result = relative_to ( Path :: new ( "/etc" ) , Span :: test_data ( ) , & args) ;
252+
253+ // On case-insensitive filesystems (Windows, macOS), this should work
254+ // On case-sensitive filesystems (Linux, FreeBSD), this should fail
255+ if is_case_insensitive_filesystem ( ) {
256+ match result {
257+ Value :: String { val, .. } => {
258+ assert_eq ! ( val, "" ) ;
259+ }
260+ _ => panic ! ( "Expected string result on case-insensitive filesystem" ) ,
261+ }
262+ } else {
263+ match result {
264+ Value :: Error { .. } => {
265+ // Expected on case-sensitive filesystems
266+ }
267+ _ => panic ! ( "Expected error on case-sensitive filesystem" ) ,
268+ }
269+ }
270+ }
271+
272+ #[ test]
273+ fn test_case_insensitive_with_subpath ( ) {
274+ use nu_protocol:: { Span , Value } ;
275+ use std:: path:: Path ;
276+
277+ let args = Arguments {
278+ path : Spanned {
279+ item : "/Home/User" . to_string ( ) ,
280+ span : Span :: test_data ( ) ,
281+ } ,
282+ } ;
283+
284+ let result = relative_to ( Path :: new ( "/home/user/documents" ) , Span :: test_data ( ) , & args) ;
285+
286+ if is_case_insensitive_filesystem ( ) {
287+ match result {
288+ Value :: String { val, .. } => {
289+ assert_eq ! ( val, "documents" ) ;
290+ }
291+ _ => panic ! ( "Expected string result on case-insensitive filesystem" ) ,
292+ }
293+ } else {
294+ match result {
295+ Value :: Error { .. } => {
296+ // Expected on case-sensitive filesystems
297+ }
298+ _ => panic ! ( "Expected error on case-sensitive filesystem" ) ,
299+ }
300+ }
301+ }
302+
303+ #[ test]
304+ fn test_truly_different_paths ( ) {
305+ use nu_protocol:: { Span , Value } ;
306+ use std:: path:: Path ;
307+
308+ let args = Arguments {
309+ path : Spanned {
310+ item : "/Different/Path" . to_string ( ) ,
311+ span : Span :: test_data ( ) ,
312+ } ,
313+ } ;
314+
315+ let result = relative_to ( Path :: new ( "/home/user" ) , Span :: test_data ( ) , & args) ;
316+
317+ // This should fail on all filesystems since paths are truly different
318+ match result {
319+ Value :: Error { .. } => { }
320+ _ => panic ! ( "Expected error for truly different paths" ) ,
321+ }
322+ }
171323}
0 commit comments