Very early work in progress
This commit is contained in:
parent
6a248b29cb
commit
2bbfc3bc30
5 changed files with 349 additions and 4 deletions
300
main.go
Normal file
300
main.go
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
gg is a recursive grep written in Go, with some shortcuts for everyday use.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/logrusorgru/aurora/v4"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// TODO:
|
||||
// - option to squelch aurora output
|
||||
// - detect if terminal
|
||||
// - binary file detection
|
||||
// - long-line / minified-file detection
|
||||
// - ignore files by extension (or glob?)
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "gg pattern [path1 [path2 …]]",
|
||||
Short: "gg is a recursive grep",
|
||||
Long: `gg is a recursive grep. Given a regexp (or fixed pattern) it will search for
|
||||
the pattern recursively in the current working directory. It will print a
|
||||
coloured header per file along with the matching line and pattern.
|
||||
|
||||
It is possible to scan specific files or directories, rather than the default
|
||||
current working directory. To do this, simply specify the path(s) as arguments
|
||||
following the pattern.
|
||||
|
||||
It is possible to scan for multiple patterns using the -e (or -Q) argument,
|
||||
which can be repeated multiple times. -e specifies a regular expression and
|
||||
-Q a fixed pattern. When using either flag, any non-flag arguments are treated
|
||||
as paths to scan.
|
||||
|
||||
Search defaults to case-sensitive but the -I flag may be passed to make regular
|
||||
expression searches case-insensitive. Alternatively, the "(?i)" construct may be
|
||||
added to a regular expression to make that specific expression case insensitive.
|
||||
Fixed pattern matches are always case-sensitive.`,
|
||||
|
||||
RunE: run,
|
||||
}
|
||||
|
||||
var (
|
||||
searchRegexp []string
|
||||
regexps []*regexp.Regexp
|
||||
searchFixed []string
|
||||
searchBytes [][]byte
|
||||
searchPath []string
|
||||
ignoreList []string
|
||||
ignoreMap map[string]struct{}
|
||||
ignoreCase bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().StringSliceVarP(&searchRegexp, "grep", "e", nil, "pattern to match (regular expression)")
|
||||
rootCmd.Flags().StringSliceVarP(&searchFixed, "fixed", "Q", nil, "pattern to match (fixed string)")
|
||||
rootCmd.Flags().StringSliceVarP(&ignoreList, "ignore", "i", []string{".git"}, "files/directories to ignore")
|
||||
rootCmd.Flags().BoolVarP(&ignoreCase, "ignore-case", "I", false, "make all searches case insensitive")
|
||||
}
|
||||
|
||||
func run(c *cobra.Command, args []string) error {
|
||||
if len(searchRegexp) == 0 && len(searchFixed) == 0 {
|
||||
if len(args) == 0 {
|
||||
return errors.New("no pattern specified")
|
||||
}
|
||||
searchRegexp = append(searchRegexp, args[0])
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
searchPath = args
|
||||
if len(searchPath) == 0 {
|
||||
searchPath = append(searchPath, ".")
|
||||
}
|
||||
|
||||
ignoreMap = make(map[string]struct{}, len(ignoreList))
|
||||
for _, i := range ignoreList {
|
||||
ignoreMap[i] = struct{}{}
|
||||
}
|
||||
|
||||
for _, r := range searchRegexp {
|
||||
if ignoreCase {
|
||||
r = "(?i)" + r
|
||||
}
|
||||
re, err := regexp.Compile(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
regexps = append(regexps, re)
|
||||
}
|
||||
|
||||
for _, s := range searchFixed {
|
||||
searchBytes = append(searchBytes, []byte(s))
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, path := range searchPath {
|
||||
if err := search(path); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func recurse(path string) error {
|
||||
d, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, de := range d {
|
||||
name := de.Name()
|
||||
fullPath := filepath.Join(path, name)
|
||||
|
||||
if _, ignored := ignoreMap[name]; ignored {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := search(fullPath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func search(path string) error {
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case st.IsDir():
|
||||
return recurse(path)
|
||||
|
||||
case !st.Mode().IsRegular(),
|
||||
st.Size() == 0:
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fullData, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_PRIVATE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Munmap(fullData)
|
||||
|
||||
var printedHeader bool
|
||||
|
||||
var (
|
||||
data = fullData
|
||||
lineNum int
|
||||
b, b2 strings.Builder
|
||||
)
|
||||
for len(data) > 0 {
|
||||
eol := bytes.IndexByte(data, '\n')
|
||||
lineNum++
|
||||
var line []byte
|
||||
if eol == -1 {
|
||||
line = data
|
||||
data = nil
|
||||
} else {
|
||||
line = data[:eol]
|
||||
data = data[eol+1:]
|
||||
}
|
||||
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
loc := matches(line)
|
||||
if loc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !printedHeader {
|
||||
printedHeader = true
|
||||
fmt.Println(aurora.BgBlue(aurora.Bold(path)))
|
||||
}
|
||||
|
||||
b.Reset()
|
||||
fmt.Fprintf(&b, "%4d: ", aurora.Gray(16, lineNum))
|
||||
|
||||
if loc[0] < 128 {
|
||||
escape(&b, line[0:loc[0]])
|
||||
} else {
|
||||
start := loc[0] - 128
|
||||
for i := 0; i < 5; i++ {
|
||||
if utf8.RuneStart(line[start]) {
|
||||
break
|
||||
}
|
||||
start++
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, "(%d bytes)%s", start, aurora.Magenta("…"))
|
||||
escape(&b, line[start:loc[0]])
|
||||
}
|
||||
|
||||
if loc[1]-loc[0] < 128 {
|
||||
b2.Reset()
|
||||
escape(&b2, line[loc[0]:loc[1]])
|
||||
b.WriteString(aurora.Bold(b2.String()).String())
|
||||
|
||||
if loc[1]+128 > len(line) {
|
||||
escape(&b, line[loc[1]:])
|
||||
} else {
|
||||
end := loc[1] + 128
|
||||
for i := 0; i < 5; i++ {
|
||||
if utf8.RuneStart(line[end]) {
|
||||
break
|
||||
}
|
||||
end--
|
||||
}
|
||||
escape(&b, line[loc[1]:end])
|
||||
fmt.Fprintf(&b, "(%d bytes)%s", len(line)-end, aurora.Magenta("…"))
|
||||
}
|
||||
|
||||
} else {
|
||||
end := loc[1]
|
||||
for i := 0; i < 5; i++ {
|
||||
if utf8.RuneStart(line[end]) {
|
||||
break
|
||||
}
|
||||
end--
|
||||
}
|
||||
|
||||
b2.Reset()
|
||||
escape(&b2, line[loc[0]:end])
|
||||
b.WriteString(aurora.Bold(b2.String()).String())
|
||||
fmt.Fprintf(&b, "%s(%d bytes)", aurora.Magenta("…"), len(line)-end)
|
||||
|
||||
}
|
||||
|
||||
b.WriteRune('\n')
|
||||
fmt.Print(b.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func matches(data []byte) (loc []int) {
|
||||
for _, re := range regexps {
|
||||
loc := re.FindIndex(data)
|
||||
if loc != nil {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
for _, s := range searchBytes {
|
||||
pos := bytes.Index(data, s)
|
||||
if pos != -1 {
|
||||
return []int{pos, pos + len(s)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func escape(b *strings.Builder, s []byte) {
|
||||
for len(s) > 0 {
|
||||
r, size := utf8.DecodeRune(s)
|
||||
s = s[size:]
|
||||
|
||||
switch {
|
||||
case r == utf8.RuneError && size == 1:
|
||||
b.WriteString(aurora.Magenta("\uFFFD").String())
|
||||
|
||||
case r == '\r':
|
||||
b.WriteString(aurora.Magenta(`\r`).String())
|
||||
|
||||
case r == '\t':
|
||||
b.WriteString(aurora.Magenta(`\t`).String())
|
||||
|
||||
case unicode.IsPrint(r):
|
||||
b.WriteRune(r)
|
||||
|
||||
default:
|
||||
b.WriteString(aurora.Magenta(".").String())
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue