From ee1bd8ee15a1306d1f9201acc41ef39cd9f99a1b Mon Sep 17 00:00:00 2001 From: Kevin Crawley Date: Sun, 18 Sep 2016 20:42:32 -0500 Subject: [PATCH] Added glob/match support to afero --- match.go | 110 ++++++++++++++++++++++++++++++ match_test.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 match.go create mode 100644 match_test.go diff --git a/match.go b/match.go new file mode 100644 index 0000000..08b3b7e --- /dev/null +++ b/match.go @@ -0,0 +1,110 @@ +// Copyright © 2014 Steve Francia . +// Copyright 2009 The Go Authors. 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 afero + +import ( + "path/filepath" + "sort" + "strings" +) + +// Glob returns the names of all files matching pattern or nil +// if there is no matching file. The syntax of patterns is the same +// as in Match. The pattern may describe hierarchical names such as +// /usr/*/bin/ed (assuming the Separator is '/'). +// +// Glob ignores file system errors such as I/O errors reading directories. +// The only possible returned error is ErrBadPattern, when pattern +// is malformed. +// +// This was adapted from (http://golang.org/pkg/path/filepath) and uses several +// built-ins from that package. +func Glob(fs Fs, pattern string) (matches []string, err error) { + if !hasMeta(pattern) { + // afero does not support Lstat directly. + if _, err = lstatIfOs(fs, pattern); err != nil { + return nil, nil + } + return []string{pattern}, nil + } + + dir, file := filepath.Split(pattern) + switch dir { + case "": + dir = "." + case string(filepath.Separator): + // nothing + default: + dir = dir[0 : len(dir)-1] // chop off trailing separator + } + + if !hasMeta(dir) { + return glob(fs, dir, file, nil) + } + + var m []string + m, err = Glob(fs, dir) + if err != nil { + return + } + for _, d := range m { + matches, err = glob(fs, d, file, matches) + if err != nil { + return + } + } + return +} + +// glob searches for files matching pattern in the directory dir +// and appends them to matches. If the directory cannot be +// opened, it returns the existing matches. New matches are +// added in lexicographical order. +func glob(fs Fs, dir, pattern string, matches []string) (m []string, e error) { + m = matches + fi, err := fs.Stat(dir) + if err != nil { + return + } + if !fi.IsDir() { + return + } + d, err := fs.Open(dir) + if err != nil { + return + } + defer d.Close() + + names, _ := d.Readdirnames(-1) + sort.Strings(names) + + for _, n := range names { + matched, err := filepath.Match(pattern, n) + if err != nil { + return m, err + } + if matched { + m = append(m, filepath.Join(dir, n)) + } + } + return +} + +// hasMeta reports whether path contains any of the magic characters +// recognized by Match. +func hasMeta(path string) bool { + // TODO(niemeyer): Should other magic characters be added here? + return strings.IndexAny(path, "*?[") >= 0 +} diff --git a/match_test.go b/match_test.go new file mode 100644 index 0000000..21e1fae --- /dev/null +++ b/match_test.go @@ -0,0 +1,183 @@ +// Copyright © 2014 Steve Francia . +// Copyright 2009 The Go Authors. 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 afero + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +// contains returns true if vector contains the string s. +func contains(vector []string, s string) bool { + for _, elem := range vector { + if elem == s { + return true + } + } + return false +} + +func setupGlobDirRoot(t *testing.T, fs Fs) string { + path := testDir(fs) + setupGlobFiles(t, fs, path) + return path +} + +func setupGlobDirReusePath(t *testing.T, fs Fs, path string) string { + testRegistry[fs] = append(testRegistry[fs], path) + return setupGlobFiles(t, fs, path) +} + +func setupGlobFiles(t *testing.T, fs Fs, path string) string { + testSubDir := filepath.Join(path, "globs", "bobs") + err := fs.MkdirAll(testSubDir, 0700) + if err != nil && !os.IsExist(err) { + t.Fatal(err) + } + + f, err := fs.Create(filepath.Join(testSubDir, "/matcher")) + if err != nil { + t.Fatal(err) + } + f.WriteString("Testfile 1 content") + f.Close() + + f, err = fs.Create(filepath.Join(testSubDir, "/../submatcher")) + if err != nil { + t.Fatal(err) + } + f.WriteString("Testfile 2 content") + f.Close() + + f, err = fs.Create(filepath.Join(testSubDir, "/../../match")) + if err != nil { + t.Fatal(err) + } + f.WriteString("Testfile 3 content") + f.Close() + + return testSubDir +} + +func TestGlob(t *testing.T) { + defer removeAllTestFiles(t) + var testDir string + for i, fs := range Fss { + if i == 0 { + testDir = setupGlobDirRoot(t, fs) + } else { + setupGlobDirReusePath(t, fs, testDir) + } + } + + var globTests = []struct { + pattern, result string + }{ + {testDir + "/globs/bobs/matcher", testDir + "/globs/bobs/matcher"}, + {testDir + "/globs/*/mat?her", testDir + "/globs/bobs/matcher"}, + {testDir + "/globs/bobs/../*", testDir + "/globs/submatcher"}, + {testDir + "/match", testDir + "/match"}, + } + + for _, fs := range Fss { + + for _, tt := range globTests { + pattern := tt.pattern + result := tt.result + if runtime.GOOS == "windows" { + pattern = filepath.Clean(pattern) + result = filepath.Clean(result) + } + matches, err := Glob(fs, pattern) + if err != nil { + t.Errorf("Glob error for %q: %s", pattern, err) + continue + } + if !contains(matches, result) { + t.Errorf("Glob(%#q) = %#v want %v", pattern, matches, result) + } + } + for _, pattern := range []string{"no_match", "../*/no_match"} { + matches, err := Glob(fs, pattern) + if err != nil { + t.Errorf("Glob error for %q: %s", pattern, err) + continue + } + if len(matches) != 0 { + t.Errorf("Glob(%#q) = %#v want []", pattern, matches) + } + } + + } +} + +func TestGlobSymlink(t *testing.T) { + defer removeAllTestFiles(t) + + fs := &OsFs{} + testDir := setupGlobDirRoot(t, fs) + + err := os.Symlink("target", filepath.Join(testDir, "symlink")) + if err != nil { + t.Skipf("skipping on %s", runtime.GOOS) + } + + var globSymlinkTests = []struct { + path, dest string + brokenLink bool + }{ + {"test1", "link1", false}, + {"test2", "link2", true}, + } + + for _, tt := range globSymlinkTests { + path := filepath.Join(testDir, tt.path) + dest := filepath.Join(testDir, tt.dest) + f, err := fs.Create(path) + if err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + err = os.Symlink(path, dest) + if err != nil { + t.Fatal(err) + } + if tt.brokenLink { + // Break the symlink. + fs.Remove(path) + } + matches, err := Glob(fs, dest) + if err != nil { + t.Errorf("GlobSymlink error for %q: %s", dest, err) + } + if !contains(matches, dest) { + t.Errorf("Glob(%#q) = %#v want %v", dest, matches, dest) + } + } +} + + +func TestGlobError(t *testing.T) { + for _, fs := range Fss { + _, err := Glob(fs, "[7]") + if err != nil { + t.Error("expected error for bad pattern; got none") + } + } +}