182 lines
4.5 KiB
Go
182 lines
4.5 KiB
Go
/*
|
|
gg is a recursive grep written in Go, with some shortcuts for everyday use.
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
|
|
"github.com/bmatcuk/doublestar/v4"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// TODO:
|
|
// - it would be better to make fixed patterns case insensitive too.
|
|
// - configurable defaults for exclude.
|
|
|
|
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.
|
|
|
|
Files and directories can be excluded with the -x option. This supports bash-style
|
|
globs with '*', '?', '[a-z]', '{this,that}', or '/**/' to match zero or more
|
|
directories. By default, .git and vim swap files are ignored.`,
|
|
|
|
RunE: run,
|
|
}
|
|
|
|
var (
|
|
searchRegexp []string
|
|
regexps []*regexp.Regexp
|
|
searchFixed []string
|
|
searchBytes [][]byte
|
|
searchPath []string
|
|
excludeList []string
|
|
binaryFile notPlainTextFlag
|
|
minifiedFile notPlainTextFlag
|
|
ignoreCase bool
|
|
noColour bool
|
|
display *Display
|
|
printedFull 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(&excludeList, "exclude", "x", []string{".git", ".*.swp"}, "files/directories to exclude")
|
|
rootCmd.Flags().BoolVarP(&ignoreCase, "ignore-case", "i", false, "make all searches case insensitive")
|
|
rootCmd.Flags().BoolVarP(&noColour, "no-colour", "C", false, "disable colour output")
|
|
rootCmd.Flags().Var(&binaryFile, "binary", "what to do with binary files")
|
|
rootCmd.Flags().Var(&minifiedFile, "minified", "what to do with minified text files")
|
|
}
|
|
|
|
func run(c *cobra.Command, args []string) error {
|
|
display = NewDisplay(noColour)
|
|
|
|
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, ".")
|
|
}
|
|
|
|
for _, x := range excludeList {
|
|
if !doublestar.ValidatePattern(x) {
|
|
return fmt.Errorf("invalid exclude pattern %q", x)
|
|
}
|
|
}
|
|
|
|
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
|
|
NextFile:
|
|
for _, de := range d {
|
|
name := de.Name()
|
|
fullPath := filepath.Join(path, name)
|
|
|
|
for _, x := range excludeList {
|
|
if exclude, _ := doublestar.Match(x, fullPath); exclude {
|
|
continue NextFile
|
|
}
|
|
}
|
|
|
|
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()
|
|
data, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_PRIVATE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Munmap(data)
|
|
|
|
file(path, data)
|
|
return nil
|
|
}
|