Skip to content

Commit e53428b

Browse files
committed
csearch, cgrep: add -A, -B, -C, -V (cgrep only) flags
Also start on HTML output for a possible web interface. cmd/cgrep: add -v flag
1 parent fad9cf8 commit e53428b

3 files changed

Lines changed: 191 additions & 17 deletions

File tree

cmd/cgrep/cgrep.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import (
1414
"github.com/google/codesearch/regexp"
1515
)
1616

17-
var usageMessage = `usage: cgrep [-c] [-h] [-i] [-l] [-n] regexp [file...]
17+
var usageMessage = `usage: cgrep [-c] [-h] [-i] [-l] [-n] [-v] regexp [file...]
1818
1919
Cgrep behaves like grep, searching for regexp, an RE2 (nearly PCRE) regular expression.
2020
21-
The -c, -h, -i, -l, and -n flags are as in grep, although note that as per Go's
21+
The -c, -h, -i, -l, -n, and -v flags are as in grep, although note that as per Go's
2222
flag parsing convention, they cannot be combined: the option pair -i -n
2323
cannot be abbreviated to -in.
2424
`
@@ -37,6 +37,7 @@ func main() {
3737
log.SetPrefix("cgrep: ")
3838
var g regexp.Grep
3939
g.AddFlags()
40+
g.AddVFlag()
4041
g.Stdout = os.Stdout
4142
g.Stderr = os.Stderr
4243
flag.Usage = usage

cmd/csearch/csearch.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package main
66

77
import (
88
"archive/zip"
9+
"bytes"
910
"flag"
1011
"fmt"
1112
"log"
@@ -141,6 +142,10 @@ func Main() {
141142

142143
for _, fileid := range post {
143144
name := ix.Name(fileid).String()
145+
if g.L && (pat == "(?m)" || pat == "(?i)(?m)") {
146+
g.Reader(bytes.NewReader(nil), name)
147+
continue
148+
}
144149
file, err := os.Open(string(name))
145150
if err != nil {
146151
if i := strings.Index(name, ".zip#"); i >= 0 {

regexp/match.go

Lines changed: 183 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import (
99
"encoding/binary"
1010
"flag"
1111
"fmt"
12+
"html"
1213
"io"
1314
"os"
1415
"regexp/syntax"
1516
"sort"
17+
"strconv"
18+
"strings"
1619

1720
"github.com/google/codesearch/sparse"
1821
)
@@ -338,9 +341,9 @@ func (m *matcher) matchString(b string, beginText, endText bool) (end int) {
338341

339342
// isWordByte reports whether the byte c is a word character: ASCII only.
340343
// This is used to implement \b and \B. This is not right for Unicode, but:
341-
// - it's hard to get right in a byte-at-a-time matching world
342-
// (the DFA has only one-byte lookahead)
343-
// - this crude approximation is the same one PCRE uses
344+
// - it's hard to get right in a byte-at-a-time matching world
345+
// (the DFA has only one-byte lookahead)
346+
// - this crude approximation is the same one PCRE uses
344347
func isWordByte(c int) bool {
345348
return 'A' <= c && c <= 'Z' ||
346349
'a' <= c && c <= 'z' ||
@@ -358,8 +361,16 @@ type Grep struct {
358361
C bool // C flag - print count of matches
359362
N bool // N flag - print line numbers
360363
H bool // H flag - do not print file names
364+
V bool // V flag - print non-matching lines (only for cgrep, not csearch)
361365

362-
Match bool
366+
HTML bool // emit HTML output for csweb
367+
Match bool // were any matches found?
368+
Matches int // how many matches were found?
369+
Limit int // stop after this many matches
370+
Limited bool // stopped because of limit
371+
372+
PreContext int // number of lines to print after
373+
PostContext int // number of lines to print before
363374

364375
buf []byte
365376
}
@@ -369,12 +380,34 @@ func (g *Grep) AddFlags() {
369380
flag.BoolVar(&g.C, "c", false, "print match counts only")
370381
flag.BoolVar(&g.N, "n", false, "show line numbers")
371382
flag.BoolVar(&g.H, "h", false, "omit file names")
383+
flag.IntVar(&g.PreContext, "B", 0, "show `n` lines before match")
384+
flag.IntVar(&g.PostContext, "A", 0, "show `n` lines after match")
385+
flag.Func("C", "show `n` lines before and after match", func(s string) error {
386+
n, err := strconv.Atoi(s)
387+
if err != nil {
388+
return err
389+
}
390+
g.PreContext = n
391+
g.PostContext = n
392+
return nil
393+
})
394+
}
395+
396+
func (g *Grep) AddVFlag() {
397+
flag.BoolVar(&g.V, "v", false, "show non-matching lines")
398+
}
399+
400+
func (g *Grep) esc(s string) string {
401+
if g.HTML {
402+
return html.EscapeString(s)
403+
}
404+
return s
372405
}
373406

374407
func (g *Grep) File(name string) {
375408
f, err := os.Open(name)
376409
if err != nil {
377-
fmt.Fprintf(g.Stderr, "%s\n", err)
410+
fmt.Fprintf(g.Stderr, "%s\n", g.esc(err.Error()))
378411
return
379412
}
380413
defer f.Close()
@@ -402,7 +435,7 @@ func (g *Grep) Reader(r io.Reader, name string) {
402435
}
403436
var (
404437
buf = g.buf[:0]
405-
needLineno = g.N
438+
needLineno = g.N || g.HTML
406439
lineno = 1
407440
count = 0
408441
prefix = ""
@@ -412,28 +445,40 @@ func (g *Grep) Reader(r io.Reader, name string) {
412445
if !g.H {
413446
prefix = name + ":"
414447
}
448+
chunkStart := 0
415449
for {
416450
n, err := io.ReadFull(r, buf[len(buf):cap(buf)])
417451
buf = buf[:len(buf)+n]
418452
end := len(buf)
419453
if err == nil {
420-
i := bytes.LastIndex(buf, nl)
421-
if i >= 0 {
422-
end = i + 1
454+
// Stop scan before trailing fragment of a line;
455+
// also stop before g.PostContext whole lines,
456+
// so we know we'll have the context we need to print.
457+
d := lineSuffixLen(buf, g.PostContext+1)
458+
if d < len(buf) {
459+
end = len(buf) - d
423460
}
424461
} else {
425462
endText = true
426463
}
427-
chunkStart := 0
428464
for chunkStart < end {
429465
m1 := g.Regexp.Match(buf[chunkStart:end], beginText, endText) + chunkStart
430466
beginText = false
431467
if m1 < chunkStart {
432468
break
433469
}
434470
g.Match = true
471+
if g.Limit > 0 && g.Matches >= g.Limit {
472+
g.Limited = true
473+
return
474+
}
475+
g.Matches++
435476
if g.L {
436-
fmt.Fprintf(g.Stdout, "%s\n", name)
477+
if g.HTML {
478+
fmt.Fprintf(g.Stdout, "<a href=\"show/%s\">%s</a>\n", g.esc(name), g.esc(name))
479+
} else {
480+
fmt.Fprintf(g.Stdout, "%s\n", name)
481+
}
437482
return
438483
}
439484
lineStart := bytes.LastIndex(buf[chunkStart:m1], nl) + 1 + chunkStart
@@ -452,6 +497,18 @@ func (g *Grep) Reader(r io.Reader, name string) {
452497
switch {
453498
case g.C:
454499
count++
500+
case g.PreContext+g.PostContext > 0:
501+
fmt.Fprintf(g.Stdout, "%s%d:\n", prefix, lineno)
502+
before, match, after := lineContext(g.PreContext, g.PostContext, buf, lineStart, lineEnd)
503+
for _, line := range before {
504+
fmt.Fprintf(g.Stdout, "\t\t%s\n", line)
505+
}
506+
fmt.Fprintf(g.Stdout, "\t>>\t%s\n", match)
507+
for _, line := range after {
508+
fmt.Fprintf(g.Stdout, "\t\t%s\n", line)
509+
}
510+
case g.HTML:
511+
fmt.Fprintf(g.Stdout, "<a href=\"/show/%s?q=%s#L%d\">%s:%d</a>:%s%s", g.esc(strings.ReplaceAll(name, "#", ">")), g.esc(g.Regexp.String()), lineno, g.esc(name), lineno, g.esc(string(line)), nl)
455512
case g.N:
456513
fmt.Fprintf(g.Stdout, "%s%d:%s%s", prefix, lineno, line, nl)
457514
default:
@@ -465,16 +522,127 @@ func (g *Grep) Reader(r io.Reader, name string) {
465522
if needLineno && err == nil {
466523
lineno += countNL(buf[chunkStart:end])
467524
}
468-
n = copy(buf, buf[end:])
525+
// Slide pre-context and unprocessed bytes down to start of buffer.
526+
d := lineSuffixLen(buf[:end], g.PreContext)
527+
if d == end {
528+
// Not enough room; give up on context.
529+
d = 0
530+
}
531+
n = copy(buf, buf[end-d:])
469532
buf = buf[:n]
470-
if len(buf) == 0 && err != nil {
533+
chunkStart = d
534+
if endText && err != nil {
471535
if err != io.EOF && err != io.ErrUnexpectedEOF {
472-
fmt.Fprintf(g.Stderr, "%s: %v\n", name, err)
536+
fmt.Fprintf(g.Stderr, "%s: %v\n", g.esc(name), err)
473537
}
474538
break
475539
}
476540
}
477541
if g.C && count > 0 {
478-
fmt.Fprintf(g.Stdout, "%s: %d\n", name, count)
542+
if g.HTML {
543+
fmt.Fprintf(g.Stdout, "<a href=\"show/%s?q=%s\">%s</a>: %d\n", g.esc(name), g.esc(g.Regexp.String()), g.esc(name), count)
544+
} else {
545+
fmt.Fprintf(g.Stdout, "%s: %d\n", name, count)
546+
}
547+
}
548+
}
549+
550+
func lineSuffixLen(buf []byte, n int) int {
551+
end := len(buf)
552+
for i := 0; i < n; i++ {
553+
j := bytes.LastIndex(buf[:end], nl)
554+
if j < 0 {
555+
break
556+
}
557+
end = j
558+
}
559+
if j := bytes.LastIndex(buf[:end], nl); j >= 0 {
560+
return len(buf) - (j + 1)
561+
}
562+
return len(buf)
563+
}
564+
565+
func linePrefixLen(buf []byte, lines int) int {
566+
start := 0
567+
for i := 0; i < lines; i++ {
568+
j := bytes.IndexByte(buf[start:], '\n')
569+
if j < 0 {
570+
return len(buf)
571+
}
572+
start += j + 1
573+
}
574+
return start
575+
}
576+
577+
func lineContext(numBefore, numAfter int, buf []byte, lineStart, lineEnd int) (before [][]byte, line []byte, after [][]byte) {
578+
beforeChunk := buf[lineStart-lineSuffixLen(buf[:lineStart], numBefore) : lineStart]
579+
afterChunk := buf[lineEnd : lineEnd+linePrefixLen(buf[lineEnd:], numAfter)]
580+
581+
line = chomp(buf[lineStart:lineEnd])
582+
before = bytes.SplitAfter(beforeChunk, nl)
583+
if len(before[len(before)-1]) == 0 {
584+
before = before[:len(before)-1]
585+
}
586+
for i := range before {
587+
before[i] = chomp(before[i])
588+
}
589+
after = bytes.Split(afterChunk, nl)
590+
if len(after[len(after)-1]) == 0 {
591+
after = after[:len(after)-1]
592+
}
593+
for i := range after {
594+
after[i] = chomp(after[i])
595+
}
596+
597+
var prefix []byte
598+
prefix = updatePrefix(prefix, line)
599+
for _, l := range before {
600+
prefix = updatePrefix(prefix, l)
601+
}
602+
for _, l := range after {
603+
prefix = updatePrefix(prefix, l)
604+
}
605+
606+
line = cutPrefix(line, prefix)
607+
for i, l := range before {
608+
before[i] = cutPrefix(l, prefix)
609+
}
610+
for i, l := range after {
611+
after[i] = cutPrefix(l, prefix)
612+
}
613+
return
614+
}
615+
616+
func updatePrefix(prefix, line []byte) []byte {
617+
if prefix == nil {
618+
i := 0
619+
for i < len(line) && (line[i] == ' ' || line[i] == '\t') {
620+
i++
621+
}
622+
return line[:i]
623+
}
624+
625+
i := 0
626+
for i < len(line) && i < len(prefix) && line[i] == prefix[i] {
627+
i++
628+
}
629+
if i >= len(line) {
630+
return prefix
631+
}
632+
return prefix[:i]
633+
}
634+
635+
func cutPrefix(line, prefix []byte) []byte {
636+
if len(prefix) > len(line) {
637+
return nil
638+
}
639+
return line[len(prefix):]
640+
}
641+
642+
func chomp(s []byte) []byte {
643+
i := len(s)
644+
for i > 0 && (s[i-1] == ' ' || s[i-1] == '\t' || s[i-1] == '\r' || s[i-1] == '\n') {
645+
i--
479646
}
647+
return s[:i]
480648
}

0 commit comments

Comments
 (0)