88 "io"
99 "os"
1010 "regexp"
11+ "slices"
1112 "strings"
1213 "time"
1314
@@ -24,16 +25,36 @@ const (
2425 DefaultTimeout = 15 * time .Second
2526)
2627
28+ // Common error patterns for --std-errors flag
29+ var commonErrorPatterns = []string {
30+ "ERROR" ,
31+ "FATAL" ,
32+ "FAIL" ,
33+ "Exception" ,
34+ "panic" ,
35+ "Traceback.*most.recent.call" ,
36+ "command not found" ,
37+ "java.lang.*Exception" ,
38+ "segmentation fault" ,
39+ "org.jruby.exceptions" ,
40+ "Gem::MissingSpecError" ,
41+ "Permission denied" ,
42+ }
43+
2744type cfg struct {
28- Container string
29- Timeout time.Duration
30- IgnoreCase bool
31- Retry int
32- Patterns []string
33- InvertMatch bool
34-
35- compiled []* regexp.Regexp
36- highlighter func (string ) string
45+ Container string
46+ Timeout time.Duration
47+ IgnoreCase bool
48+ Retry int
49+ Patterns []string
50+ NotExpected []string
51+ NotExpectedExclude []string
52+ InvertMatch bool
53+ DefaultErrors bool
54+
55+ compiled []* regexp.Regexp
56+ notExpectedCompiled []* regexp.Regexp
57+ highlighter func (string ) string
3758}
3859
3960func Command () * cobra.Command {
@@ -55,7 +76,10 @@ func Command() *cobra.Command {
5576 cmd .Flags ().DurationVarP (& cfg .Timeout , "timeout" , "t" , DefaultTimeout , "time to wait for logs to appear" )
5677 cmd .Flags ().IntVarP (& cfg .Retry , "retry" , "r" , 3 , "number of times to retry a failed request" )
5778 cmd .Flags ().BoolVarP (& cfg .IgnoreCase , "ignore-case" , "i" , true , "toggle to ignore case for the match" )
58- cmd .Flags ().StringArrayVarP (& cfg .Patterns , "regexp" , "e" , nil , "regular expression to match" )
79+ cmd .Flags ().StringArrayVarP (& cfg .Patterns , "regexp" , "e" , nil , "regular expression to match (must be present)" )
80+ cmd .Flags ().StringArrayVar (& cfg .NotExpected , "ne" , nil , "regular expression that must NOT be present" )
81+ cmd .Flags ().StringArrayVar (& cfg .NotExpectedExclude , "ne-exclude" , nil , "exclude specific patterns from --std-errors (only works with --std-errors)" )
82+ cmd .Flags ().BoolVar (& cfg .DefaultErrors , "std-errors" , false , fmt .Sprintf ("check for %d standard error patterns" , len (commonErrorPatterns )))
5983 cmd .Flags ().BoolVarP (& cfg .InvertMatch , "invert-match" , "v" , false , "toggle to invert the match" )
6084
6185 return cmd
@@ -116,9 +140,9 @@ func (c *cfg) retryableRun(ctx context.Context) error {
116140
117141 matches := []match {}
118142 matchedPatterns := make (map [int ]bool )
143+ notExpectedMatches := []match {}
119144
120145 // Use stdcopy to properly handle Docker's multiplexed stream format
121-
122146 var stdoutBuf , stderrBuf bytes.Buffer
123147 if _ , err := stdcopy .StdCopy (& stdoutBuf , & stderrBuf , reader ); err != nil && err != io .EOF {
124148 return fmt .Errorf ("error reading container logs: %v" , err )
@@ -128,6 +152,8 @@ func (c *cfg) retryableRun(ctx context.Context) error {
128152 scanner := bufio .NewScanner (strings .NewReader (stdoutBuf .String ()))
129153 for scanner .Scan () {
130154 line := scanner .Text ()
155+
156+ // Check expected patterns
131157 for i , re := range c .compiled {
132158 if re .MatchString (line ) {
133159 matches = append (matches , match {
@@ -138,12 +164,24 @@ func (c *cfg) retryableRun(ctx context.Context) error {
138164 break
139165 }
140166 }
167+
168+ // Check not-expected patterns
169+ for _ , re := range c .notExpectedCompiled {
170+ if re .MatchString (line ) {
171+ notExpectedMatches = append (notExpectedMatches , match {
172+ Container : c .Container ,
173+ Text : re .ReplaceAllStringFunc (line , c .highlighter ),
174+ })
175+ }
176+ }
141177 }
142178
143179 // Process stderr
144180 scanner = bufio .NewScanner (strings .NewReader (stderrBuf .String ()))
145181 for scanner .Scan () {
146182 line := scanner .Text ()
183+
184+ // Check expected patterns
147185 for i , re := range c .compiled {
148186 if re .MatchString (line ) {
149187 matches = append (matches , match {
@@ -153,21 +191,45 @@ func (c *cfg) retryableRun(ctx context.Context) error {
153191 matchedPatterns [i ] = true
154192 }
155193 }
194+
195+ // Check not-expected patterns
196+ for _ , re := range c .notExpectedCompiled {
197+ if re .MatchString (line ) {
198+ notExpectedMatches = append (notExpectedMatches , match {
199+ Container : c .Container ,
200+ Text : re .ReplaceAllStringFunc (line , c .highlighter ),
201+ })
202+ }
203+ }
156204 }
157205
158206 // Print all matches at the end
159207 nmatches := len (matches )
160- clog .InfoContextf (ctx , "found %d matches in container %s" , nmatches , c .Container )
208+ nNotExpected := len (notExpectedMatches )
209+
210+ clog .InfoContextf (ctx , "found %d expected matches in container %s" , nmatches , c .Container )
161211 for i , m := range matches {
162- clog .InfoContextf (ctx , "-- [%d/%d] in %s: %s" , i + 1 , nmatches , m .Container , m .Text )
212+ clog .InfoContextf (ctx , "-- [%d/%d] expected in %s: %s" , i + 1 , nmatches , m .Container , m .Text )
213+ }
214+
215+ if nNotExpected > 0 {
216+ clog .InfoContextf (ctx , "found %d not-expected matches in container %s" , nNotExpected , c .Container )
217+ for i , m := range notExpectedMatches {
218+ clog .InfoContextf (ctx , "-- [%d/%d] not-expected in %s: %s" , i + 1 , nNotExpected , m .Container , m .Text )
219+ }
163220 }
164221
165222 if c .InvertMatch && nmatches > 0 {
166223 return fmt .Errorf ("found %d unwanted matches in container %s" , nmatches , c .Container )
167224 }
168225
169- if ! c .InvertMatch {
170- // Check if all patterns were matched
226+ // Fail if any not-expected patterns were found
227+ if nNotExpected > 0 {
228+ return fmt .Errorf ("found %d not-expected matches in container %s" , nNotExpected , c .Container )
229+ }
230+
231+ // Check if all expected patterns were matched (only if not using invert match)
232+ if ! c .InvertMatch && len (c .Patterns ) > 0 {
171233 if len (matchedPatterns ) < len (c .compiled ) {
172234 // Find which patterns were not matched
173235 var missingPatterns []string
@@ -176,7 +238,7 @@ func (c *cfg) retryableRun(ctx context.Context) error {
176238 missingPatterns = append (missingPatterns , pattern )
177239 }
178240 }
179- return fmt .Errorf ("no match found for pattern(s): %v" , missingPatterns )
241+ return fmt .Errorf ("no match found for expected pattern(s): %v" , missingPatterns )
180242 }
181243 }
182244
@@ -186,18 +248,57 @@ func (c *cfg) retryableRun(ctx context.Context) error {
186248func (c * cfg ) prerun (_ context.Context , args []string ) error {
187249 c .Container = args [0 ]
188250
189- if len (c .Patterns ) == 0 {
190- return fmt .Errorf ("expected at least one pattern via -e/--regexp" )
251+ // Validate --ne-exclude requires --std-errors
252+ if len (c .NotExpectedExclude ) > 0 && ! c .DefaultErrors {
253+ return fmt .Errorf ("--ne-exclude can only be used with --std-errors" )
254+ }
255+
256+ // Add default error patterns if --std-errors is specified
257+ if c .DefaultErrors {
258+ // Start with all default patterns
259+ patterns := make ([]string , len (commonErrorPatterns ))
260+ copy (patterns , commonErrorPatterns )
261+
262+ // Remove excluded patterns
263+ for _ , exclude := range c .NotExpectedExclude {
264+ filtered := []string {}
265+ for _ , pattern := range patterns {
266+ if pattern != exclude {
267+ filtered = append (filtered , pattern )
268+ }
269+ }
270+ patterns = filtered
271+ }
272+
273+ // Add defaults (after exclusions) to not-expected patterns
274+ c .NotExpected = append (c .NotExpected , patterns ... )
275+ }
276+
277+ if len (c .Patterns ) == 0 && len (c .NotExpected ) == 0 {
278+ return fmt .Errorf ("expected at least one pattern via -e/--regexp or --ne" )
279+ }
280+
281+ // Check for conflicting patterns (same pattern in both -e and --ne)
282+ for _ , expected := range c .Patterns {
283+ if slices .Contains (c .NotExpected , expected ) {
284+ return fmt .Errorf ("conflicting pattern '%s' found in both -e and --ne flags" , expected )
285+ }
191286 }
192287
193- // Compile all the patterns
194288 for _ , p := range c .Patterns {
195289 if c .IgnoreCase {
196290 p = "(?i)" + p
197291 }
198292 c .compiled = append (c .compiled , regexp .MustCompile (p ))
199293 }
200294
295+ for _ , p := range c .NotExpected {
296+ if c .IgnoreCase {
297+ p = "(?i)" + p
298+ }
299+ c .notExpectedCompiled = append (c .notExpectedCompiled , regexp .MustCompile (p ))
300+ }
301+
201302 c .highlighter = func (s string ) string {
202303 if isatty .IsTerminal (os .Stdout .Fd ()) {
203304 return "\x1b [32;1m" + s + "\x1b [0m"
0 commit comments