Add -I include option, and some usage examples

This commit is contained in:
Laurence Withers 2023-07-07 11:49:54 +01:00
parent eacffb4fe1
commit f6cd64cf3b
1 changed files with 78 additions and 17 deletions

95
main.go
View File

@ -47,9 +47,25 @@ to a regular expression to make that specific expression case insensitive.
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. Symlinks named on
the command line are followed, but by default symlinks are not followed when
recursing into directories. -L allows them to be dereferenced.`,
directories. By default, .git and vim swap files are ignored. Similarly, -I
filters files to include. Examples:
# ignore files/dirs with .js or .css suffix
gg -x '*.js' -x '*.css' pattern
# only match files with .go suffix (any subdir)
gg -I '*.go' pattern
# only match files whose parent dir is "stuff", but ignore "foo" subdir
gg -x ./foo -I 'stuff/*' pattern
# only match .js files with a directory "things" in the path, but ignore
# .min.js (e.g. will match "foo/things/bar/my.js")
gg -I 'things/**/*.js' -x '*.min.js' pattern
Symlinks named on the command line are followed, but by default symlinks are
not followed when recursing into directories. -L allows them to be
dereferenced.`,
RunE: run,
}
@ -60,6 +76,7 @@ var (
searchFixed []string
searchPath []string
excludeList []string
includeList []string
ignoreCase bool
noColour bool
binaryFile notPlainTextFlag
@ -78,6 +95,7 @@ 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().StringSliceVarP(&includeList, "include", "I", nil, "files/directories to include")
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")
@ -86,12 +104,10 @@ func init() {
}
func run(c *cobra.Command, args []string) error {
// if we got past argument passing, then returned errors are runtime
// things (like file not found) that shouldn't trigger a usage message.
c.SilenceUsage = true
display = NewDisplay(noColour)
// if no -e or -Q flag is passed, then the first arg is taken to be
// the pattern to match
if len(searchRegexp) == 0 && len(searchFixed) == 0 {
if len(args) == 0 {
return errors.New("no pattern specified")
@ -100,11 +116,18 @@ func run(c *cobra.Command, args []string) error {
args = args[1:]
}
// remaining arguments are treated as search paths; an empty list is
// taken to mean the CWD
searchPath = args
if len(searchPath) == 0 {
searchPath = append(searchPath, ".")
}
// if we got past argument passing, then returned errors are runtime
// things (like file not found) that shouldn't trigger a usage message.
c.SilenceUsage = true
// for -x and -I, an undecorated pattern is treated as a suffix match
for i, x := range excludeList {
if !strings.HasPrefix(x, "**/") && !strings.HasPrefix(x, "./") {
x = "**/" + x
@ -114,7 +137,17 @@ func run(c *cobra.Command, args []string) error {
return fmt.Errorf("invalid exclude pattern %q", x)
}
}
for i, x := range includeList {
if !strings.HasPrefix(x, "**/") && !strings.HasPrefix(x, "./") {
x = "**/" + x
includeList[i] = x
}
if !doublestar.ValidatePattern(x) {
return fmt.Errorf("invalid include pattern %q", x)
}
}
// compile regular expressions for matching
for _, r := range searchRegexp {
if ignoreCase {
r = "(?i)" + r
@ -125,7 +158,6 @@ func run(c *cobra.Command, args []string) error {
}
regexps = append(regexps, re)
}
for _, r := range searchFixed {
r = regexp.QuoteMeta(r)
if ignoreCase {
@ -137,6 +169,7 @@ func run(c *cobra.Command, args []string) error {
regexps = append(regexps, re)
}
// search over named paths
var errs []error
for _, path := range searchPath {
if err := search(path, true); err != nil {
@ -154,17 +187,11 @@ func recurse(path string) error {
}
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
}
fullPath := filepath.Join(path, de.Name())
if !shouldSearch(fullPath, de.IsDir()) {
continue
}
if err := search(fullPath, followSymlinks); err != nil {
errs = append(errs, err)
}
@ -172,6 +199,40 @@ NextFile:
return errors.Join(errs...)
}
// shouldSearch matches the full path of the file against the include and
// exclude lists, returning true if we should consider the file/directory for
// searching and false if not.
func shouldSearch(fullPath string, isDir bool) bool {
// process the exclude list first
for _, x := range excludeList {
if exclude, _ := doublestar.Match(x, fullPath); exclude {
return false
}
}
// if the include list is empty, everything is included
if len(includeList) == 0 {
return true
}
for _, x := range includeList {
match, _ := doublestar.Match(x, fullPath)
fmt.Printf("[DEBUG] x=%q fullPath=%q isDir=%t match=%t\n", x, fullPath, isDir, match)
// if it's a directory, and we have at least one recursive
// matcher, then search
if isDir && strings.HasPrefix(x, "**/") {
return true
}
if include, _ := doublestar.Match(x, fullPath); include {
return true
}
}
return false
}
func search(path string, deref bool) error {
var (
st os.FileInfo