Add GlobWithExcludes to pathtools

Add GlobWithExcludes, which takes a pattern and a list of exclude
patterns, and returns all files that match the pattern but do not
match any exclude patterns.

Change-Id: I8b94b3ba5a37409071b475b9a4035f52f47863f1
diff --git a/pathtools/glob.go b/pathtools/glob.go
index 4d67e3f..239a3c7 100644
--- a/pathtools/glob.go
+++ b/pathtools/glob.go
@@ -30,6 +30,15 @@
 // extension that recursive glob (** matching zero or more complete path
 // entries) is supported.
 func Glob(pattern string) (matches, dirs []string, err error) {
+	return GlobWithExcludes(pattern, nil)
+}
+
+// GlobWithExcludes returns the list of files that match the given pattern but
+// do not match the given exclude patterns, along with the list of directories
+// that were searched to construct the file list.  The supported glob and
+// exclude patterns are equivalent to filepath.Glob, with an extension that
+// recursive glob (** matching zero or more complete path entries) is supported.
+func GlobWithExcludes(pattern string, excludes []string) (matches, dirs []string, err error) {
 	if !isWild(pattern) {
 		// If there are no wilds in the pattern, just return whether the file at the pattern
 		// exists or not.  Uses filepath.Glob instead of manually statting to get consistent
@@ -41,7 +50,16 @@
 		matches, dirs, err = glob(pattern, false)
 	}
 
-	return matches, dirs, err
+	if err != nil {
+		return nil, nil, err
+	}
+
+	matches, err = filterExcludes(matches, excludes)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return matches, dirs, nil
 }
 
 // glob is a recursive helper function to handle globbing each level of the pattern individually,
@@ -130,6 +148,110 @@
 	return dirs, err
 }
 
+// Filters the strings in matches based on the glob patterns in excludes.  Hierarchical (a/*) and
+// recursive (**) glob patterns are supported.
+func filterExcludes(matches []string, excludes []string) ([]string, error) {
+	if len(excludes) == 0 {
+		return matches, nil
+	}
+
+	var ret []string
+matchLoop:
+	for _, m := range matches {
+		for _, e := range excludes {
+			exclude, err := match(e, m)
+			if err != nil {
+				return nil, err
+			}
+			if exclude {
+				continue matchLoop
+			}
+		}
+		ret = append(ret, m)
+	}
+
+	return ret, nil
+}
+
+// match returns true if name matches pattern using the same rules as filepath.Match, but supporting
+// hierarchical patterns (a/*) and recursive globs (**).
+func match(pattern, name string) (bool, error) {
+	if filepath.Base(pattern) == "**" {
+		return false, GlobLastRecursiveErr
+	}
+
+	for {
+		var patternFile, nameFile string
+		pattern, patternFile = saneSplit(pattern)
+		name, nameFile = saneSplit(name)
+
+		if patternFile == "**" {
+			return matchPrefix(pattern, filepath.Join(name, nameFile))
+		}
+
+		if nameFile == "" && patternFile == "" {
+			return true, nil
+		} else if nameFile == "" || patternFile == "" {
+			return false, nil
+		}
+
+		match, err := filepath.Match(patternFile, nameFile)
+		if err != nil || !match {
+			return match, err
+		}
+	}
+}
+
+// matchPrefix returns true if the beginning of name matches pattern using the same rules as
+// filepath.Match, but supporting hierarchical patterns (a/*).  Recursive globs (**) are not
+// supported, they should have been handled in match().
+func matchPrefix(pattern, name string) (bool, error) {
+	if len(pattern) > 0 && pattern[0] == '/' {
+		if len(name) > 0 && name[0] == '/' {
+			pattern = pattern[1:]
+			name = name[1:]
+		} else {
+			return false, nil
+		}
+	}
+
+	for {
+		var patternElem, nameElem string
+		patternElem, pattern = saneSplitFirst(pattern)
+		nameElem, name = saneSplitFirst(name)
+
+		if patternElem == "." {
+			patternElem = ""
+		}
+		if nameElem == "." {
+			nameElem = ""
+		}
+
+		if patternElem == "**" {
+			return false, GlobMultipleRecursiveErr
+		}
+
+		if patternElem == "" {
+			return true, nil
+		} else if nameElem == "" {
+			return false, nil
+		}
+
+		match, err := filepath.Match(patternElem, nameElem)
+		if err != nil || !match {
+			return match, err
+		}
+	}
+}
+
+func saneSplitFirst(path string) (string, string) {
+	i := strings.IndexRune(path, filepath.Separator)
+	if i < 0 {
+		return path, ""
+	}
+	return path[:i], path[i+1:]
+}
+
 func GlobPatternList(patterns []string, prefix string) (globedList []string, depDirs []string, err error) {
 	var (
 		matches []string
diff --git a/pathtools/glob_test.go b/pathtools/glob_test.go
index d75b8a9..efd63b0 100644
--- a/pathtools/glob_test.go
+++ b/pathtools/glob_test.go
@@ -24,10 +24,11 @@
 var pwd, _ = os.Getwd()
 
 var globTestCases = []struct {
-	pattern string
-	matches []string
-	dirs    []string
-	err     error
+	pattern  string
+	matches  []string
+	excludes []string
+	dirs     []string
+	err      error
 }{
 	// Current directory tests
 	{
@@ -119,14 +120,14 @@
 
 	// clean tests
 	{
-		pattern:  "./c/*/*.ext",
-		matches:  []string{"c/f/f.ext", "c/g/g.ext"},
-		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+		pattern: "./c/*/*.ext",
+		matches: []string{"c/f/f.ext", "c/g/g.ext"},
+		dirs:    []string{"c", "c/f", "c/g", "c/h"},
 	},
 	{
-		pattern:  "c/../c/*/*.ext",
-		matches:  []string{"c/f/f.ext", "c/g/g.ext"},
-		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+		pattern: "c/../c/*/*.ext",
+		matches: []string{"c/f/f.ext", "c/g/g.ext"},
+		dirs:    []string{"c", "c/f", "c/g", "c/h"},
 	},
 
 	// recursive tests
@@ -193,15 +194,189 @@
 		pattern: "**/**",
 		err:     GlobLastRecursiveErr,
 	},
+
+	// exclude tests
+	{
+		pattern:  "*.ext",
+		excludes: []string{"d.ext"},
+		matches:  []string{"e.ext"},
+		dirs:     []string{"."},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"a/b"},
+		matches:  []string{"a/a", "b/a", "c/c", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"a/b", "c/c"},
+		matches:  []string{"a/a", "b/a", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"c/*", "*/a"},
+		matches:  []string{"a/b"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"*/*"},
+		matches:  nil,
+		dirs:     []string{".", "a", "b", "c"},
+	},
+
+	// absolute exclude tests
+	{
+		pattern:  filepath.Join(pwd, "testdata/c/*/*.ext"),
+		excludes: []string{filepath.Join(pwd, "testdata/c/*/f.ext")},
+		matches: []string{
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+	{
+		pattern:  filepath.Join(pwd, "testdata/c/*/*.ext"),
+		excludes: []string{filepath.Join(pwd, "testdata/c/f/*.ext")},
+		matches: []string{
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+
+	// recursive exclude tests
+	{
+		pattern:  "*.ext",
+		excludes: []string{"**/*.ext"},
+		matches:  nil,
+		dirs:     []string{"."},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"**/b"},
+		matches:  []string{"a/a", "b/a", "c/c", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "*/*",
+		excludes: []string{"a/**/*"},
+		matches:  []string{"b/a", "c/c", "c/f", "c/g", "c/h"},
+		dirs:     []string{".", "a", "b", "c"},
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/*"},
+		matches:  nil,
+		dirs:     []string{".", "a", "a/a", "a/b", "b", "c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "*/*/*",
+		excludes: []string{"a/**/a"},
+		matches:  []string{"a/b/b", "c/f/f.ext", "c/g/g.ext", "c/h/h"},
+		dirs:     []string{".", "a", "b", "c", "a/a", "a/b", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "*/*/*",
+		excludes: []string{"**/a"},
+		matches:  []string{"a/b/b", "c/f/f.ext", "c/g/g.ext", "c/h/h"},
+		dirs:     []string{".", "a", "b", "c", "a/a", "a/b", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "c/*/*.ext",
+		excludes: []string{"c/**/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+
+	// absoulte recursive exclude tests
+	{
+		pattern:  filepath.Join(pwd, "testdata/c/*/*.ext"),
+		excludes: []string{filepath.Join(pwd, "testdata/**/f.ext")},
+		matches: []string{
+			filepath.Join(pwd, "testdata/c/g/g.ext"),
+		},
+		dirs: []string{
+			filepath.Join(pwd, "testdata/c"),
+			filepath.Join(pwd, "testdata/c/f"),
+			filepath.Join(pwd, "testdata/c/g"),
+			filepath.Join(pwd, "testdata/c/h"),
+		},
+	},
+
+	// clean exclude tests
+	{
+		pattern:  "./c/*/*.ext",
+		excludes: []string{"./c/*/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "c/*/*.ext",
+		excludes: []string{"./c/*/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+	{
+		pattern:  "./c/*/*.ext",
+		excludes: []string{"c/*/f.ext"},
+		matches:  []string{"c/g/g.ext"},
+		dirs:     []string{"c", "c/f", "c/g", "c/h"},
+	},
+
+	// recursive exclude error tests
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/**/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"a/**/**/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/a/**/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/**/a/*"},
+		err:      GlobMultipleRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"a/**"},
+		err:      GlobLastRecursiveErr,
+	},
+	{
+		pattern:  "**/*",
+		excludes: []string{"**/**"},
+		err:      GlobLastRecursiveErr,
+	},
 }
 
 func TestGlob(t *testing.T) {
 	os.Chdir("testdata")
 	defer os.Chdir("..")
 	for _, testCase := range globTestCases {
-		matches, dirs, err := Glob(testCase.pattern)
+		matches, dirs, err := GlobWithExcludes(testCase.pattern, testCase.excludes)
 		if err != testCase.err {
 			t.Errorf(" pattern: %q", testCase.pattern)
+			if testCase.excludes != nil {
+				t.Errorf("excludes: %q", testCase.excludes)
+			}
 			t.Errorf("   error: %s", err)
 			continue
 		}
@@ -209,12 +384,18 @@
 		if !reflect.DeepEqual(matches, testCase.matches) {
 			t.Errorf("incorrect matches list:")
 			t.Errorf(" pattern: %q", testCase.pattern)
+			if testCase.excludes != nil {
+				t.Errorf("excludes: %q", testCase.excludes)
+			}
 			t.Errorf("     got: %#v", matches)
 			t.Errorf("expected: %#v", testCase.matches)
 		}
 		if !reflect.DeepEqual(dirs, testCase.dirs) {
 			t.Errorf("incorrect dirs list:")
 			t.Errorf(" pattern: %q", testCase.pattern)
+			if testCase.excludes != nil {
+				t.Errorf("excludes: %q", testCase.excludes)
+			}
 			t.Errorf("     got: %#v", dirs)
 			t.Errorf("expected: %#v", testCase.dirs)
 		}