@@ -36,7 +36,8 @@ pub struct Glob {
3636 /// Patterns stored in Arc for cheap cloning into closures
3737 patterns : Arc < [ Pattern ] > ,
3838 absolute : bool ,
39- posix : bool ,
39+ posix_explicit_true : bool ,
40+ posix_explicit_false : bool ,
4041 #[ allow( dead_code) ]
4142 nobrace : bool ,
4243 #[ allow( dead_code) ]
@@ -242,7 +243,8 @@ impl Glob {
242243 . unwrap_or_else ( || std:: env:: current_dir ( ) . unwrap_or_else ( |_| PathBuf :: from ( "." ) ) ) ;
243244
244245 let absolute = options. absolute . unwrap_or ( false ) ;
245- let posix = options. posix . unwrap_or ( false ) ;
246+ let posix_explicit_true = options. posix == Some ( true ) ;
247+ let posix_explicit_false = options. posix == Some ( false ) ;
246248 let nobrace = options. nobrace . unwrap_or ( false ) ;
247249 let noext = options. noext . unwrap_or ( false ) ;
248250 let dot = options. dot . unwrap_or ( false ) ;
@@ -435,7 +437,8 @@ impl Glob {
435437 cwd,
436438 patterns,
437439 absolute,
438- posix,
440+ posix_explicit_true,
441+ posix_explicit_false,
439442 nobrace,
440443 noext,
441444 dot,
@@ -499,11 +502,12 @@ impl Glob {
499502 let mut seen: AHashSet < String > = AHashSet :: with_capacity ( estimated_capacity) ;
500503 let mut ignored_dirs: AHashSet < String > = AHashSet :: with_capacity ( 8 ) ; // Most globs have few ignored dirs
501504
502- // When includeChildMatches is false, track matched paths to exclude their children
503- let mut matched_parents: AHashSet < String > = if self . include_child_matches {
504- AHashSet :: new ( ) // Empty, won't be used
505+ // When includeChildMatches is false, track (result, normalized) pairs for post-filtering
506+ // This is needed because filesystem order may cause children to be seen before parents on Linux
507+ let mut matched_with_normalized: Vec < ( String , String ) > = if self . include_child_matches {
508+ Vec :: new ( ) // Empty, won't be used
505509 } else {
506- AHashSet :: with_capacity ( estimated_capacity / 4 )
510+ Vec :: with_capacity ( estimated_capacity)
507511 } ;
508512
509513 // Pre-allocate a reusable buffer for path formatting
@@ -716,13 +720,6 @@ impl Glob {
716720 continue ;
717721 }
718722
719- // When includeChildMatches is false, skip paths that are children of already-matched paths
720- if !self . include_child_matches
721- && self . is_child_of_matched ( & normalized, & matched_parents)
722- {
723- continue ;
724- }
725-
726723 // Check if any pattern matches
727724 // For patterns that end with /, only match if entry is a directory
728725 let is_dir = entry. is_dir ( ) ;
@@ -766,15 +763,45 @@ impl Glob {
766763
767764 // Deduplicate results (important for overlapping brace expansions)
768765 if seen. insert ( result. clone ( ) ) {
769- // When includeChildMatches is false, track this path to exclude its children
766+ // When includeChildMatches is false, track (result, normalized) for post-filtering
770767 if !self . include_child_matches {
771- matched_parents . insert ( normalized. into_owned ( ) ) ;
768+ matched_with_normalized . push ( ( result . clone ( ) , normalized. into_owned ( ) ) ) ;
772769 }
773770 results. push ( result) ;
774771 }
775772 }
776773 }
777774
775+ // When includeChildMatches is false, post-process to filter out children
776+ // This handles cases where filesystem order causes children to be seen before parents
777+ if !self . include_child_matches && !matched_with_normalized. is_empty ( ) {
778+ // Sort by path depth (number of segments) - shorter paths first
779+ matched_with_normalized. sort_by_key ( |( _, norm) | norm. matches ( '/' ) . count ( ) ) ;
780+
781+ // Filter out children using a set of matched parents
782+ let mut parents: AHashSet < & str > = AHashSet :: with_capacity ( matched_with_normalized. len ( ) ) ;
783+ let mut filtered_results: Vec < String > =
784+ Vec :: with_capacity ( matched_with_normalized. len ( ) ) ;
785+
786+ for ( result, normalized) in & matched_with_normalized {
787+ // Check if this path is a child of any already-matched parent
788+ let is_child = parents. iter ( ) . any ( |parent| {
789+ let parent_bytes = parent. as_bytes ( ) ;
790+ let norm_bytes = normalized. as_bytes ( ) ;
791+ norm_bytes. starts_with ( parent_bytes)
792+ && norm_bytes. len ( ) > parent_bytes. len ( )
793+ && norm_bytes. get ( parent_bytes. len ( ) ) == Some ( & b'/' )
794+ } ) ;
795+
796+ if !is_child {
797+ parents. insert ( normalized. as_str ( ) ) ;
798+ filtered_results. push ( result. clone ( ) ) ;
799+ }
800+ }
801+
802+ return filtered_results;
803+ }
804+
778805 results
779806 }
780807
@@ -795,11 +822,11 @@ impl Glob {
795822 let mut seen: AHashSet < String > = AHashSet :: with_capacity ( estimated_capacity) ;
796823 let mut ignored_dirs: AHashSet < String > = AHashSet :: with_capacity ( 8 ) ;
797824
798- // When includeChildMatches is false, track matched paths to exclude their children
799- let mut matched_parents : AHashSet < String > = if self . include_child_matches {
800- AHashSet :: new ( )
825+ // When includeChildMatches is false, track (result, normalized) pairs for post-filtering
826+ let mut matched_with_normalized : Vec < ( PathData , String ) > = if self . include_child_matches {
827+ Vec :: new ( )
801828 } else {
802- AHashSet :: with_capacity ( estimated_capacity / 4 )
829+ Vec :: with_capacity ( estimated_capacity)
803830 } ;
804831
805832 // Check if any pattern matches the cwd itself ("**" or ".").
@@ -954,13 +981,6 @@ impl Glob {
954981 continue ;
955982 }
956983
957- // When includeChildMatches is false, skip paths that are children of already-matched paths
958- if !self . include_child_matches
959- && self . is_child_of_matched ( & normalized, & matched_parents)
960- {
961- continue ;
962- }
963-
964984 // Check if any pattern matches
965985 let is_dir = entry. is_dir ( ) ;
966986
@@ -995,20 +1015,51 @@ impl Glob {
9951015 normalized. replace ( '/' , "\\ " )
9961016 } ;
9971017 if seen. insert ( output_path. clone ( ) ) {
998- // When includeChildMatches is false, track this path to exclude its children
999- // (use the normalized path with forward slashes for internal tracking)
1000- if !self . include_child_matches {
1001- matched_parents. insert ( output_path. replace ( '\\' , "/" ) ) ;
1002- }
1003-
1004- results. push ( PathData {
1005- path : output_path,
1018+ let path_data = PathData {
1019+ path : output_path. clone ( ) ,
10061020 is_directory : is_dir,
10071021 is_file : entry. is_file ( ) ,
10081022 is_symlink : entry. is_symlink ( ) ,
1009- } ) ;
1023+ } ;
1024+
1025+ // When includeChildMatches is false, track for post-filtering
1026+ if !self . include_child_matches {
1027+ let norm_path = output_path. replace ( '\\' , "/" ) ;
1028+ matched_with_normalized. push ( ( path_data. clone ( ) , norm_path) ) ;
1029+ }
1030+
1031+ results. push ( path_data) ;
1032+ }
1033+ }
1034+ }
1035+
1036+ // When includeChildMatches is false, post-process to filter out children
1037+ if !self . include_child_matches && !matched_with_normalized. is_empty ( ) {
1038+ // Sort by path depth (number of segments) - shorter paths first
1039+ matched_with_normalized. sort_by_key ( |( _, norm) | norm. matches ( '/' ) . count ( ) ) ;
1040+
1041+ // Filter out children using a set of matched parents
1042+ let mut parents: AHashSet < & str > = AHashSet :: with_capacity ( matched_with_normalized. len ( ) ) ;
1043+ let mut filtered_results: Vec < PathData > =
1044+ Vec :: with_capacity ( matched_with_normalized. len ( ) ) ;
1045+
1046+ for ( path_data, normalized) in & matched_with_normalized {
1047+ // Check if this path is a child of any already-matched parent
1048+ let is_child = parents. iter ( ) . any ( |parent| {
1049+ let parent_bytes = parent. as_bytes ( ) ;
1050+ let norm_bytes = normalized. as_bytes ( ) ;
1051+ norm_bytes. starts_with ( parent_bytes)
1052+ && norm_bytes. len ( ) > parent_bytes. len ( )
1053+ && norm_bytes. get ( parent_bytes. len ( ) ) == Some ( & b'/' )
1054+ } ) ;
1055+
1056+ if !is_child {
1057+ parents. insert ( normalized. as_str ( ) ) ;
1058+ filtered_results. push ( path_data. clone ( ) ) ;
10101059 }
10111060 }
1061+
1062+ return filtered_results;
10121063 }
10131064
10141065 results
@@ -1020,7 +1071,7 @@ impl Glob {
10201071 /// (e.g., `C:\foo\bar` → `//?/C:/foo/bar`) to match glob's behavior.
10211072 fn format_path ( & self , path : & std:: path:: Path ) -> String {
10221073 let path_str = path. to_string_lossy ( ) . to_string ( ) ;
1023- if self . posix {
1074+ if self . posix_explicit_true {
10241075 // On Windows with posix: true, convert absolute paths to UNC form
10251076 #[ cfg( target_os = "windows" ) ]
10261077 {
@@ -1118,7 +1169,7 @@ impl Glob {
11181169 buffer. clear ( ) ;
11191170 let path_str = path. to_string_lossy ( ) ;
11201171
1121- if self . posix {
1172+ if self . posix_explicit_true {
11221173 // On Windows with posix: true, convert absolute paths to UNC form
11231174 // e.g., C:\foo\bar → //?/C:/foo/bar
11241175 #[ cfg( target_os = "windows" ) ]
@@ -1970,15 +2021,12 @@ impl Glob {
19702021
19712022 /// Check if backslashes should be normalized to forward slashes.
19722023 ///
1973- /// On Windows with posix: false (the default), glob v13 outputs backslashes.
1974- /// On Windows with posix: true, glob v13 outputs forward slashes.
1975- /// On Unix, glob v13 always outputs forward slashes.
2024+ /// glob v13 uses forward slashes by default on all platforms.
2025+ /// Only when posix is explicitly set to false on Windows are backslashes used.
19762026 #[ inline]
19772027 fn should_normalize_backslashes ( & self ) -> bool {
1978- // Use forward slashes when:
1979- // - On Unix (always)
1980- // - On Windows with posix: true
1981- self . posix || !cfg ! ( target_os = "windows" )
2028+ // Use forward slashes unless posix was explicitly set to false on Windows
2029+ !cfg ! ( target_os = "windows" ) || !self . posix_explicit_false
19822030 }
19832031
19842032 /// Normalize path separators based on platform and posix option.
0 commit comments