From 016ef0868a589b3e643188f615d857de2b7e1794 Mon Sep 17 00:00:00 2001 From: Bob Matcuk Date: Mon, 2 Jan 2023 12:45:34 -0500 Subject: [PATCH] closes #79 added WithFilesOnly --- .codecov.yml | 2 ++ README.md | 8 +++++++ doublestar_test.go | 49 ++++++++++++++++++++++++++++++++++++--- glob.go | 38 +++++++++++++++++++------------ globoptions.go | 45 ++++++++++++++++++++++++++++++------ globwalk.go | 57 +++++++++++++++++++++++++++------------------- 6 files changed, 152 insertions(+), 47 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 56e887c..db6e504 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,3 +6,5 @@ coverage: patch: default: target: 70% +ignore: + - globoptions.go diff --git a/README.md b/README.md index af2e5cc..91f3084 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,14 @@ this check. In other words, a pattern such as `{a,b}/*` may fail if either `a` or `b` do not exist but `*/{a,b}` will never fail because the star may match nothing. +```go +WithFilesOnly() +``` + +If passed, doublestar will only return "files" from `Glob`, `GlobWalk`, or +`FilepathGlob`. In this context, "files" are anything that is not a directory +or a symlink to a directory. + ### Glob ```go diff --git a/doublestar_test.go b/doublestar_test.go index 7264efb..cddd619 100644 --- a/doublestar_test.go +++ b/doublestar_test.go @@ -188,6 +188,10 @@ var matchTests = []MatchTest{ {"nopermission/file", "nopermission/file", true, false, nil, true, false, true, !onWindows, 0, 0}, } +// Calculate the number of results that we expect WithFilesOnly at runtime and +// memoize them here +var numResultsFilesOnly []int + func TestValidatePattern(t *testing.T) { for idx, tt := range matchTests { testValidatePatternWith(t, idx, tt) @@ -366,8 +370,12 @@ func TestGlobWithFailOnPatternNotExist(t *testing.T) { doGlobTest(t, WithFailOnPatternNotExist()) } +func TestGlobWithFilesOnly(t *testing.T) { + doGlobTest(t, WithFilesOnly()) +} + func TestGlobWithAllOptions(t *testing.T) { - doGlobTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist()) + doGlobTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly()) } func doGlobTest(t *testing.T, opts ...GlobOption) { @@ -406,8 +414,12 @@ func TestGlobWalkWithFailOnPatternNotExist(t *testing.T) { doGlobWalkTest(t, WithFailOnPatternNotExist()) } +func TestGlobWalkWithFilesOnly(t *testing.T) { + doGlobWalkTest(t, WithFilesOnly()) +} + func TestGlobWalkWithAllOptions(t *testing.T) { - doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist()) + doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly()) } func doGlobWalkTest(t *testing.T, opts ...GlobOption) { @@ -459,6 +471,10 @@ func TestFilepathGlobWithFailOnPatternNotExist(t *testing.T) { doFilepathGlobTest(t, WithFailOnPatternNotExist()) } +func TestFilepathGlobWithFilesOnly(t *testing.T) { + doFilepathGlobTest(t, WithFilesOnly()) +} + func doFilepathGlobTest(t *testing.T, opts ...GlobOption) { glob := newGlob(opts...) fsys := os.DirFS("test") @@ -520,11 +536,14 @@ func verifyGlobResults(t *testing.T, idx int, fn string, tt MatchTest, g *glob, if onWindows { numResults = tt.winNumResults } + if g.filesOnly { + numResults = numResultsFilesOnly[idx] + } if len(matches) != numResults { t.Errorf("#%v. %v(%#q, %#v) = %#v - should have %#v results, got %#v", idx, fn, tt.pattern, g, matches, numResults, len(matches)) } - if inSlice(tt.testPath, matches) != tt.shouldMatchGlob { + if !g.filesOnly && inSlice(tt.testPath, matches) != tt.shouldMatchGlob { if tt.shouldMatchGlob { t.Errorf("#%v. %v(%#q, %#v) = %#v - doesn't contain %v, but should", idx, fn, tt.pattern, g, matches, tt.testPath) } else { @@ -637,6 +656,27 @@ func compareSlices(a, b []string) bool { return len(diff) == 0 } +func buildNumResultsFilesOnly() { + testLen := len(matchTests) + numResultsFilesOnly = make([]int, testLen, testLen) + + fsys := os.DirFS("test") + g := newGlob() + for idx, tt := range matchTests { + if tt.testOnDisk { + count := 0 + GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error { + isDir, _ := g.isDir(fsys, "", p, d) + if !isDir { + count++ + } + return nil + }) + numResultsFilesOnly[idx] = count + } + } +} + func mkdirp(parts ...string) { dirs := path.Join(parts...) err := os.MkdirAll(dirs, 0755) @@ -729,5 +769,8 @@ func TestMain(m *testing.M) { } } + // initialize numResultsFilesOnly + buildNumResultsFilesOnly() + os.Exit(m.Run()) } diff --git a/glob.go b/glob.go index 3e9c6b9..0393a27 100644 --- a/glob.go +++ b/glob.go @@ -68,12 +68,12 @@ func (g *glob) doGlob(fsys fs.FS, pattern string, m []string, firstSegment, befo // pattern exist? // The pattern may contain escaped wildcard characters for an exact path match. path := unescapeMeta(pattern) - _, pathExists, pathErr := g.exists(fsys, path, beforeMeta) + pathInfo, pathExists, pathErr := g.exists(fsys, path, beforeMeta) if pathErr != nil { return nil, pathErr } - if pathExists { + if pathExists && (!firstSegment || !g.filesOnly || !pathInfo.IsDir()) { matches = append(matches, path) } @@ -194,15 +194,17 @@ func (g *glob) globDir(fsys fs.FS, dir, pattern string, matches []string, canMat m = matches if pattern == "" { - // pattern can be an empty string if the original pattern ended in a slash, - // in which case, we should just return dir, but only if it actually exists - // and it's a directory (or a symlink to a directory) - _, isDir, err := g.isPathDir(fsys, dir, beforeMeta) - if err != nil { - return nil, err - } - if isDir { - m = append(m, dir) + if !canMatchFiles || !g.filesOnly { + // pattern can be an empty string if the original pattern ended in a + // slash, in which case, we should just return dir, but only if it + // actually exists and it's a directory (or a symlink to a directory) + _, isDir, err := g.isPathDir(fsys, dir, beforeMeta) + if err != nil { + return nil, err + } + if isDir { + m = append(m, dir) + } } return } @@ -230,11 +232,16 @@ func (g *glob) globDir(fsys fs.FS, dir, pattern string, matches []string, canMat } if matched { matched = canMatchFiles - if !matched { + if !matched || g.filesOnly { matched, e = g.isDir(fsys, dir, name, info) if e != nil { return } + if canMatchFiles { + // if we're here, it's because g.filesOnly + // is set and we don't want directories + matched = !matched + } } if matched { m = append(m, path.Join(dir, name)) @@ -255,8 +262,11 @@ func (g *glob) globDoubleStar(fsys fs.FS, dir string, matches []string, canMatch } } - // `**` can match *this* dir, so add it - matches = append(matches, dir) + if !g.filesOnly { + // `**` can match *this* dir, so add it + matches = append(matches, dir) + } + for _, info := range dirs { name := info.Name() isDir, err := g.isDir(fsys, dir, name, info) diff --git a/globoptions.go b/globoptions.go index f00b074..6b3d057 100644 --- a/globoptions.go +++ b/globoptions.go @@ -1,9 +1,12 @@ package doublestar +import "strings" + // glob is an internal type to store options during globbing. type glob struct { failOnIOErrors bool failOnPatternNotExist bool + filesOnly bool } // GlobOption represents a setting that can be passed to Glob, GlobWalk, and @@ -45,6 +48,16 @@ func WithFailOnPatternNotExist() GlobOption { } } +// WithFilesOnly is an option that can be passed to Glob, GlobWalk, or +// FilepathGlob. If passed, doublestar will only return files that match the +// pattern, not directories. +// +func WithFilesOnly() GlobOption { + return func(g *glob) { + g.filesOnly = true + } +} + // forwardErrIfFailOnIOErrors is used to wrap the return values of I/O // functions. When failOnIOErrors is enabled, it will return err; otherwise, it // always returns nil. @@ -69,13 +82,31 @@ func (g *glob) handlePatternNotExist(canFail bool) error { // Format options for debugging/testing purposes func (g *glob) GoString() string { - if g.failOnIOErrors { - if g.failOnPatternNotExist { - return "opts: WithFailOnIOErrors, WithFailOnPatternNotExist" + var b strings.Builder + b.WriteString("opts: ") + + hasOpts := false + if (g.failOnIOErrors) { + b.WriteString("WithFailOnIOErrors") + hasOpts = true + } + if (g.failOnPatternNotExist) { + if hasOpts { + b.WriteString(", ") + } + b.WriteString("WithFailOnPatternNotExist") + hasOpts = true + } + if (g.filesOnly) { + if hasOpts { + b.WriteString(", ") } - return "opts: WithFailOnIOErrors" - } else if g.failOnPatternNotExist { - return "opts: WithFailOnPatternNotExist" + b.WriteString("WithFilesOnly") + hasOpts = true + } + + if !hasOpts { + b.WriteString("nil") } - return "opts: nil" + return b.String() } diff --git a/globwalk.go b/globwalk.go index 95e981b..84e764f 100644 --- a/globwalk.go +++ b/globwalk.go @@ -76,7 +76,7 @@ func (g *glob) doGlobWalk(fsys fs.FS, pattern string, firstSegment, beforeMeta b // The pattern may contain escaped wildcard characters for an exact path match. path := unescapeMeta(pattern) info, pathExists, err := g.exists(fsys, path, beforeMeta) - if pathExists { + if pathExists && (!firstSegment || !g.filesOnly || !info.IsDir()) { err = fn(path, dirEntryFromFileInfo(info)) if err == SkipDir { err = nil @@ -241,17 +241,19 @@ func (g *glob) doGlobAltsWalk(fsys fs.FS, d, pattern string, startIdx, openingId func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, beforeMeta bool, fn GlobWalkFunc) (e error) { if pattern == "" { - // pattern can be an empty string if the original pattern ended in a slash, - // in which case, we should just return dir, but only if it actually exists - // and it's a directory (or a symlink to a directory) - info, isDir, err := g.isPathDir(fsys, dir, beforeMeta) - if err != nil { - return err - } - if isDir { - e = fn(dir, dirEntryFromFileInfo(info)) - if e == SkipDir { - e = nil + if !canMatchFiles || !g.filesOnly { + // pattern can be an empty string if the original pattern ended in a + // slash, in which case, we should just return dir, but only if it + // actually exists and it's a directory (or a symlink to a directory) + info, isDir, err := g.isPathDir(fsys, dir, beforeMeta) + if err != nil { + return err + } + if isDir { + e = fn(dir, dirEntryFromFileInfo(info)) + if e == SkipDir { + e = nil + } } } return @@ -266,11 +268,13 @@ func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, befor if !dirExists || !info.IsDir() { return nil } - if e = fn(dir, dirEntryFromFileInfo(info)); e != nil { - if e == SkipDir { - e = nil + if !canMatchFiles || !g.filesOnly { + if e = fn(dir, dirEntryFromFileInfo(info)); e != nil { + if e == SkipDir { + e = nil + } + return } - return } return g.globDoubleStarWalk(fsys, dir, canMatchFiles, fn) } @@ -292,11 +296,16 @@ func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, befor } if matched { matched = canMatchFiles - if !matched { + if !matched || g.filesOnly { matched, e = g.isDir(fsys, dir, name, info) if e != nil { return e } + if canMatchFiles { + // if we're here, it's because g.filesOnly + // is set and we don't want directories + matched = !matched + } } if matched { if e = fn(path.Join(dir, name), info); e != nil { @@ -325,7 +334,6 @@ func (g *glob) globDoubleStarWalk(fsys fs.FS, dir string, canMatchFiles bool, fn return g.forwardErrIfFailOnIOErrors(err) } - // `**` can match *this* dir, so add it for _, info := range dirs { name := info.Name() isDir, err := g.isDir(fsys, dir, name, info) @@ -335,12 +343,15 @@ func (g *glob) globDoubleStarWalk(fsys fs.FS, dir string, canMatchFiles bool, fn if isDir { p := path.Join(dir, name) - if e = fn(p, info); e != nil { - if e == SkipDir { - e = nil - continue + if !canMatchFiles || !g.filesOnly { + // `**` can match *this* dir, so add it + if e = fn(p, info); e != nil { + if e == SkipDir { + e = nil + continue + } + return } - return } if e = g.globDoubleStarWalk(fsys, p, canMatchFiles, fn); e != nil { return