summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-17 10:42:51 +0300
committerPaul Buetow <paul@buetow.org>2025-06-17 10:42:51 +0300
commit069dff93d8c6c8f0ba28d9f6123fa9b1430c2f92 (patch)
treef85d97709bef1d1737011874944cf580b3213cd3
parent0a53b4e3352532e17522461b338d469d85210056 (diff)
Fix environment variable consistency and implement grep context lines support
- Changed DTAIL_USE_CHANNELLESS to use 'yes' instead of 'true' for consistency - Added support for --before, --after, and --max context options in channelless GrepProcessor - Implemented before context buffering and after context counting - Fixed consecutive match handling to avoid duplicate before context output - Context lines implementation matches original channel-based behavior structure - Still debugging after context line count issue in TestDGrepContext1 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--go.mod2
-rw-r--r--go.sum14
-rw-r--r--internal/io/fs/directprocessor.go120
-rw-r--r--internal/server/handlers/readcommand.go12
4 files changed, 126 insertions, 22 deletions
diff --git a/go.mod b/go.mod
index edef243..11f4515 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,8 @@ require (
)
require (
+ golang.org/x/lint v0.0.0-20241112194109-818c5a804067 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.12.0 // indirect
+ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 // indirect
)
diff --git a/go.sum b/go.sum
index f0d2329..9545fe0 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,22 @@
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/lint v0.0.0-20241112194109-818c5a804067 h1:adDmSQyFTCiv19j015EGKJBoaa7ElV0Q1Wovb/4G7NA=
+golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/io/fs/directprocessor.go b/internal/io/fs/directprocessor.go
index 72d58f0..006934f 100644
--- a/internal/io/fs/directprocessor.go
+++ b/internal/io/fs/directprocessor.go
@@ -310,16 +310,39 @@ type GrepProcessor struct {
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) *GrepProcessor {
- return &GrepProcessor{
- regex: re,
- plain: plain,
- noColor: noColor,
- hostname: hostname,
+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 {
@@ -331,7 +354,42 @@ func (gp *GrepProcessor) Cleanup() error {
}
func (gp *GrepProcessor) ProcessLine(line []byte, lineNum int, filePath string, stats *stats, sourceID string) ([]byte, bool) {
- if !gp.regex.Match(line) {
+ isMatch := gp.regex.Match(line)
+
+ // Handle after context lines
+ if gp.afterRemaining > 0 {
+ gp.afterRemaining--
+ // Send this line as context even if it doesn't match
+ if stats != nil {
+ stats.updateLineMatched() // Count context lines as transmitted
+ }
+ return gp.formatLine(line, lineNum, filePath, stats, sourceID), true
+ }
+
+ // Handle lines that don't match the regex
+ if !isMatch {
+ // 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
}
@@ -340,12 +398,45 @@ func (gp *GrepProcessor) ProcessLine(line []byte, lineNum int, filePath string,
stats.updateLineMatched()
}
+ // 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 {
result := make([]byte, len(line)+1)
copy(result, line)
result[len(line)] = '\n'
- return result, true
+ return result
}
// Format exactly like original basehandler.go for non-plain mode
@@ -371,11 +462,7 @@ func (gp *GrepProcessor) ProcessLine(line []byte, lineNum int, filePath string,
result = append(result, line...)
result = append(result, '\n')
- return result, true
-}
-
-func (gp *GrepProcessor) Flush() []byte {
- return nil
+ return result
}
// CatProcessor handles cat-style output
@@ -435,8 +522,8 @@ func (cp *CatProcessor) ProcessLine(line []byte, lineNum int, filePath string, s
transmittedPerc, protocol.FieldDelimiter, count, protocol.FieldDelimiter,
sourceID, protocol.FieldDelimiter, string(line))
- // Apply ANSI color formatting if not in noColor mode
- if !cp.noColor {
+ // Apply ANSI color formatting if not in plain mode and not noColor mode
+ if !cp.plain && !cp.noColor {
colorized := brush.Colorfy(protocolLine)
// Add color reset prefix for all lines except the first
@@ -467,7 +554,8 @@ func (cp *CatProcessor) ProcessLine(line []byte, lineNum int, filePath string, s
func (cp *CatProcessor) Flush() []byte {
// Add final color reset line to match original behavior (no trailing newline)
- if !cp.noColor {
+ // Only in non-plain mode with colors enabled
+ if !cp.plain && !cp.noColor {
return []byte("\x1b[39m\x1b[49m\x1b[49m\x1b[39m")
}
return nil
diff --git a/internal/server/handlers/readcommand.go b/internal/server/handlers/readcommand.go
index cbc4b1e..17054df 100644
--- a/internal/server/handlers/readcommand.go
+++ b/internal/server/handlers/readcommand.go
@@ -48,7 +48,7 @@ func (r *readCommand) Start(ctx context.Context, ltx lcontext.LContext,
}
// Check if channelless mode is enabled
- useChannelless := os.Getenv("DTAIL_USE_CHANNELLESS") == "true"
+ useChannelless := os.Getenv("DTAIL_USE_CHANNELLESS") == "yes"
if useChannelless {
dlog.Server.Debug("Using channelless processing mode")
@@ -289,7 +289,7 @@ func (r *readCommand) readFilesChannelless(ctx context.Context, ltx lcontext.LCo
output := NewNetworkOutputWriter(nil, r.server.serverMessages, r.server.user)
// Create appropriate processor based on mode
- processor := r.createChannellessProcessor(re)
+ processor := r.createChannellessProcessor(re, ltx)
// Process each file
for _, path := range paths {
@@ -321,7 +321,7 @@ func (r *readCommand) readChannellessStdin(ctx context.Context, ltx lcontext.LCo
output := NewNetworkOutputWriter(nil, r.server.serverMessages, r.server.user)
// Create appropriate processor based on mode
- processor := r.createChannellessProcessor(re)
+ processor := r.createChannellessProcessor(re, ltx)
// Create direct processor with "-" as globID for stdin
directProcessor := fs.NewDirectProcessor(processor, output, "-", ltx)
@@ -336,14 +336,14 @@ func (r *readCommand) readChannellessStdin(ctx context.Context, ltx lcontext.LCo
}
// createChannellessProcessor creates the appropriate processor based on command mode
-func (r *readCommand) createChannellessProcessor(re regex.Regex) fs.LineProcessor {
+func (r *readCommand) createChannellessProcessor(re regex.Regex, ltx lcontext.LContext) fs.LineProcessor {
hostname := r.server.hostname // Use server hostname
plain := r.server.plain // Use actual plain mode from server
noColor := false // Enable colors by default in channelless mode
switch r.mode {
case omode.GrepClient:
- return fs.NewGrepProcessor(re, plain, noColor, hostname)
+ return fs.NewGrepProcessor(re, plain, noColor, hostname, ltx.BeforeContext, ltx.AfterContext, ltx.MaxCount)
case omode.CatClient:
return fs.NewCatProcessor(plain, noColor, hostname)
case omode.TailClient:
@@ -353,6 +353,6 @@ func (r *readCommand) createChannellessProcessor(re regex.Regex) fs.LineProcesso
return fs.NewMapProcessor(plain, hostname)
default:
// Default to grep behavior
- return fs.NewGrepProcessor(re, plain, noColor, hostname)
+ return fs.NewGrepProcessor(re, plain, noColor, hostname, ltx.BeforeContext, ltx.AfterContext, ltx.MaxCount)
}
}