@@ -144,17 +144,84 @@ path."#
144
144
fn relative_to ( path : & Path , span : Span , args : & Arguments ) -> Value {
145
145
let lhs = expand_to_real_path ( path) ;
146
146
let rhs = expand_to_real_path ( & args. path . item ) ;
147
+
147
148
match lhs. strip_prefix ( & rhs) {
148
149
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
+ } ,
153
165
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
+ }
158
225
}
159
226
}
160
227
@@ -168,4 +235,89 @@ mod tests {
168
235
169
236
test_examples ( PathRelativeTo { } )
170
237
}
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
+ }
171
323
}
0 commit comments