Skip to content

Commit 7d8d0d6

Browse files
rahulduvediDentrax
andauthored
Add dual pattern matching to kgrep/dgrep with --ne and --default flags (#180)
* Add dual pattern matching to kgrep/dgrep: --ne for forbidden patterns Signed-off-by: Rahul Duvedi <53002164+rahulduvedi@users.noreply.github.com> Co-authored-by: Furkan Türkal <furkan.turkal@hotmail.com>
1 parent 07edc2b commit 7d8d0d6

File tree

4 files changed

+246
-32
lines changed

4 files changed

+246
-32
lines changed

tw/pkg/commands/dgrep/dgrep.go

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
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+
2744
type 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

3960
func 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 {
186248
func (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

Comments
 (0)