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