diff --git a/README.md b/README.md index ec8fb94..f59e793 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Each group features a set of equivalent clauses. Each source should be a relative or absolute path to a directory on your machine. -Source paths may include environment variables (e.g. `$GOPATH`) or tildes (`~`). Use a hyphen (`-`) to exclude a directory. +Source paths may include environment variables (e.g. `$GOPATH`) or tildes (`~`). Use a hyphen (`-`) to exclude a directory. Source paths also support usage of [glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)). In the case that a directory begins with a hypgen (e.g. `-foo`), use the following to include it as a source: @@ -114,11 +114,11 @@ In the case that a directory begins with a hypgen (e.g. `-foo`), use the followi ``` ```console ->>> ... FROM ., ~/Desktop ... +>>> ... FROM ~/Desktop, ./*/**.go ... ``` ```console ->>> ... FROM ~/Desktop, $GOPATH, -.git/ ... +>>> ... FROM $GOPATH, -.git/ ... ``` ### Condition diff --git a/query/excluder.go b/query/excluder.go index bb3a581..01105df 100644 --- a/query/excluder.go +++ b/query/excluder.go @@ -8,19 +8,19 @@ import ( // Excluder allows us to support different methods of excluding in the future. type Excluder interface { - ShouldExclude(path string) bool + shouldExclude(path string) bool } -// RegexpExclude uses regular expressions to tell if a file/path should be +// regexpExclude uses regular expressions to tell if a file/path should be // excluded. -type RegexpExclude struct { +type regexpExclude struct { exclusions []string regex *regexp.Regexp } // ShouldExclude will return a boolean denoting whether or not the path should // be excluded based on the given slice of exclusions. -func (r *RegexpExclude) ShouldExclude(path string) bool { +func (r *regexpExclude) shouldExclude(path string) bool { if r.regex == nil { r.buildRegex() } @@ -31,7 +31,7 @@ func (r *RegexpExclude) ShouldExclude(path string) bool { } // buildRegex builds the regular expression for this RegexpExclude. -func (r *RegexpExclude) buildRegex() { +func (r *regexpExclude) buildRegex() { exclusions := make([]string, len(r.exclusions)) for i, exclusion := range r.exclusions { // Wrap exclusion in ^ and (/.*)?$ AFTER trimming trailing slashes and diff --git a/query/excluder_test.go b/query/excluder_test.go index 809afde..61f24dc 100644 --- a/query/excluder_test.go +++ b/query/excluder_test.go @@ -9,7 +9,7 @@ type ExcluderCase struct { func TestShouldExclude_ExpectAllExcluded(t *testing.T) { exclusions := []string{".git", ".gitignore"} - excluder := RegexpExclude{exclusions: exclusions} + excluder := regexpExclude{exclusions: exclusions} cases := []ExcluderCase{ {".git", true}, {".git/", true}, @@ -18,7 +18,7 @@ func TestShouldExclude_ExpectAllExcluded(t *testing.T) { } for _, c := range cases { - actual := excluder.ShouldExclude(c.input) + actual := excluder.shouldExclude(c.input) if actual != c.expected { t.Fatalf("\nExpected %v\n Got %v", c.expected, actual) } @@ -27,7 +27,7 @@ func TestShouldExclude_ExpectAllExcluded(t *testing.T) { func TestShouldExclude_ExpectNotExcluded(t *testing.T) { exclusions := []string{".git"} - excluder := RegexpExclude{exclusions: exclusions} + excluder := regexpExclude{exclusions: exclusions} cases := []ExcluderCase{ {".git", true}, {".git/", true}, @@ -36,7 +36,7 @@ func TestShouldExclude_ExpectNotExcluded(t *testing.T) { } for _, c := range cases { - actual := excluder.ShouldExclude(c.input) + actual := excluder.shouldExclude(c.input) if actual != c.expected { t.Fatalf("\nExpected %v\n Got %v", c.expected, actual) } diff --git a/query/query.go b/query/query.go index 43d38cc..c69d19c 100644 --- a/query/query.go +++ b/query/query.go @@ -3,6 +3,7 @@ package query import ( "os" "path/filepath" + "strings" ) // Query represents an input query. @@ -44,42 +45,68 @@ func (q *Query) HasAttribute(attributes ...string) bool { // evaluating the condition tree for each file. This method calls workFunc on // each "successful" file. func (q *Query) Execute(workFunc interface{}) error { - seen := make(map[string]bool) - excluder := &RegexpExclude{exclusions: q.Sources["exclude"]} + seen := map[string]bool{} + excluder := ®expExclude{exclusions: q.Sources["exclude"]} for _, src := range q.Sources["include"] { - err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + // TODO: Improve our method of detecting if src is a glob pattern. This + // currently doesn't support usage of square brackets, since the tokenizer + // doesn't recognize these as part of a directory. + // + // Pattern reference: https://golang.org/pkg/path/filepath/#Match. + if strings.ContainsAny(src, "*?") { + // If src does _resemble_ a glob pattern, we find all matches and + // evaluate the condition tree against each. + matches, err := filepath.Glob(src) if err != nil { return err } - if path == "." { - return nil + for _, match := range matches { + if err = filepath.Walk(match, q.walkFunc(seen, excluder, workFunc)); err != nil { + return err + } } + continue + } - // Avoid walking a single directory more than once. - if _, ok := seen[path]; ok { - return nil - } - seen[path] = true + if err := filepath.Walk(src, q.walkFunc(seen, excluder, workFunc)); err != nil { + return err + } + } - if excluder.ShouldExclude(path) { - return nil - } + return nil +} - if !q.ConditionTree.evaluateTree(path, info) { - return nil - } +// walkFunc returns a filepath.WalkFunc which evaluates the condition tree +// against the given file. +func (q *Query) walkFunc(seen map[string]bool, excluder Excluder, + workFunc interface{}) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } - results := q.applyModifiers(path, info) - workFunc.(func(string, os.FileInfo, map[string]interface{}))(path, info, results) + if path == "." { return nil - }) + } - if err != nil { - return err + // Avoid walking a single directory more than once. + if _, ok := seen[path]; ok { + return nil } - } + seen[path] = true - return nil + if excluder.shouldExclude(path) { + return nil + } + + if !q.ConditionTree.evaluateTree(path, info) { + return nil + } + + results := q.applyModifiers(path, info) + workFunc.(func(string, os.FileInfo, map[string]interface{}))(path, info, results) + return nil + } }