Rewrite pathtools.Glob to track partially-matched directories

The directory structure:
  a/
    a
  b/
    b
With the glob pattern */a would previously return []string{"a"} for
dirs, but it needs to return []string{"a", "b"} in order to re-run
the generator if a file called "a" is created inside b/.

Rewrite Glob to manually recurse through path elements, only calling
filepath.Glob for a pattern with wilds in the last element of the
path, and add the globbed directory to the dirs list each time.

Also add tests and test data for pathtools.Glob.

Change-Id: Ibbdb2f99809ea0826d4fa82066cf84103005ef57
This commit is contained in:
Colin Cross 2015-04-23 13:29:28 -07:00
parent 63d5d4d9e4
commit 4b793e5798
11 changed files with 186 additions and 23 deletions

View file

@ -15,6 +15,7 @@
package pathtools
import (
"os"
"path/filepath"
"strings"
)
@ -22,38 +23,44 @@ import (
// 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.
func Glob(pattern string) (matches, dirs []string, err error) {
matches, err = filepath.Glob(pattern)
if err != nil {
return nil, nil, err
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))
return matches, dirs, err
}
wildIndices := wildElements(pattern)
if len(wildIndices) > 0 {
for _, match := range matches {
dir := filepath.Dir(match)
dirElems := strings.Split(dir, string(filepath.Separator))
for _, index := range wildIndices {
dirs = append(dirs, strings.Join(dirElems[:index],
string(filepath.Separator)))
dir, file := saneSplit(pattern)
dirMatches, dirs, err := Glob(dir)
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
}
matches = append(matches, newMatches...)
}
}
return
return matches, dirs, nil
}
func wildElements(pattern string) []int {
elems := strings.Split(pattern, string(filepath.Separator))
var result []int
for i, elem := range elems {
if isWild(elem) {
result = append(result, i)
}
// Faster version of dir, file := filepath.Dir(path), filepath.File(path)
// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
// not "/"
func saneSplit(path string) (dir, file string) {
dir, file = filepath.Split(path)
switch dir {
case "":
dir = "."
case "/":
// Nothing
default:
dir = dir[:len(dir)-1]
}
return result
return dir, file
}
func isWild(pattern string) bool {

156
pathtools/glob_test.go Normal file
View file

@ -0,0 +1,156 @@
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pathtools
import (
"os"
"path/filepath"
"reflect"
"testing"
)
var pwd, _ = os.Getwd()
var globTestCases = []struct {
pattern string
matches []string
dirs []string
}{
// Current directory tests
{
pattern: "*",
matches: []string{"a", "b", "c", "d.ext", "e.ext"},
dirs: []string{"."},
},
{
pattern: "*.ext",
matches: []string{"d.ext", "e.ext"},
dirs: []string{"."},
},
{
pattern: "*/a",
matches: []string{"a/a", "b/a"},
dirs: []string{".", "a", "b", "c"},
},
{
pattern: "*/*/a",
matches: []string{"a/a/a"},
dirs: []string{".", "a", "b", "c", "a/a", "a/b", "c/f", "c/g", "c/h"},
},
{
pattern: "*/a/a",
matches: []string{"a/a/a"},
dirs: []string{".", "a", "b", "c", "a/a"},
},
// ./ directory tests
{
pattern: "./*",
matches: []string{"a", "b", "c", "d.ext", "e.ext"},
dirs: []string{"."},
},
{
pattern: "./*.ext",
matches: []string{"d.ext", "e.ext"},
dirs: []string{"."},
},
{
pattern: "./*/a",
matches: []string{"a/a", "b/a"},
dirs: []string{".", "a", "b", "c"},
},
{
pattern: "./[ac]/a",
matches: []string{"a/a"},
dirs: []string{".", "a", "c"},
},
// subdirectory tests
{
pattern: "c/*/*.ext",
matches: []string{"c/f/f.ext", "c/g/g.ext"},
dirs: []string{"c", "c/f", "c/g", "c/h"},
},
{
pattern: "a/*/a",
matches: []string{"a/a/a"},
dirs: []string{"a", "a/a", "a/b"},
},
// absolute tests
{
pattern: filepath.Join(pwd, "testdata/c/*/*.ext"),
matches: []string{
filepath.Join(pwd, "testdata/c/f/f.ext"),
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"),
},
},
// no-wild tests
{
pattern: "a",
matches: []string{"a"},
dirs: nil,
},
{
pattern: "a/a",
matches: []string{"a/a"},
dirs: nil,
},
// 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/../c/*/*.ext",
matches: []string{"c/f/f.ext", "c/g/g.ext"},
dirs: []string{"c", "c/f", "c/g", "c/h"},
},
}
func TestGlob(t *testing.T) {
os.Chdir("testdata")
defer os.Chdir("..")
for _, testCase := range globTestCases {
matches, dirs, err := Glob(testCase.pattern)
if err != nil {
t.Errorf(" pattern: %q", testCase.pattern)
t.Errorf(" error: %s", err.Error())
continue
}
if !reflect.DeepEqual(matches, testCase.matches) {
t.Errorf("incorrect matches list:")
t.Errorf(" pattern: %q", testCase.pattern)
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)
t.Errorf(" got: %#v", dirs)
t.Errorf("expected: %#v", testCase.dirs)
}
}
}

0
pathtools/testdata/a/a/a vendored Normal file
View file

0
pathtools/testdata/a/b/b vendored Normal file
View file

0
pathtools/testdata/b/a vendored Normal file
View file

0
pathtools/testdata/c/c vendored Normal file
View file

0
pathtools/testdata/c/f/f.ext vendored Normal file
View file

0
pathtools/testdata/c/g/g.ext vendored Normal file
View file

0
pathtools/testdata/c/h/h vendored Normal file
View file

0
pathtools/testdata/d.ext vendored Normal file
View file

0
pathtools/testdata/e.ext vendored Normal file
View file