Add -I include option, and some usage examples
This commit is contained in:
parent
eacffb4fe1
commit
f6cd64cf3b
95
main.go
95
main.go
|
@ -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
|
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
|
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
|
directories. By default, .git and vim swap files are ignored. Similarly, -I
|
||||||
the command line are followed, but by default symlinks are not followed when
|
filters files to include. Examples:
|
||||||
recursing into directories. -L allows them to be dereferenced.`,
|
|
||||||
|
# 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,
|
RunE: run,
|
||||||
}
|
}
|
||||||
|
@ -60,6 +76,7 @@ var (
|
||||||
searchFixed []string
|
searchFixed []string
|
||||||
searchPath []string
|
searchPath []string
|
||||||
excludeList []string
|
excludeList []string
|
||||||
|
includeList []string
|
||||||
ignoreCase bool
|
ignoreCase bool
|
||||||
noColour bool
|
noColour bool
|
||||||
binaryFile notPlainTextFlag
|
binaryFile notPlainTextFlag
|
||||||
|
@ -78,6 +95,7 @@ func init() {
|
||||||
rootCmd.Flags().StringSliceVarP(&searchRegexp, "grep", "e", nil, "pattern to match (regular expression)")
|
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(&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(&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(&ignoreCase, "ignore-case", "i", false, "make all searches case insensitive")
|
||||||
rootCmd.Flags().BoolVarP(&noColour, "no-colour", "C", false, "disable colour output")
|
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(&binaryFile, "binary", "what to do with binary files")
|
||||||
|
@ -86,12 +104,10 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(c *cobra.Command, args []string) error {
|
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)
|
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(searchRegexp) == 0 && len(searchFixed) == 0 {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return errors.New("no pattern specified")
|
return errors.New("no pattern specified")
|
||||||
|
@ -100,11 +116,18 @@ func run(c *cobra.Command, args []string) error {
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remaining arguments are treated as search paths; an empty list is
|
||||||
|
// taken to mean the CWD
|
||||||
searchPath = args
|
searchPath = args
|
||||||
if len(searchPath) == 0 {
|
if len(searchPath) == 0 {
|
||||||
searchPath = append(searchPath, ".")
|
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 {
|
for i, x := range excludeList {
|
||||||
if !strings.HasPrefix(x, "**/") && !strings.HasPrefix(x, "./") {
|
if !strings.HasPrefix(x, "**/") && !strings.HasPrefix(x, "./") {
|
||||||
x = "**/" + x
|
x = "**/" + x
|
||||||
|
@ -114,7 +137,17 @@ func run(c *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("invalid exclude pattern %q", x)
|
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 {
|
for _, r := range searchRegexp {
|
||||||
if ignoreCase {
|
if ignoreCase {
|
||||||
r = "(?i)" + r
|
r = "(?i)" + r
|
||||||
|
@ -125,7 +158,6 @@ func run(c *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
regexps = append(regexps, re)
|
regexps = append(regexps, re)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range searchFixed {
|
for _, r := range searchFixed {
|
||||||
r = regexp.QuoteMeta(r)
|
r = regexp.QuoteMeta(r)
|
||||||
if ignoreCase {
|
if ignoreCase {
|
||||||
|
@ -137,6 +169,7 @@ func run(c *cobra.Command, args []string) error {
|
||||||
regexps = append(regexps, re)
|
regexps = append(regexps, re)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// search over named paths
|
||||||
var errs []error
|
var errs []error
|
||||||
for _, path := range searchPath {
|
for _, path := range searchPath {
|
||||||
if err := search(path, true); err != nil {
|
if err := search(path, true); err != nil {
|
||||||
|
@ -154,17 +187,11 @@ func recurse(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
NextFile:
|
|
||||||
for _, de := range d {
|
for _, de := range d {
|
||||||
name := de.Name()
|
fullPath := filepath.Join(path, de.Name())
|
||||||
fullPath := filepath.Join(path, name)
|
if !shouldSearch(fullPath, de.IsDir()) {
|
||||||
|
continue
|
||||||
for _, x := range excludeList {
|
|
||||||
if exclude, _ := doublestar.Match(x, fullPath); exclude {
|
|
||||||
continue NextFile
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := search(fullPath, followSymlinks); err != nil {
|
if err := search(fullPath, followSymlinks); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
@ -172,6 +199,40 @@ NextFile:
|
||||||
return errors.Join(errs...)
|
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 {
|
func search(path string, deref bool) error {
|
||||||
var (
|
var (
|
||||||
st os.FileInfo
|
st os.FileInfo
|
||||||
|
|
Loading…
Reference in New Issue