Add recursive glob support to pathtools.Glob
Recursive globs are supported by passing ** in any single non-final path element. For example, path/**/*.java will find all files named *.java under "path". Change-Id: Ifebd76f8959289f7d0d378504053c1c6b88cdeed
This commit is contained in:
parent
4b793e5798
commit
7bac3c6cf2
2 changed files with 139 additions and 10 deletions
|
@ -15,14 +15,38 @@
|
|||
package pathtools
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var GlobMultipleRecursiveErr = errors.New("pattern contains multiple **")
|
||||
var GlobLastRecursiveErr = errors.New("pattern ** as last path element")
|
||||
|
||||
// Glob returns the list of files that match the given pattern along with the
|
||||
// list of directories that were searched to construct the file list.
|
||||
// The supported glob patterns are equivalent to filepath.Glob, with an
|
||||
// extension that recursive glob (** matching zero or more complete path
|
||||
// entries) is supported.
|
||||
func Glob(pattern 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
|
||||
// results.
|
||||
matches, err = filepath.Glob(filepath.Clean(pattern))
|
||||
} else if filepath.Base(pattern) == "**" {
|
||||
return nil, nil, GlobLastRecursiveErr
|
||||
} else {
|
||||
matches, dirs, err = glob(pattern, false)
|
||||
}
|
||||
|
||||
return matches, dirs, err
|
||||
}
|
||||
|
||||
// glob is a recursive helper function to handle globbing each level of the pattern individually,
|
||||
// allowing searched directories to be tracked. Also handles the recursive glob pattern, **.
|
||||
func glob(pattern string, hasRecursive bool) (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
|
||||
|
@ -32,25 +56,48 @@ func Glob(pattern string) (matches, dirs []string, err error) {
|
|||
}
|
||||
|
||||
dir, file := saneSplit(pattern)
|
||||
dirMatches, dirs, err := Glob(dir)
|
||||
|
||||
if file == "**" {
|
||||
if hasRecursive {
|
||||
return matches, dirs, GlobMultipleRecursiveErr
|
||||
}
|
||||
hasRecursive = true
|
||||
}
|
||||
|
||||
dirMatches, dirs, err := glob(dir, hasRecursive)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, m := range dirMatches {
|
||||
if info, _ := os.Stat(m); info.IsDir() {
|
||||
dirs = append(dirs, m)
|
||||
newMatches, err := filepath.Glob(filepath.Join(m, file))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
if file == "**" {
|
||||
recurseDirs, err := walkAllDirs(m)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
matches = append(matches, recurseDirs...)
|
||||
} else {
|
||||
dirs = append(dirs, m)
|
||||
newMatches, err := filepath.Glob(filepath.Join(m, file))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
matches = append(matches, newMatches...)
|
||||
}
|
||||
matches = append(matches, newMatches...)
|
||||
}
|
||||
}
|
||||
|
||||
return matches, dirs, nil
|
||||
}
|
||||
|
||||
// Faster version of dir, file := filepath.Dir(path), filepath.File(path)
|
||||
// Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations
|
||||
// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
|
||||
// not "/"
|
||||
// not "/". Returns ".", "" if path is "."
|
||||
func saneSplit(path string) (dir, file string) {
|
||||
if path == "." {
|
||||
return ".", ""
|
||||
}
|
||||
dir, file = filepath.Split(path)
|
||||
switch dir {
|
||||
case "":
|
||||
|
@ -67,6 +114,22 @@ func isWild(pattern string) bool {
|
|||
return strings.ContainsAny(pattern, "*?[")
|
||||
}
|
||||
|
||||
// Returns a list of all directories under dir
|
||||
func walkAllDirs(dir string) (dirs []string, err error) {
|
||||
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Mode().IsDir() {
|
||||
dirs = append(dirs, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return dirs, err
|
||||
}
|
||||
|
||||
func GlobPatternList(patterns []string, prefix string) (globedList []string, depDirs []string, err error) {
|
||||
var (
|
||||
matches []string
|
||||
|
|
|
@ -27,6 +27,7 @@ var globTestCases = []struct {
|
|||
pattern string
|
||||
matches []string
|
||||
dirs []string
|
||||
err error
|
||||
}{
|
||||
// Current directory tests
|
||||
{
|
||||
|
@ -127,6 +128,71 @@ var globTestCases = []struct {
|
|||
matches: []string{"c/f/f.ext", "c/g/g.ext"},
|
||||
dirs: []string{"c", "c/f", "c/g", "c/h"},
|
||||
},
|
||||
|
||||
// recursive tests
|
||||
{
|
||||
pattern: "**/a",
|
||||
matches: []string{"a", "a/a", "a/a/a", "b/a"},
|
||||
dirs: []string{".", "a", "a/a", "a/b", "b", "c", "c/f", "c/g", "c/h"},
|
||||
},
|
||||
{
|
||||
pattern: "a/**/a",
|
||||
matches: []string{"a/a", "a/a/a"},
|
||||
dirs: []string{"a", "a/a", "a/b"},
|
||||
},
|
||||
{
|
||||
pattern: "a/**/*",
|
||||
matches: []string{"a/a", "a/b", "a/a/a", "a/b/b"},
|
||||
dirs: []string{"a", "a/a", "a/b"},
|
||||
},
|
||||
|
||||
// absolute recursive tests
|
||||
{
|
||||
pattern: filepath.Join(pwd, "testdata/**/*.ext"),
|
||||
matches: []string{
|
||||
filepath.Join(pwd, "testdata/d.ext"),
|
||||
filepath.Join(pwd, "testdata/e.ext"),
|
||||
filepath.Join(pwd, "testdata/c/f/f.ext"),
|
||||
filepath.Join(pwd, "testdata/c/g/g.ext"),
|
||||
},
|
||||
dirs: []string{
|
||||
filepath.Join(pwd, "testdata"),
|
||||
filepath.Join(pwd, "testdata/a"),
|
||||
filepath.Join(pwd, "testdata/a/a"),
|
||||
filepath.Join(pwd, "testdata/a/b"),
|
||||
filepath.Join(pwd, "testdata/b"),
|
||||
filepath.Join(pwd, "testdata/c"),
|
||||
filepath.Join(pwd, "testdata/c/f"),
|
||||
filepath.Join(pwd, "testdata/c/g"),
|
||||
filepath.Join(pwd, "testdata/c/h"),
|
||||
},
|
||||
},
|
||||
|
||||
// recursive error tests
|
||||
{
|
||||
pattern: "**/**/*",
|
||||
err: GlobMultipleRecursiveErr,
|
||||
},
|
||||
{
|
||||
pattern: "a/**/**/*",
|
||||
err: GlobMultipleRecursiveErr,
|
||||
},
|
||||
{
|
||||
pattern: "**/a/**/*",
|
||||
err: GlobMultipleRecursiveErr,
|
||||
},
|
||||
{
|
||||
pattern: "**/**/a/*",
|
||||
err: GlobMultipleRecursiveErr,
|
||||
},
|
||||
{
|
||||
pattern: "a/**",
|
||||
err: GlobLastRecursiveErr,
|
||||
},
|
||||
{
|
||||
pattern: "**/**",
|
||||
err: GlobLastRecursiveErr,
|
||||
},
|
||||
}
|
||||
|
||||
func TestGlob(t *testing.T) {
|
||||
|
@ -134,9 +200,9 @@ func TestGlob(t *testing.T) {
|
|||
defer os.Chdir("..")
|
||||
for _, testCase := range globTestCases {
|
||||
matches, dirs, err := Glob(testCase.pattern)
|
||||
if err != nil {
|
||||
if err != testCase.err {
|
||||
t.Errorf(" pattern: %q", testCase.pattern)
|
||||
t.Errorf(" error: %s", err.Error())
|
||||
t.Errorf(" error: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue