@@ -724,22 +724,15 @@ impl Interpreter {
724724 for expanded in fields {
725725 let brace_expanded = self . expand_braces ( & expanded) ;
726726 for item in brace_expanded {
727- if self . contains_glob_chars ( & item) {
728- let glob_matches = self . expand_glob ( & item) . await ?;
729- if glob_matches. is_empty ( ) {
730- let nullglob = self
731- . variables
732- . get ( "SHOPT_nullglob" )
733- . map ( |v| v == "1" )
734- . unwrap_or ( false ) ;
735- if !nullglob {
736- vals. push ( item) ;
737- }
738- } else {
739- vals. extend ( glob_matches) ;
727+ match self . expand_glob_item ( & item) . await {
728+ Ok ( items) => vals. extend ( items) ,
729+ Err ( pat) => {
730+ self . last_exit_code = 1 ;
731+ return Ok ( ExecResult :: err (
732+ format ! ( "-bash: no match: {}\n " , pat) ,
733+ 1 ,
734+ ) ) ;
740735 }
741- } else {
742- vals. push ( item) ;
743736 }
744737 }
745738 }
@@ -852,22 +845,15 @@ impl Interpreter {
852845 for expanded in fields {
853846 let brace_expanded = self . expand_braces ( & expanded) ;
854847 for item in brace_expanded {
855- if self . contains_glob_chars ( & item) {
856- let glob_matches = self . expand_glob ( & item) . await ?;
857- if glob_matches. is_empty ( ) {
858- let nullglob = self
859- . variables
860- . get ( "SHOPT_nullglob" )
861- . map ( |v| v == "1" )
862- . unwrap_or ( false ) ;
863- if !nullglob {
864- values. push ( item) ;
865- }
866- } else {
867- values. extend ( glob_matches) ;
848+ match self . expand_glob_item ( & item) . await {
849+ Ok ( items) => values. extend ( items) ,
850+ Err ( pat) => {
851+ self . last_exit_code = 1 ;
852+ return Ok ( ExecResult :: err (
853+ format ! ( "-bash: no match: {}\n " , pat) ,
854+ 1 ,
855+ ) ) ;
868856 }
869- } else {
870- values. push ( item) ;
871857 }
872858 }
873859 }
@@ -2076,6 +2062,11 @@ impl Interpreter {
20762062
20772063 /// Simple glob pattern matching with support for *, ?, and [...]
20782064 fn glob_match ( & self , value : & str , pattern : & str ) -> bool {
2065+ self . glob_match_impl ( value, pattern, false )
2066+ }
2067+
2068+ /// Glob match with optional case-insensitive mode
2069+ fn glob_match_impl ( & self , value : & str , pattern : & str , nocase : bool ) -> bool {
20792070 let mut value_chars = value. chars ( ) . peekable ( ) ;
20802071 let mut pattern_chars = pattern. chars ( ) . peekable ( ) ;
20812072
@@ -2093,14 +2084,14 @@ impl Interpreter {
20932084 while value_chars. peek ( ) . is_some ( ) {
20942085 let remaining_value: String = value_chars. clone ( ) . collect ( ) ;
20952086 let remaining_pattern: String = pattern_chars. clone ( ) . collect ( ) ;
2096- if self . glob_match ( & remaining_value, & remaining_pattern) {
2087+ if self . glob_match_impl ( & remaining_value, & remaining_pattern, nocase ) {
20972088 return true ;
20982089 }
20992090 value_chars. next ( ) ;
21002091 }
21012092 // Also try with empty match
21022093 let remaining_pattern: String = pattern_chars. collect ( ) ;
2103- return self . glob_match ( "" , & remaining_pattern) ;
2094+ return self . glob_match_impl ( "" , & remaining_pattern, nocase ) ;
21042095 }
21052096 ( Some ( '?' ) , Some ( _) ) => {
21062097 pattern_chars. next ( ) ;
@@ -2109,7 +2100,10 @@ impl Interpreter {
21092100 ( Some ( '?' ) , None ) => return false ,
21102101 ( Some ( '[' ) , Some ( v) ) => {
21112102 pattern_chars. next ( ) ; // consume '['
2112- if let Some ( matched) = self . match_bracket_expr ( & mut pattern_chars, v) {
2103+ let match_char = if nocase { v. to_ascii_lowercase ( ) } else { v } ;
2104+ if let Some ( matched) =
2105+ self . match_bracket_expr ( & mut pattern_chars, match_char, nocase)
2106+ {
21132107 if matched {
21142108 value_chars. next ( ) ;
21152109 } else {
@@ -2122,7 +2116,12 @@ impl Interpreter {
21222116 }
21232117 ( Some ( '[' ) , None ) => return false ,
21242118 ( Some ( p) , Some ( v) ) => {
2125- if p == v {
2119+ let matches = if nocase {
2120+ p. eq_ignore_ascii_case ( & v)
2121+ } else {
2122+ p == v
2123+ } ;
2124+ if matches {
21262125 pattern_chars. next ( ) ;
21272126 value_chars. next ( ) ;
21282127 } else {
@@ -2140,6 +2139,7 @@ impl Interpreter {
21402139 & self ,
21412140 pattern_chars : & mut std:: iter:: Peekable < std:: str:: Chars < ' _ > > ,
21422141 value_char : char ,
2142+ nocase : bool ,
21432143 ) -> Option < bool > {
21442144 let mut chars_in_class = Vec :: new ( ) ;
21452145 let mut negate = false ;
@@ -2182,7 +2182,12 @@ impl Interpreter {
21822182 }
21832183 }
21842184
2185- let matched = chars_in_class. contains ( & value_char) ;
2185+ let matched = if nocase {
2186+ let lc = value_char. to_ascii_lowercase ( ) ;
2187+ chars_in_class. iter ( ) . any ( |& c| c. to_ascii_lowercase ( ) == lc)
2188+ } else {
2189+ chars_in_class. contains ( & value_char)
2190+ } ;
21862191 Some ( if negate { !matched } else { matched } )
21872192 }
21882193
@@ -2692,25 +2697,12 @@ impl Interpreter {
26922697
26932698 // Step 2: For each brace-expanded item, do glob expansion
26942699 for item in brace_expanded {
2695- if self . contains_glob_chars ( & item) {
2696- let glob_matches = self . expand_glob ( & item) . await ?;
2697- if glob_matches. is_empty ( ) {
2698- // nullglob: unmatched globs expand to nothing
2699- let nullglob = self
2700- . variables
2701- . get ( "SHOPT_nullglob" )
2702- . map ( |v| v == "1" )
2703- . unwrap_or ( false ) ;
2704- if !nullglob {
2705- // Default: keep original pattern (bash behavior)
2706- args. push ( item) ;
2707- }
2708- // With nullglob: skip (produce nothing)
2709- } else {
2710- args. extend ( glob_matches) ;
2700+ match self . expand_glob_item ( & item) . await {
2701+ Ok ( items) => args. extend ( items) ,
2702+ Err ( pat) => {
2703+ self . last_exit_code = 1 ;
2704+ return Ok ( ExecResult :: err ( format ! ( "-bash: no match: {}\n " , pat) , 1 ) ) ;
27112705 }
2712- } else {
2713- args. push ( item) ;
27142706 }
27152707 }
27162708 }
@@ -6187,14 +6179,82 @@ impl Interpreter {
61876179 s. contains ( '*' ) || s. contains ( '?' ) || s. contains ( '[' )
61886180 }
61896181
6182+ /// Check if dotglob shopt is enabled
6183+ fn is_dotglob ( & self ) -> bool {
6184+ self . variables
6185+ . get ( "SHOPT_dotglob" )
6186+ . map ( |v| v == "1" )
6187+ . unwrap_or ( false )
6188+ }
6189+
6190+ /// Check if nocaseglob shopt is enabled
6191+ fn is_nocaseglob ( & self ) -> bool {
6192+ self . variables
6193+ . get ( "SHOPT_nocaseglob" )
6194+ . map ( |v| v == "1" )
6195+ . unwrap_or ( false )
6196+ }
6197+
6198+ /// Check if noglob (set -f) is enabled
6199+ fn is_noglob ( & self ) -> bool {
6200+ self . variables
6201+ . get ( "SHOPT_f" )
6202+ . map ( |v| v == "1" )
6203+ . unwrap_or ( false )
6204+ }
6205+
6206+ /// Check if failglob shopt is enabled
6207+ fn is_failglob ( & self ) -> bool {
6208+ self . variables
6209+ . get ( "SHOPT_failglob" )
6210+ . map ( |v| v == "1" )
6211+ . unwrap_or ( false )
6212+ }
6213+
6214+ /// Check if globstar shopt is enabled
6215+ fn is_globstar ( & self ) -> bool {
6216+ self . variables
6217+ . get ( "SHOPT_globstar" )
6218+ . map ( |v| v == "1" )
6219+ . unwrap_or ( false )
6220+ }
6221+
6222+ /// Expand glob for a single item, applying noglob/failglob/nullglob.
6223+ /// Returns Err(pattern) if failglob triggers, Ok(items) otherwise.
6224+ async fn expand_glob_item ( & self , item : & str ) -> std:: result:: Result < Vec < String > , String > {
6225+ if !self . contains_glob_chars ( item) || self . is_noglob ( ) {
6226+ return Ok ( vec ! [ item. to_string( ) ] ) ;
6227+ }
6228+ let glob_matches = self . expand_glob ( item) . await . unwrap_or_default ( ) ;
6229+ if glob_matches. is_empty ( ) {
6230+ if self . is_failglob ( ) {
6231+ return Err ( item. to_string ( ) ) ;
6232+ }
6233+ let nullglob = self
6234+ . variables
6235+ . get ( "SHOPT_nullglob" )
6236+ . map ( |v| v == "1" )
6237+ . unwrap_or ( false ) ;
6238+ if nullglob {
6239+ Ok ( vec ! [ ] )
6240+ } else {
6241+ Ok ( vec ! [ item. to_string( ) ] )
6242+ }
6243+ } else {
6244+ Ok ( glob_matches)
6245+ }
6246+ }
6247+
61906248 /// Expand a glob pattern against the filesystem
61916249 async fn expand_glob ( & self , pattern : & str ) -> Result < Vec < String > > {
6192- // Check for ** (recursive glob)
6193- if pattern. contains ( "**" ) {
6250+ // Check for ** (recursive glob) — only when globstar is enabled
6251+ if pattern. contains ( "**" ) && self . is_globstar ( ) {
61946252 return self . expand_glob_recursive ( pattern) . await ;
61956253 }
61966254
61976255 let mut matches = Vec :: new ( ) ;
6256+ let dotglob = self . is_dotglob ( ) ;
6257+ let nocase = self . is_nocaseglob ( ) ;
61986258
61996259 // Split pattern into directory and filename parts
62006260 let path = Path :: new ( pattern) ;
@@ -6233,9 +6293,17 @@ impl Interpreter {
62336293 Err ( _) => return Ok ( matches) ,
62346294 } ;
62356295
6296+ // Check if pattern explicitly starts with dot
6297+ let pattern_starts_with_dot = file_pattern. starts_with ( '.' ) ;
6298+
62366299 // Match each entry against the pattern
62376300 for entry in entries {
6238- if self . glob_match ( & entry. name , & file_pattern) {
6301+ // Skip dotfiles unless dotglob is set or pattern explicitly starts with '.'
6302+ if entry. name . starts_with ( '.' ) && !dotglob && !pattern_starts_with_dot {
6303+ continue ;
6304+ }
6305+
6306+ if self . glob_match_impl ( & entry. name , & file_pattern, nocase) {
62396307 // Construct the full path
62406308 let full_path = if path. is_absolute ( ) {
62416309 dir. join ( & entry. name ) . to_string_lossy ( ) . to_string ( )
@@ -6264,6 +6332,8 @@ impl Interpreter {
62646332 async fn expand_glob_recursive ( & self , pattern : & str ) -> Result < Vec < String > > {
62656333 let is_absolute = pattern. starts_with ( '/' ) ;
62666334 let components: Vec < & str > = pattern. split ( '/' ) . filter ( |s| !s. is_empty ( ) ) . collect ( ) ;
6335+ let dotglob = self . is_dotglob ( ) ;
6336+ let nocase = self . is_nocaseglob ( ) ;
62676337
62686338 // Find the ** component
62696339 let star_star_idx = match components. iter ( ) . position ( |& c| c == "**" ) {
@@ -6300,16 +6370,24 @@ impl Interpreter {
63006370 // ** alone matches all files recursively
63016371 if let Ok ( entries) = self . fs . read_dir ( dir) . await {
63026372 for entry in entries {
6373+ if entry. name . starts_with ( '.' ) && !dotglob {
6374+ continue ;
6375+ }
63036376 if !entry. metadata . file_type . is_dir ( ) {
63046377 matches. push ( dir. join ( & entry. name ) . to_string_lossy ( ) . to_string ( ) ) ;
63056378 }
63066379 }
63076380 }
63086381 } else if after_pattern. len ( ) == 1 {
63096382 // Single pattern after **: match files in this directory
6383+ let pat = after_pattern[ 0 ] ;
6384+ let pattern_starts_with_dot = pat. starts_with ( '.' ) ;
63106385 if let Ok ( entries) = self . fs . read_dir ( dir) . await {
63116386 for entry in entries {
6312- if self . glob_match ( & entry. name , after_pattern[ 0 ] ) {
6387+ if entry. name . starts_with ( '.' ) && !dotglob && !pattern_starts_with_dot {
6388+ continue ;
6389+ }
6390+ if self . glob_match_impl ( & entry. name , pat, nocase) {
63136391 matches. push ( dir. join ( & entry. name ) . to_string_lossy ( ) . to_string ( ) ) ;
63146392 }
63156393 }
0 commit comments