Merge pull request #81 from bmatcuk/withfilesonly
Added WithFilesOnly option
diff --git a/.codecov.yml b/.codecov.yml
index 56e887c..db6e504 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -6,3 +6,5 @@
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 @@
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 @@
{"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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
+ // 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 @@
// 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 @@
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 @@
}
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 @@
}
}
- // `**` 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 @@
}
}
+// 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 @@
// Format options for debugging/testing purposes
func (g *glob) GoString() string {
- if g.failOnIOErrors {
- if g.failOnPatternNotExist {
- return "opts: WithFailOnIOErrors, WithFailOnPatternNotExist"
- }
- return "opts: WithFailOnIOErrors"
- } else if g.failOnPatternNotExist {
- return "opts: WithFailOnPatternNotExist"
+ var b strings.Builder
+ b.WriteString("opts: ")
+
+ hasOpts := false
+ if (g.failOnIOErrors) {
+ b.WriteString("WithFailOnIOErrors")
+ hasOpts = true
}
- return "opts: nil"
+ if (g.failOnPatternNotExist) {
+ if hasOpts {
+ b.WriteString(", ")
+ }
+ b.WriteString("WithFailOnPatternNotExist")
+ hasOpts = true
+ }
+ if (g.filesOnly) {
+ if hasOpts {
+ b.WriteString(", ")
+ }
+ b.WriteString("WithFilesOnly")
+ hasOpts = true
+ }
+
+ if !hasOpts {
+ b.WriteString("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 @@
// 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) 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 @@
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 @@
}
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 @@
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 @@
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