summaryrefslogtreecommitdiff
path: root/internal/io/fs/grepprocessor.go
blob: 57c4c2f92aa2dbf4178a9a551934f731687afd20 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package fs

import (
	"context"
	"fmt"

	"github.com/mimecast/dtail/internal/color/brush"
	"github.com/mimecast/dtail/internal/protocol"
	"github.com/mimecast/dtail/internal/regex"
)

// GrepProcessor handles grep-style filtering
type GrepProcessor struct {
	regex    regex.Regex
	plain    bool
	noColor  bool
	hostname string

	// Context handling
	beforeContext int
	afterContext  int
	maxCount      int

	// State for context processing
	matchCount     int
	afterRemaining int
	beforeBuffer   [][]byte
	beforeLineNums []int
}

// NewGrepProcessor creates a new grep processor
func NewGrepProcessor(re regex.Regex, plain, noColor bool, hostname string, beforeContext, afterContext, maxCount int) *GrepProcessor {
	gp := &GrepProcessor{
		regex:          re,
		plain:          plain,
		noColor:        noColor,
		hostname:       hostname,
		beforeContext:  beforeContext,
		afterContext:   afterContext,
		maxCount:       maxCount,
		matchCount:     0,
		afterRemaining: 0,
	}

	if beforeContext > 0 {
		gp.beforeBuffer = make([][]byte, 0, beforeContext)
		gp.beforeLineNums = make([]int, 0, beforeContext)
	}

	return gp
}

func (gp *GrepProcessor) Initialize(ctx context.Context) error {
	return nil
}

func (gp *GrepProcessor) Cleanup() error {
	return nil
}

// ProcessLine processes a single line for grep filtering with context support.
// Returns formatted output for matching lines and their context, or nil for non-matching lines.
// Handles before/after context lines and respects maxCount limit.
func (gp *GrepProcessor) ProcessLine(line []byte, lineNum int, filePath string, stats *stats, sourceID string) ([]byte, bool) {
	isMatch := gp.regex.Match(line)

	// Handle lines that don't match the regex
	if !isMatch {
		// Handle after context lines (only for non-matching lines)
		if gp.afterRemaining > 0 {
			gp.afterRemaining--
			// Send this line as context
			return gp.formatLine(line, lineNum, filePath, stats, sourceID), true
		}
		// If we have before context, buffer this line
		if gp.beforeContext > 0 {
			// Make a copy of the line for buffering
			lineCopy := make([]byte, len(line))
			copy(lineCopy, line)

			// Add to buffer, removing oldest if at capacity
			if len(gp.beforeBuffer) >= gp.beforeContext {
				gp.beforeBuffer = gp.beforeBuffer[1:]
				gp.beforeLineNums = gp.beforeLineNums[1:]
			}
			gp.beforeBuffer = append(gp.beforeBuffer, lineCopy)
			gp.beforeLineNums = append(gp.beforeLineNums, lineNum)
		}
		return nil, false
	}

	// Line matches the regex
	gp.matchCount++

	// Check if we've reached maxCount
	if gp.maxCount > 0 && gp.matchCount > gp.maxCount {
		return nil, false
	}

	// Stats will be updated by DirectProcessor when the line is actually sent

	// Build result with before context, current line, and set up after context
	var result []byte

	// First, output any before context lines
	if gp.beforeContext > 0 {
		for i, beforeLine := range gp.beforeBuffer {
			beforeLineNum := gp.beforeLineNums[i]
			formatted := gp.formatLine(beforeLine, beforeLineNum, filePath, stats, sourceID)
			result = append(result, formatted...)
		}
		// Clear the buffer since we've used it
		gp.beforeBuffer = gp.beforeBuffer[:0]
		gp.beforeLineNums = gp.beforeLineNums[:0]
	}

	// Add the matching line
	formatted := gp.formatLine(line, lineNum, filePath, stats, sourceID)
	result = append(result, formatted...)

	// Set up after context (only if we're not already in after context mode)
	if gp.afterContext > 0 && gp.afterRemaining == 0 {
		gp.afterRemaining = gp.afterContext
	}

	return result, true
}

func (gp *GrepProcessor) Flush() []byte {
	return nil
}

// formatLine formats a line for output (shared by matching lines and context lines)
func (gp *GrepProcessor) formatLine(line []byte, lineNum int, filePath string, stats *stats, sourceID string) []byte {
	// Format output to match existing behavior
	if gp.plain {
		// If line already ends with a line ending, preserve it as-is
		// Otherwise, add LF for consistency with bufio.Scanner behavior
		if len(line) > 0 && (line[len(line)-1] == '\n' || (len(line) > 1 && line[len(line)-2] == '\r' && line[len(line)-1] == '\n')) {
			// Line already has line ending, preserve it exactly
			result := make([]byte, len(line))
			copy(result, line)
			return result
		} else {
			// Line doesn't have line ending, add LF
			result := make([]byte, len(line)+1)
			copy(result, line)
			result[len(line)] = '\n'
			return result
		}
	}

	// Format exactly like original basehandler.go for non-plain mode
	// REMOTE|{hostname}|{TransmittedPerc}|{Count}|{SourceID}|{Content}¬
	var transmittedPerc int
	var count uint64
	if stats != nil {
		transmittedPerc = stats.transmittedPerc()
		count = stats.totalLineCount()
	}

	// Build the protocol line
	protocolLine := fmt.Sprintf("REMOTE%s%s%s%3d%s%v%s%s%s%s",
		protocol.FieldDelimiter, gp.hostname, protocol.FieldDelimiter,
		transmittedPerc, protocol.FieldDelimiter, count, protocol.FieldDelimiter,
		sourceID, protocol.FieldDelimiter, string(line))

	// Apply ANSI color formatting if not in plain mode and not noColor mode.
	if !gp.plain && !gp.noColor {
		colorized := brush.Colorfy(protocolLine)
		result := make([]byte, len(colorized)+1)
		copy(result, colorized)
		result[len(colorized)] = '\n'
		return result
	}

	// No color formatting
	result := make([]byte, len(protocolLine)+1)
	copy(result, protocolLine)
	result[len(protocolLine)] = '\n'

	return result
}