Part 1 of the Implementation for new 'subpackages()` built-in helper function.

Design proposal: https://docs.google.com/document/d/13UOT0GoQofxDW40ILzH2sWpUOmuYy6QZ7CUmhej9vgk/edit#

This CL modifies the globber infrastructure to support an additional mode of listing sub-directories.

* Add new Globber Operation enum allowing, Globber implementations to
  discriminate between glob, glob w/directories and the future sub-packages
  use-case.

* Modify UnixGlob to replace Predicate and bools with UnixGlobPathDiscriminator interface for:
  a) Determining whether to traverse a sub-directory (previously was lambda)
  b) function for determing what entries to include in the List<Path> produced by UnixGlob.globAsync.

  These allow relatively simple re-use of the same logic for both subpackages and glob

4) Add a few tests for UnixGlob to ensure both cases continue to work as expected.

PiperOrigin-RevId: 421125424
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/BUILD b/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
index 7da1f11..d156e18 100644
--- a/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/BUILD
@@ -33,6 +33,7 @@
         "//src/main/java/com/google/devtools/build/lib/exec:module_action_context_registry",
         "//src/main/java/com/google/devtools/build/lib/exec:spawn_strategy_resolver",
         "//src/main/java/com/google/devtools/build/lib/packages",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber",
         "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/rules/cpp",
         "//src/main/java/com/google/devtools/build/lib/skyframe:containing_package_lookup_value",
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java b/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java
index 4c27684..ab119a4 100644
--- a/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/IncludeParser.java
@@ -38,6 +38,7 @@
 import com.google.devtools.build.lib.concurrent.BlazeInterners;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.includescanning.IncludeParser.Inclusion.Kind;
+import com.google.devtools.build.lib.packages.Globber;
 import com.google.devtools.build.lib.packages.NoSuchPackageException;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
@@ -348,7 +349,7 @@
                   containingPackageLookupValue.getContainingPackageName(),
                   containingPackageLookupValue.getContainingPackageRoot(),
                   pattern,
-                  /*excludeDirs=*/ true,
+                  Globber.Operation.FILES,
                   relativePath.relativeTo(packageFragment)));
         } catch (InvalidGlobPatternException e) {
           env.getListener()
diff --git a/src/main/java/com/google/devtools/build/lib/packages/BUILD b/src/main/java/com/google/devtools/build/lib/packages/BUILD
index 42d4cb4..146d5cf 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/packages/BUILD
@@ -25,17 +25,35 @@
 )
 
 java_library(
+    name = "globber",
+    srcs = ["Globber.java"],
+)
+
+java_library(
+    name = "globber_utils",
+    srcs = ["GlobberUtils.java"],
+    deps = [
+        ":globber",
+        "//third_party:error_prone_annotations",
+    ],
+)
+
+java_library(
     name = "packages",
     srcs = glob(
         ["*.java"],
         exclude = [
             "BuilderFactoryForTesting.java",  # see builder_factory_for_testing
+            "Globber.java",
+            "GlobberUtils.java",
             "ExecGroup.java",
             "ConfiguredAttributeMapper.java",
         ],
     ),
     deps = [
         ":exec_group",
+        ":globber",
+        ":globber_utils",
         "//src/main/java/com/google/devtools/build/docgen/annot",
         "//src/main/java/com/google/devtools/build/lib/actions:execution_requirements",
         "//src/main/java/com/google/devtools/build/lib/actions:thread_state_receiver",
diff --git a/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
index 33c882d..11caecc 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
@@ -15,7 +15,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
@@ -29,6 +28,7 @@
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.lib.vfs.UnixGlobPathDiscriminator;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -44,36 +44,26 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
-/**
- * Caches the results of glob expansion for a package.
- */
+/** Caches the results of glob expansion for a package. */
 @ThreadSafety.ThreadCompatible
 public class GlobCache {
   /**
-   * A mapping from glob expressions (e.g. "*.java") to the list of files it
-   * matched (in the order returned by VFS) at the time the package was
-   * constructed.  Required for sound dependency analysis.
+   * A mapping from glob expressions (e.g. "*.java") to the list of files it matched (in the order
+   * returned by VFS) at the time the package was constructed. Required for sound dependency
+   * analysis.
    *
-   * We don't use a Multimap because it provides no way to distinguish "key not
-   * present" from (key -> {}).
+   * <p>We don't use a Multimap because it provides no way to distinguish "key not present" from
+   * (key -> {}).
    */
-  private final Map<Pair<String, Boolean>, Future<List<Path>>> globCache = new HashMap<>();
+  private final Map<Pair<String, Globber.Operation>, Future<List<Path>>> globCache =
+      new HashMap<>();
 
-  /**
-   * The directory in which our package's BUILD file resides.
-   */
+  /** The directory in which our package's BUILD file resides. */
   private final Path packageDirectory;
 
-  /**
-   * The name of the package we belong to.
-   */
+  /** The name of the package we belong to. */
   private final PackageIdentifier packageId;
 
-  /**
-   * The package locator-based directory traversal predicate.
-   */
-  private final Predicate<Path> childDirectoryPredicate;
-
   /** System call caching layer. */
   private final AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls;
 
@@ -84,6 +74,10 @@
 
   private final AtomicBoolean globalStarted = new AtomicBoolean(false);
 
+  private final CachingPackageLocator packageLocator;
+
+  private final ImmutableSet<PathFragment> ignoredGlobPrefixes;
+
   /**
    * Create a glob expansion cache.
    *
@@ -120,47 +114,55 @@
     this.maxDirectoriesToEagerlyVisit = maxDirectoriesToEagerlyVisit;
 
     Preconditions.checkNotNull(locator);
-    childDirectoryPredicate =
-        directory -> {
-          if (directory.equals(packageDirectory)) {
-            return true;
-          }
+    this.packageLocator = locator;
+    this.ignoredGlobPrefixes = ignoredGlobPrefixes;
+  }
 
-          PathFragment subPackagePath =
-              packageId.getPackageFragment().getRelative(directory.relativeTo(packageDirectory));
+  private boolean globCacheShouldTraverseDirectory(Path directory) {
+    if (directory.equals(packageDirectory)) {
+      return true;
+    }
 
-          for (PathFragment ignoredPrefix : ignoredGlobPrefixes) {
-            if (subPackagePath.startsWith(ignoredPrefix)) {
-              return false;
-            }
-          }
+    PathFragment subPackagePath =
+        packageId.getPackageFragment().getRelative(directory.relativeTo(packageDirectory));
 
-          PackageIdentifier subPackageId =
-              PackageIdentifier.create(packageId.getRepository(), subPackagePath);
-          return locator.getBuildFileForPackage(subPackageId) == null;
-        };
+    for (PathFragment ignoredPrefix : ignoredGlobPrefixes) {
+      if (subPackagePath.startsWith(ignoredPrefix)) {
+        return false;
+      }
+    }
+
+    return !isSubPackage(PackageIdentifier.create(packageId.getRepository(), subPackagePath));
+  }
+
+  private boolean isSubPackage(Path directory) {
+    return isSubPackage(
+        PackageIdentifier.create(
+            packageId.getRepository(),
+            packageId.getPackageFragment().getRelative(directory.relativeTo(packageDirectory))));
+  }
+
+  private boolean isSubPackage(PackageIdentifier subPackageId) {
+    return packageLocator.getBuildFileForPackage(subPackageId) != null;
   }
 
   /**
-   * Returns the future result of evaluating glob "pattern" against this
-   * package's directory, using the package's cache of previously-started
-   * globs if possible.
+   * Returns the future result of evaluating glob "pattern" against this package's directory, using
+   * the package's cache of previously-started globs if possible.
    *
-   * @return the list of paths matching the pattern, relative to the package's
-   *   directory.
-   * @throws BadGlobException if the glob was syntactically invalid, or
-   *  contained uplevel references.
+   * @return the list of paths matching the pattern, relative to the package's directory.
+   * @throws BadGlobException if the glob was syntactically invalid, or contained uplevel
+   *     references.
    */
-  Future<List<Path>> getGlobUnsortedAsync(String pattern, boolean excludeDirs)
+  Future<List<Path>> getGlobUnsortedAsync(String pattern, Globber.Operation globberOperation)
       throws BadGlobException {
-    Future<List<Path>> cached = globCache.get(Pair.of(pattern, excludeDirs));
+    Future<List<Path>> cached = globCache.get(Pair.of(pattern, globberOperation));
     if (cached == null) {
-      if (maxDirectoriesToEagerlyVisit > -1
-          && !globalStarted.getAndSet(true)) {
+      if (maxDirectoriesToEagerlyVisit > -1 && !globalStarted.getAndSet(true)) {
         packageDirectory.prefetchPackageAsync(maxDirectoriesToEagerlyVisit);
       }
-      cached = safeGlobUnsorted(pattern, excludeDirs);
-      setGlobPaths(pattern, excludeDirs, cached);
+      cached = safeGlobUnsorted(pattern, globberOperation);
+      setGlobPaths(pattern, globberOperation, cached);
     }
     return cached;
   }
@@ -168,20 +170,20 @@
   @VisibleForTesting
   List<String> getGlobUnsorted(String pattern)
       throws IOException, BadGlobException, InterruptedException {
-    return getGlobUnsorted(pattern, false);
+    return getGlobUnsorted(pattern, Globber.Operation.FILES_AND_DIRS);
   }
 
   @VisibleForTesting
-  protected List<String> getGlobUnsorted(String pattern, boolean excludeDirs)
+  protected List<String> getGlobUnsorted(String pattern, Globber.Operation globberOperation)
       throws IOException, BadGlobException, InterruptedException {
-    Future<List<Path>> futureResult = getGlobUnsortedAsync(pattern, excludeDirs);
+    Future<List<Path>> futureResult = getGlobUnsortedAsync(pattern, globberOperation);
     List<Path> globPaths = fromFuture(futureResult);
     // Replace the UnixGlob.GlobFuture with a completed future object, to allow
     // garbage collection of the GlobFuture and GlobVisitor objects.
     if (!(futureResult instanceof SettableFuture<?>)) {
       SettableFuture<List<Path>> completedFuture = SettableFuture.create();
       completedFuture.set(globPaths);
-      globCache.put(Pair.of(pattern, excludeDirs), completedFuture);
+      globCache.put(Pair.of(pattern, globberOperation), completedFuture);
     }
 
     List<String> result = Lists.newArrayListWithCapacity(globPaths.size());
@@ -198,16 +200,15 @@
   }
 
   /** Adds glob entries to the cache. */
-  private void setGlobPaths(String pattern, boolean excludeDirectories, Future<List<Path>> result) {
-    globCache.put(Pair.of(pattern, excludeDirectories), result);
+  private void setGlobPaths(
+      String pattern, Globber.Operation globberOperation, Future<List<Path>> result) {
+    globCache.put(Pair.of(pattern, globberOperation), result);
   }
 
-  /**
-   * Actually execute a glob against the filesystem.  Otherwise similar to
-   * getGlob().
-   */
+  /** Actually execute a glob against the filesystem. Otherwise similar to getGlob(). */
   @VisibleForTesting
-  Future<List<Path>> safeGlobUnsorted(String pattern, boolean excludeDirs) throws BadGlobException {
+  Future<List<Path>> safeGlobUnsorted(String pattern, Globber.Operation globberOperation)
+      throws BadGlobException {
     // Forbidden patterns:
     if (pattern.indexOf('?') != -1) {
       throw new BadGlobException("glob pattern '" + pattern + "' contains forbidden '?' wildcard");
@@ -220,8 +221,7 @@
     try {
       return UnixGlob.forPath(packageDirectory)
           .addPattern(pattern)
-          .setExcludeDirectories(excludeDirs)
-          .setDirectoryFilter(childDirectoryPredicate)
+          .setPathDiscriminator(new GlobUnixPathDiscriminator(globberOperation))
           .setExecutor(globExecutor)
           .setFilesystemCalls(syscalls)
           .globAsync();
@@ -230,18 +230,14 @@
     }
   }
 
-  /**
-   * Sanitize the future exceptions - the only expected checked exception
-   * is IOException.
-   */
+  /** Sanitize the future exceptions - the only expected checked exception is IOException. */
   private static List<Path> fromFuture(Future<List<Path>> future)
       throws IOException, InterruptedException {
     try {
       return future.get();
     } catch (ExecutionException e) {
       Throwable cause = e.getCause();
-      Throwables.propagateIfPossible(cause,
-          IOException.class, InterruptedException.class);
+      Throwables.propagateIfPossible(cause, IOException.class, InterruptedException.class);
       throw new RuntimeException(e);
     }
   }
@@ -253,26 +249,24 @@
    * <p>Called by PackageFactory via Package.
    */
   public List<String> globUnsorted(
-      List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+      List<String> includes,
+      List<String> excludes,
+      Globber.Operation globberOperation,
+      boolean allowEmpty)
       throws IOException, BadGlobException, InterruptedException {
     // Start globbing all patterns in parallel. The getGlob() calls below will
     // block on an individual pattern's results, but the other globs can
     // continue in the background.
     for (String pattern : includes) {
       @SuppressWarnings("unused")
-      Future<?> possiblyIgnoredError = getGlobUnsortedAsync(pattern, excludeDirs);
+      Future<?> possiblyIgnoredError = getGlobUnsortedAsync(pattern, globberOperation);
     }
 
     HashSet<String> results = new HashSet<>();
     for (String pattern : includes) {
-      List<String> items = getGlobUnsorted(pattern, excludeDirs);
+      List<String> items = getGlobUnsorted(pattern, globberOperation);
       if (!allowEmpty && items.isEmpty()) {
-        throw new BadGlobException(
-            "glob pattern '"
-                + pattern
-                + "' didn't match anything, but allow_empty is set to False "
-                + "(the default value of allow_empty can be set with "
-                + "--incompatible_disallow_empty_glob).");
+        GlobberUtils.throwBadGlobExceptionEmptyResult(pattern, globberOperation);
       }
       results.addAll(items);
     }
@@ -282,21 +276,16 @@
       throw new BadGlobException(ex.getMessage());
     }
     if (!allowEmpty && results.isEmpty()) {
-      throw new BadGlobException(
-          "all files in the glob have been excluded, but allow_empty is set to False "
-              + "(the default value of allow_empty can be set with "
-              + "--incompatible_disallow_empty_glob).");
+      GlobberUtils.throwBadGlobExceptionAllExcluded(globberOperation);
     }
     return new ArrayList<>(results);
   }
 
-  public Set<Pair<String, Boolean>> getKeySet() {
+  public Set<Pair<String, Globber.Operation>> getKeySet() {
     return globCache.keySet();
   }
 
-  /**
-   * Block on the completion of all potentially-abandoned background tasks.
-   */
+  /** Block on the completion of all potentially-abandoned background tasks. */
   public void finishBackgroundTasks() {
     finishBackgroundTasks(globCache.values());
   }
@@ -334,4 +323,45 @@
   public String toString() {
     return "GlobCache for " + packageId + " in " + packageDirectory;
   }
+
+  /**
+   * Used by 'glob()' and 'subpackages()' with UnixGlob to determine if a directory should be
+   * traversed when recursing through a filesystem directory structure or include a Path in the
+   * result. This essentially filters out a set of ignored prefixes and then checks to see if a
+   * given sub-dir actually represents a sub-package or not when traversing.
+   *
+   * <p>The logic of including inspects the Globber.Operation to determine if it will include all
+   * files, include directories or subpackages in the output.
+   */
+  private class GlobUnixPathDiscriminator implements UnixGlobPathDiscriminator {
+    private final Globber.Operation globberOperation;
+
+    GlobUnixPathDiscriminator(Globber.Operation globberOperation) {
+      this.globberOperation = globberOperation;
+    }
+
+    @Override
+    public boolean shouldTraverseDirectory(Path directory) {
+      return globCacheShouldTraverseDirectory(directory);
+    }
+
+    @Override
+    public boolean shouldIncludePathInResult(Path path, boolean isDirectory) {
+      switch (globberOperation) {
+        case FILES_AND_DIRS:
+          return !isDirectory || !isSubPackage(path);
+        case SUBPACKAGES:
+          // no files, or root pkg
+          if (!isDirectory || path.equals(packageDirectory)) {
+            return false;
+          }
+          return isSubPackage(path);
+
+        case FILES:
+          return !isDirectory;
+      }
+      throw new IllegalStateException(
+          "Unexpected unhandled Globber.Operation enum value: " + globberOperation);
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Globber.java b/src/main/java/com/google/devtools/build/lib/packages/Globber.java
index a580a47..4bc2184 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Globber.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Globber.java
@@ -21,6 +21,19 @@
   /** An opaque token for fetching the result of a glob computation. */
   abstract class Token {}
 
+  /**
+   * Indiciates they type of globbing operations we're doing, whether looking for Files+Dirs or
+   * sub-packages.
+   */
+  enum Operation {
+    // Return only files,
+    FILES,
+    // Return files and directories, but not sub-packages
+    FILES_AND_DIRS,
+    // Return only sub-packages
+    SUBPACKAGES,
+  }
+
   /** Used to indicate an invalid glob pattern. */
   class BadGlobException extends Exception {
     public BadGlobException(String message) {
@@ -35,7 +48,7 @@
    *     invalid.
    */
   Token runAsync(
-      List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+      List<String> includes, List<String> excludes, Operation operation, boolean allowEmpty)
       throws BadGlobException, InterruptedException;
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/packages/GlobberUtils.java b/src/main/java/com/google/devtools/build/lib/packages/GlobberUtils.java
new file mode 100644
index 0000000..766cb83
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/GlobberUtils.java
@@ -0,0 +1,56 @@
+// Copyright 2022 The Bazel 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 com.google.devtools.build.lib.packages;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+
+/** Static functionality shared by different implementations of the Globber interface. */
+@CheckReturnValue
+public final class GlobberUtils {
+
+  private GlobberUtils() {}
+
+  public static void throwBadGlobExceptionEmptyResult(
+      String pattern, Globber.Operation globberOperation) throws Globber.BadGlobException {
+    switch (globberOperation) {
+      case SUBPACKAGES:
+        throw new Globber.BadGlobException(
+            "subpackages pattern '"
+                + pattern
+                + "' didn't match anything, but allow_empty is set to False (the default value)");
+      default:
+        throw new Globber.BadGlobException(
+            "glob pattern '"
+                + pattern
+                + "' didn't match anything, but allow_empty is set to False "
+                + "(the default value of allow_empty can be set with "
+                + "--incompatible_disallow_empty_glob).");
+    }
+  }
+
+  public static void throwBadGlobExceptionAllExcluded(Globber.Operation globberOperation)
+      throws Globber.BadGlobException {
+    switch (globberOperation) {
+      case SUBPACKAGES:
+        throw new Globber.BadGlobException(
+            "all subpackages in subpackages() have been excluded, but allow_empty is"
+                + " set to False ");
+      default:
+        throw new Globber.BadGlobException(
+            "all files in the glob have been excluded, but allow_empty is set to False "
+                + "(the default value of allow_empty can be set with "
+                + "--incompatible_disallow_empty_glob).");
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java b/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java
index 01246fc..1a85563 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/NonSkyframeGlobber.java
@@ -30,27 +30,34 @@
   public static class Token extends Globber.Token {
     private final List<String> includes;
     private final List<String> excludes;
-    private final boolean excludeDirs;
+    private final Globber.Operation globberOperation;
     private final boolean allowEmpty;
 
     private Token(
-        List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty) {
+        List<String> includes,
+        List<String> excludes,
+        Globber.Operation globberOperation,
+        boolean allowEmpty) {
       this.includes = includes;
       this.excludes = excludes;
-      this.excludeDirs = excludeDirs;
+      this.globberOperation = globberOperation;
       this.allowEmpty = allowEmpty;
     }
   }
 
   @Override
   public Token runAsync(
-      List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+      List<String> includes,
+      List<String> excludes,
+      Globber.Operation globberOperation,
+      boolean allowEmpty)
       throws BadGlobException {
+
     for (String pattern : includes) {
       @SuppressWarnings("unused")
-      Future<?> possiblyIgnoredError = globCache.getGlobUnsortedAsync(pattern, excludeDirs);
+      Future<?> possiblyIgnoredError = globCache.getGlobUnsortedAsync(pattern, globberOperation);
     }
-    return new Token(includes, excludes, excludeDirs, allowEmpty);
+    return new Token(includes, excludes, globberOperation, allowEmpty);
   }
 
   @Override
@@ -58,10 +65,7 @@
       throws BadGlobException, IOException, InterruptedException {
     Token ourToken = (Token) token;
     return globCache.globUnsorted(
-        ourToken.includes,
-        ourToken.excludes,
-        ourToken.excludeDirs,
-        ourToken.allowEmpty);
+        ourToken.includes, ourToken.excludes, ourToken.globberOperation, ourToken.allowEmpty);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
index dd57fe7..c1e86cc 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
@@ -610,8 +610,9 @@
     if (maxDirectoriesToEagerlyVisitInGlobbing == -2) {
       try {
         boolean allowEmpty = true;
-        globber.runAsync(globs, ImmutableList.of(), /*excludeDirs=*/ true, allowEmpty);
-        globber.runAsync(globsWithDirs, ImmutableList.of(), /*excludeDirs=*/ false, allowEmpty);
+        globber.runAsync(globs, ImmutableList.of(), Globber.Operation.FILES, allowEmpty);
+        globber.runAsync(
+            globsWithDirs, ImmutableList.of(), Globber.Operation.FILES_AND_DIRS, allowEmpty);
       } catch (BadGlobException ex) {
         logger.atWarning().withCause(ex).log(
             "Suppressing exception for globs=%s, globsWithDirs=%s", globs, globsWithDirs);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
index b18598b..9209f9d 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
@@ -99,6 +99,8 @@
 
     List<String> includes = Type.STRING_LIST.convert(include, "'glob' argument");
     List<String> excludes = Type.STRING_LIST.convert(exclude, "'glob' argument");
+    Globber.Operation op =
+        excludeDirs.signum() != 0 ? Globber.Operation.FILES : Globber.Operation.FILES_AND_DIRS;
 
     List<String> matches;
     boolean allowEmpty;
@@ -113,8 +115,7 @@
     }
 
     try {
-      Globber.Token globToken =
-          context.globber.runAsync(includes, excludes, excludeDirs.signum() != 0, allowEmpty);
+      Globber.Token globToken = context.globber.runAsync(includes, excludes, op, allowEmpty);
       matches = context.globber.fetchUnsorted(globToken);
     } catch (IOException e) {
       logger.atWarning().withCause(e).log(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index 8576a85..52b6c43 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -299,6 +299,8 @@
         "//src/main/java/com/google/devtools/build/lib/io:process_package_directory_exception",
         "//src/main/java/com/google/devtools/build/lib/packages",
         "//src/main/java/com/google/devtools/build/lib/packages:exec_group",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber_utils",
         "//src/main/java/com/google/devtools/build/lib/packages/semantics",
         "//src/main/java/com/google/devtools/build/lib/pkgcache",
         "//src/main/java/com/google/devtools/build/lib/pkgcache:QueryTransitivePackagePreloader",
@@ -1426,6 +1428,7 @@
         ":sky_functions",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/concurrent",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
         "//src/main/java/com/google/devtools/build/lib/util:string",
         "//src/main/java/com/google/devtools/build/lib/vfs",
@@ -1450,6 +1453,7 @@
         "//src/main/java/com/google/devtools/build/lib/io:file_symlink_infinite_expansion_exception",
         "//src/main/java/com/google/devtools/build/lib/io:file_symlink_infinite_expansion_uniqueness_function",
         "//src/main/java/com/google/devtools/build/lib/io:inconsistent_filesystem_exception",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
         "//src/main/java/com/google/devtools/build/skyframe",
@@ -1467,6 +1471,7 @@
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/concurrent",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
index f64d451c..a436090 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
@@ -18,6 +18,7 @@
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.concurrent.BlazeInterners;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Globber;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.util.StringCanonicalizer;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -46,7 +47,7 @@
    * @param subdir the subdirectory being looked at (must exist and must be a directory. It's
    *     assumed that there are no other packages between {@code packageName} and {@code subdir}.
    * @param pattern a valid glob pattern
-   * @param excludeDirs true if directories should be excluded from results
+   * @param globberOperation type of Globber operation being tracked.
    */
   @AutoCodec.Instantiator
   public static GlobDescriptor create(
@@ -54,36 +55,35 @@
       Root packageRoot,
       PathFragment subdir,
       String pattern,
-      boolean excludeDirs) {
+      Globber.Operation globberOperation) {
     return interner.intern(
-        new GlobDescriptor(packageId, packageRoot, subdir, pattern, excludeDirs));
-
+        new GlobDescriptor(packageId, packageRoot, subdir, pattern, globberOperation));
   }
 
   private final PackageIdentifier packageId;
   private final Root packageRoot;
   private final PathFragment subdir;
   private final String pattern;
-  private final boolean excludeDirs;
+  private final Globber.Operation globberOperation;
 
   private GlobDescriptor(
       PackageIdentifier packageId,
       Root packageRoot,
       PathFragment subdir,
       String pattern,
-      boolean excludeDirs) {
+      Globber.Operation globberOperation) {
     this.packageId = Preconditions.checkNotNull(packageId);
     this.packageRoot = Preconditions.checkNotNull(packageRoot);
     this.subdir = Preconditions.checkNotNull(subdir);
     this.pattern = Preconditions.checkNotNull(StringCanonicalizer.intern(pattern));
-    this.excludeDirs = excludeDirs;
+    this.globberOperation = globberOperation;
   }
 
   @Override
   public String toString() {
     return String.format(
-        "<GlobDescriptor packageName=%s packageRoot=%s subdir=%s pattern=%s excludeDirs=%s>",
-        packageId, packageRoot, subdir, pattern, excludeDirs);
+        "<GlobDescriptor packageName=%s packageRoot=%s subdir=%s pattern=%s globberOperation=%s>",
+        packageId, packageRoot, subdir, pattern, globberOperation.name());
   }
 
   /**
@@ -117,11 +117,9 @@
     return pattern;
   }
 
-  /**
-   * Returns true if directories should be excluded from results.
-   */
-  public boolean excludeDirs() {
-    return excludeDirs;
+  /** Returns the type of Globber operation that produced the results. */
+  public Globber.Operation globberOperation() {
+    return globberOperation;
   }
 
   @Override
@@ -133,9 +131,11 @@
       return false;
     }
     GlobDescriptor other = (GlobDescriptor) obj;
-    return packageId.equals(other.packageId) && packageRoot.equals(other.packageRoot)
-        && subdir.equals(other.subdir) && pattern.equals(other.pattern)
-        && excludeDirs == other.excludeDirs;
+    return packageId.equals(other.packageId)
+        && packageRoot.equals(other.packageRoot)
+        && subdir.equals(other.subdir)
+        && pattern.equals(other.pattern)
+        && globberOperation == other.globberOperation;
   }
 
   @Override
@@ -143,7 +143,7 @@
     // Generated instead of Objects.hashCode to avoid intermediate array required for latter.
     final int prime = 31;
     int result = 1;
-    result = prime * result + (excludeDirs ? 1231 : 1237);
+    result = prime * result + globberOperation.hashCode();
     result = prime * result + packageId.hashCode();
     result = prime * result + packageRoot.hashCode();
     result = prime * result + pattern.hashCode();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
index 5170a8a..5aef741 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionException;
 import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionUniquenessFunction;
 import com.google.devtools.build.lib.io.InconsistentFilesystemException;
+import com.google.devtools.build.lib.packages.Globber;
 import com.google.devtools.build.lib.vfs.Dirent;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.RootedPath;
@@ -62,6 +63,7 @@
   public SkyValue compute(SkyKey skyKey, Environment env)
       throws GlobFunctionException, InterruptedException {
     GlobDescriptor glob = (GlobDescriptor) skyKey.argument();
+    Globber.Operation globberOperation = glob.globberOperation();
 
     RepositoryName repositoryName = glob.getPackageId().getRepository();
     IgnoredPackagePrefixesValue ignoredPackagePrefixes =
@@ -121,7 +123,6 @@
 
     boolean globMatchesBareFile = patternTail == null;
 
-
     RootedPath dirRootedPath = RootedPath.toRootedPath(glob.getPackageRoot(), dirPathFragment);
     if (alwaysUseDirListing || containsGlobs(patternHead)) {
       // Pattern contains globs, so a directory listing is required.
@@ -139,7 +140,8 @@
         // "**" also matches an empty segment, so try the case where it is not present.
         if (globMatchesBareFile) {
           // Recursive globs aren't supposed to match the package's directory.
-          if (!glob.excludeDirs() && !globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
+          if (globberOperation == Globber.Operation.FILES_AND_DIRS
+              && !globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
             matches.add(globSubdir);
           }
         } else {
@@ -152,7 +154,7 @@
                   glob.getPackageRoot(),
                   globSubdir,
                   patternTail,
-                  glob.excludeDirs());
+                  globberOperation);
           Map<SkyKey, SkyValue> listingAndRecursiveGlobMap =
               env.getValues(
                   ImmutableList.of(keyForRecursiveGlobInCurrentDirectory, directoryListingKey));
@@ -369,7 +371,7 @@
   private static SkyKey getSkyKeyForSubdir(
       String fileName, GlobDescriptor glob, String subdirPattern) {
     if (subdirPattern == null) {
-      if (glob.excludeDirs()) {
+      if (glob.globberOperation() == Globber.Operation.FILES) {
         return null;
       } else {
         return PackageLookupValue.key(
@@ -389,7 +391,7 @@
           glob.getPackageRoot(),
           glob.getSubdir().getRelative(fileName),
           subdirPattern,
-          glob.excludeDirs());
+          glob.globberOperation());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
index bcc89fc..bc7900a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Globber;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
 import com.google.devtools.build.lib.vfs.UnixGlob;
@@ -84,7 +85,7 @@
       PackageIdentifier packageId,
       Root packageRoot,
       String pattern,
-      boolean excludeDirs,
+      Globber.Operation globOperation,
       PathFragment subdir)
       throws InvalidGlobPatternException {
     if (pattern.indexOf('?') != -1) {
@@ -96,7 +97,7 @@
       throw new InvalidGlobPatternException(pattern, error);
     }
 
-    return internalKey(packageId, packageRoot, subdir, pattern, excludeDirs);
+    return internalKey(packageId, packageRoot, subdir, pattern, globOperation);
   }
 
   /**
@@ -110,8 +111,8 @@
       Root packageRoot,
       PathFragment subdir,
       String pattern,
-      boolean excludeDirs) {
-    return GlobDescriptor.create(packageId, packageRoot, subdir, pattern, excludeDirs);
+      Globber.Operation globOperation) {
+    return GlobDescriptor.create(packageId, packageRoot, subdir, pattern, globOperation);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
index fc19b2d..74bf760 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -41,6 +41,7 @@
 import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
 import com.google.devtools.build.lib.packages.CachingPackageLocator;
 import com.google.devtools.build.lib.packages.Globber;
+import com.google.devtools.build.lib.packages.GlobberUtils;
 import com.google.devtools.build.lib.packages.InvalidPackageNameException;
 import com.google.devtools.build.lib.packages.NoSuchPackageException;
 import com.google.devtools.build.lib.packages.NonSkyframeGlobber;
@@ -854,9 +855,12 @@
 
     @Override
     public Token runAsync(
-        List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+        List<String> includes,
+        List<String> excludes,
+        Globber.Operation globberOperation,
+        boolean allowEmpty)
         throws BadGlobException, InterruptedException {
-      return delegate.runAsync(includes, excludes, excludeDirs, allowEmpty);
+      return delegate.runAsync(includes, excludes, globberOperation, allowEmpty);
     }
 
     @Override
@@ -956,10 +960,11 @@
       return ImmutableSet.copyOf(globDepsRequested);
     }
 
-    private SkyKey getGlobKey(String pattern, boolean excludeDirs) throws BadGlobException {
+    private SkyKey getGlobKey(String pattern, Globber.Operation globberOperation)
+        throws BadGlobException {
       try {
         return GlobValue.key(
-            packageId, packageRoot, pattern, excludeDirs, PathFragment.EMPTY_FRAGMENT);
+            packageId, packageRoot, pattern, globberOperation, PathFragment.EMPTY_FRAGMENT);
       } catch (InvalidGlobPatternException e) {
         throw new BadGlobException(e.getMessage());
       }
@@ -967,13 +972,16 @@
 
     @Override
     public Token runAsync(
-        List<String> includes, List<String> excludes, boolean excludeDirs, boolean allowEmpty)
+        List<String> includes,
+        List<String> excludes,
+        Globber.Operation globberOperation,
+        boolean allowEmpty)
         throws BadGlobException, InterruptedException {
       LinkedHashSet<SkyKey> globKeys = Sets.newLinkedHashSetWithExpectedSize(includes.size());
       Map<SkyKey, String> globKeyToPatternMap = Maps.newHashMapWithExpectedSize(includes.size());
 
       for (String pattern : includes) {
-        SkyKey globKey = getGlobKey(pattern, excludeDirs);
+        SkyKey globKey = getGlobKey(pattern, globberOperation);
         globKeys.add(globKey);
         globKeyToPatternMap.put(globKey, pattern);
       }
@@ -997,9 +1005,9 @@
           globsToDelegate.isEmpty()
               ? null
               : nonSkyframeGlobber.runAsync(
-                  globsToDelegate, ImmutableList.of(), excludeDirs, allowEmpty);
+                  globsToDelegate, ImmutableList.of(), globberOperation, allowEmpty);
       return new HybridToken(
-          globValueMap, globKeys, nonSkyframeIncludesToken, excludes, allowEmpty);
+          globValueMap, globKeys, nonSkyframeIncludesToken, excludes, globberOperation, allowEmpty);
     }
 
     private static Collection<SkyKey> getMissingKeys(
@@ -1057,6 +1065,8 @@
 
       private final List<String> excludes;
 
+      private final Globber.Operation globberOperation;
+
       private final boolean allowEmpty;
 
       private HybridToken(
@@ -1064,11 +1074,13 @@
           Iterable<SkyKey> includesGlobKeys,
           @Nullable NonSkyframeGlobber.Token nonSkyframeGlobberIncludesToken,
           List<String> excludes,
+          Globber.Operation globberOperation,
           boolean allowEmpty) {
         this.globValueMap = globValueMap;
         this.includesGlobKeys = includesGlobKeys;
         this.nonSkyframeGlobberIncludesToken = nonSkyframeGlobberIncludesToken;
         this.excludes = excludes;
+        this.globberOperation = globberOperation;
         this.allowEmpty = allowEmpty;
       }
 
@@ -1083,12 +1095,8 @@
             foundMatch = true;
           }
           if (!allowEmpty && !foundMatch) {
-            throw new BadGlobException(
-                "glob pattern '"
-                    + ((GlobDescriptor) includeGlobKey.argument()).getPattern()
-                    + "' didn't match anything, but allow_empty is set to False "
-                    + "(the default value of allow_empty can be set with "
-                    + "--incompatible_disallow_empty_glob).");
+            GlobberUtils.throwBadGlobExceptionEmptyResult(
+                ((GlobDescriptor) includeGlobKey.argument()).getPattern(), globberOperation);
           }
         }
         if (nonSkyframeGlobberIncludesToken != null) {
@@ -1102,10 +1110,7 @@
         List<String> result = new ArrayList<>(matches);
 
         if (!allowEmpty && result.isEmpty()) {
-          throw new BadGlobException(
-              "all files in the glob have been excluded, but allow_empty is set to False "
-                  + "(the default value of allow_empty can be set with "
-                  + "--incompatible_disallow_empty_glob).");
+          GlobberUtils.throwBadGlobExceptionAllExcluded(globberOperation);
         }
         return result;
       }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
index 23ab5ab..92f1c33 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
@@ -17,8 +17,6 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
 import com.google.common.base.Splitter;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
@@ -60,6 +58,9 @@
  * <p>Importantly, note that the glob matches are in an unspecified order.
  */
 public final class UnixGlob {
+  private static final UnixGlobPathDiscriminator DEFAULT_DISCRIMINATOR =
+      new UnixGlobPathDiscriminator() {};
+
   private UnixGlob() {}
 
   /** Indicates an invalid glob pattern. */
@@ -72,51 +73,46 @@
   private static List<Path> globInternal(
       Path base,
       Collection<String> patterns,
-      boolean excludeDirectories,
-      Predicate<Path> dirPred,
+      UnixGlobPathDiscriminator pathDiscriminator,
       FilesystemCalls syscalls,
       Executor executor)
       throws IOException, InterruptedException, BadPattern {
     GlobVisitor visitor = new GlobVisitor(executor);
-    return visitor.glob(base, patterns, excludeDirectories, dirPred, syscalls);
+    return visitor.glob(base, patterns, pathDiscriminator, syscalls);
   }
 
   private static List<Path> globInternalUninterruptible(
       Path base,
       Collection<String> patterns,
-      boolean excludeDirectories,
-      Predicate<Path> dirPred,
+      UnixGlobPathDiscriminator pathDiscriminator,
       FilesystemCalls syscalls,
       Executor executor)
       throws IOException, BadPattern {
     GlobVisitor visitor = new GlobVisitor(executor);
-    return visitor.globUninterruptible(base, patterns, excludeDirectories, dirPred, syscalls);
+    return visitor.globUninterruptible(base, patterns, pathDiscriminator, syscalls);
   }
 
   private static long globInternalAndReturnNumGlobTasksForTesting(
       Path base,
       Collection<String> patterns,
-      boolean excludeDirectories,
-      Predicate<Path> dirPred,
+      UnixGlobPathDiscriminator pathDiscriminator,
       FilesystemCalls syscalls,
       Executor executor)
       throws IOException, InterruptedException, BadPattern {
     GlobVisitor visitor = new GlobVisitor(executor);
-    visitor.glob(base, patterns, excludeDirectories, dirPred, syscalls);
+    visitor.glob(base, patterns, pathDiscriminator, syscalls);
     return visitor.getNumGlobTasksForTesting();
   }
 
   private static Future<List<Path>> globAsyncInternal(
       Path base,
       Collection<String> patterns,
-      boolean excludeDirectories,
-      Predicate<Path> dirPred,
+      UnixGlobPathDiscriminator pathDiscriminator,
       FilesystemCalls syscalls,
       Executor executor)
       throws BadPattern {
     Preconditions.checkNotNull(executor, "%s %s", base, patterns);
-    return new GlobVisitor(executor)
-        .globAsync(base, patterns, excludeDirectories, dirPred, syscalls);
+    return new GlobVisitor(executor).globAsync(base, patterns, pathDiscriminator, syscalls);
   }
 
   /**
@@ -345,8 +341,7 @@
   public static class Builder {
     private Path base;
     private List<String> patterns;
-    private boolean excludeDirectories;
-    private Predicate<Path> pathFilter;
+    private UnixGlobPathDiscriminator pathDiscriminator = DEFAULT_DISCRIMINATOR;
     private Executor executor;
     private AtomicReference<? extends FilesystemCalls> syscalls =
         new AtomicReference<>(DEFAULT_SYSCALLS);
@@ -357,8 +352,6 @@
     public Builder(Path base) {
       this.base = base;
       this.patterns = Lists.newArrayList();
-      this.excludeDirectories = false;
-      this.pathFilter = Predicates.alwaysTrue();
     }
 
     /**
@@ -402,14 +395,6 @@
     }
 
     /**
-     * If set to true, directories are not returned in the glob result.
-     */
-    public Builder setExcludeDirectories(boolean excludeDirectories) {
-      this.excludeDirectories = excludeDirectories;
-      return this;
-    }
-
-    /**
      * Sets the executor to use for parallel glob evaluation. If unset, evaluation is done
      * in-thread.
      */
@@ -418,21 +403,27 @@
       return this;
     }
 
-
     /**
-     * If set, the given predicate is called for every directory
-     * encountered. If it returns false, the corresponding item is not
-     * returned in the output and directories are not traversed either.
+     * Sets the UnixGlobPathDiscriminator which determines how to handle Path entries encountered
+     * during glob traversal. The interface determines if Paths should be added to the {@code
+     * List<Path>} results and whether to traverse a given directory during recursion.
+     *
+     * <p>The UnixGlobPathDiscriminator should only be called with Paths that have been resolved to
+     * a regular file or regular directory, it will not properly handle symlinks or special files.
+     *
+     * <p>This is used for handling the previous use case of 'excludeDirectories' where we wish to
+     * exclude files from the glob and decide which directories to traverse, like skipping sub-dirs
+     * containing BUILD files.
      */
-    public Builder setDirectoryFilter(Predicate<Path> pathFilter) {
-      this.pathFilter = pathFilter;
+    public Builder setPathDiscriminator(UnixGlobPathDiscriminator pathDiscriminator) {
+      this.pathDiscriminator = pathDiscriminator;
       return this;
     }
 
     /** Executes the glob. */
     public List<Path> glob() throws IOException, BadPattern {
       return globInternalUninterruptible(
-          base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+          base, patterns, pathDiscriminator, syscalls.get(), executor);
     }
 
     /**
@@ -441,14 +432,14 @@
      * @throws InterruptedException if the thread is interrupted.
      */
     public List<Path> globInterruptible() throws IOException, InterruptedException, BadPattern {
-      return globInternal(base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+      return globInternal(base, patterns, pathDiscriminator, syscalls.get(), executor);
     }
 
     @VisibleForTesting
     public long globInterruptibleAndReturnNumGlobTasksForTesting()
         throws IOException, InterruptedException, BadPattern {
       return globInternalAndReturnNumGlobTasksForTesting(
-          base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+          base, patterns, pathDiscriminator, syscalls.get(), executor);
     }
 
     /**
@@ -456,8 +447,7 @@
      * non-null argument.
      */
     public Future<List<Path>> globAsync() throws BadPattern {
-      return globAsyncInternal(
-          base, patterns, excludeDirectories, pathFilter, syscalls.get(), executor);
+      return globAsyncInternal(base, patterns, pathDiscriminator, syscalls.get(), executor);
     }
   }
 
@@ -522,9 +512,11 @@
 
     /**
      * Performs wildcard globbing: returns the list of filenames that match any of {@code patterns}
-     * relative to {@code base}. Directories are traversed if and only if they match {@code
-     * dirPred}. The predicate is also called for the root of the traversal. The order of the
-     * returned list is unspecified.
+     * relative to {@code base}. Directories are traversed if and only if they return true from
+     * {@code pathDiscriminator.shouldTraverseDirectory}. The predicate is also called for the root
+     * of the traversal. {@code pathDiscriminator.shouldIncludePathInResult} is called to determine
+     * if a directory result should be included in the output. The The order of the returned list is
+     * unspecified.
      *
      * <p>Patterns may include "*" and "?", but not "[a-z]".
      *
@@ -538,12 +530,11 @@
     List<Path> glob(
         Path base,
         Collection<String> patterns,
-        boolean excludeDirectories,
-        Predicate<Path> dirPred,
+        UnixGlobPathDiscriminator pathDiscriminator,
         FilesystemCalls syscalls)
         throws IOException, InterruptedException, BadPattern {
       try {
-        return globAsync(base, patterns, excludeDirectories, dirPred, syscalls).get();
+        return globAsync(base, patterns, pathDiscriminator, syscalls).get();
       } catch (ExecutionException e) {
         Throwable cause = e.getCause();
         Throwables.propagateIfPossible(cause, IOException.class);
@@ -554,13 +545,12 @@
     List<Path> globUninterruptible(
         Path base,
         Collection<String> patterns,
-        boolean excludeDirectories,
-        Predicate<Path> dirPred,
+        UnixGlobPathDiscriminator pathDiscriminator,
         FilesystemCalls syscalls)
         throws IOException, BadPattern {
       try {
         return Uninterruptibles.getUninterruptibly(
-            globAsync(base, patterns, excludeDirectories, dirPred, syscalls));
+            globAsync(base, patterns, pathDiscriminator, syscalls));
       } catch (ExecutionException e) {
         Throwable cause = e.getCause();
         Throwables.propagateIfPossible(cause, IOException.class);
@@ -580,8 +570,7 @@
     Future<List<Path>> globAsync(
         Path base,
         Collection<String> patterns,
-        boolean excludeDirectories,
-        Predicate<Path> dirPred,
+        UnixGlobPathDiscriminator pathDiscriminator,
         FilesystemCalls syscalls)
         throws BadPattern {
       FileStatus baseStat;
@@ -610,9 +599,10 @@
               ++numRecursivePatterns;
             }
           }
-          GlobTaskContext context = numRecursivePatterns > 1
-              ? new RecursiveGlobTaskContext(splitPattern, excludeDirectories, dirPred, syscalls)
-              : new GlobTaskContext(splitPattern, excludeDirectories, dirPred, syscalls);
+          GlobTaskContext context =
+              numRecursivePatterns > 1
+                  ? new RecursiveGlobTaskContext(splitPattern, pathDiscriminator, syscalls)
+                  : new GlobTaskContext(splitPattern, pathDiscriminator, syscalls);
           context.queueGlob(base, baseStat.isDirectory(), 0);
         }
       } finally {
@@ -657,10 +647,9 @@
             @Override
             public String toString() {
               return String.format(
-                  "%s glob(include=[%s], exclude_directories=%s)",
+                  "%s glob(include=[%s])",
                   base.getPathString(),
-                  "\"" + Joiner.on("\", \"").join(context.patternParts) + "\"",
-                  context.excludeDirectories);
+                  "\"" + Joiner.on("\", \"").join(context.patternParts) + "\"");
             }
           });
     }
@@ -720,18 +709,15 @@
     /** A context for evaluating all the subtasks of a single top-level glob task. */
     private class GlobTaskContext {
       private final String[] patternParts;
-      private final boolean excludeDirectories;
-      private final Predicate<Path> dirPred;
+      private final UnixGlobPathDiscriminator pathDiscriminator;
       private final FilesystemCalls syscalls;
 
       GlobTaskContext(
           String[] patternParts,
-          boolean excludeDirectories,
-          Predicate<Path> dirPred,
+          UnixGlobPathDiscriminator pathDiscriminator,
           FilesystemCalls syscalls) {
         this.patternParts = patternParts;
-        this.excludeDirectories = excludeDirectories;
-        this.dirPred = dirPred;
+        this.pathDiscriminator = pathDiscriminator;
         this.syscalls = syscalls;
       }
 
@@ -779,10 +765,9 @@
 
       private RecursiveGlobTaskContext(
           String[] patternParts,
-          boolean excludeDirectories,
-          Predicate<Path> dirPred,
+          UnixGlobPathDiscriminator pathDiscriminator,
           FilesystemCalls syscalls) {
-        super(patternParts, excludeDirectories, dirPred, syscalls);
+        super(patternParts, pathDiscriminator, syscalls);
       }
 
       @Override
@@ -811,14 +796,14 @@
      */
     private void reallyGlob(Path base, boolean baseIsDir, int idx, GlobTaskContext context)
         throws IOException {
-      if (baseIsDir && !context.dirPred.apply(base)) {
+
+      if (baseIsDir && !context.pathDiscriminator.shouldTraverseDirectory(base)) {
+        maybeAddResult(context, base, baseIsDir);
         return;
       }
 
       if (idx == context.patternParts.length) { // Base case.
-        if (!(context.excludeDirectories && baseIsDir)) {
-          results.add(base);
-        }
+        maybeAddResult(context, base, baseIsDir);
 
         return;
       }
@@ -868,6 +853,12 @@
       }
     }
 
+    private void maybeAddResult(GlobTaskContext context, Path base, boolean isDirectory) {
+      if (context.pathDiscriminator.shouldIncludePathInResult(base, isDirectory)) {
+        results.add(base);
+      }
+    }
+
     /**
      * Process symlinks asynchronously. If we should used readdir(..., Symlinks.FOLLOW), that would
      * result in a sequential symlink resolution with many file system implementations. If the
@@ -894,7 +885,7 @@
       if (isDir) {
         context.queueGlob(path, /* baseIsDir= */ true, idx + (isRecursivePattern ? 0 : 1));
       } else if (idx + 1 == context.patternParts.length) {
-        results.add(path);
+        maybeAddResult(context, path, /* isDirectory= */ false);
       }
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlobPathDiscriminator.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlobPathDiscriminator.java
new file mode 100644
index 0000000..04e49b0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlobPathDiscriminator.java
@@ -0,0 +1,41 @@
+// Copyright 2022 The Bazel 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 com.google.devtools.build.lib.vfs;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+
+/**
+ * Interface that provides details on how UnixGlob should discriminate between different Paths.
+ * Instances of this interface will be handed either real directories or real files after symlink
+ * resolution and excluding special files.
+ */
+@CheckReturnValue
+public interface UnixGlobPathDiscriminator {
+
+  /**
+   * Determine if UnixGlob should enumerate entries in this directory and traverse it during
+   * recursive globbing. Defaults to true.
+   */
+  default boolean shouldTraverseDirectory(Path path) {
+    return true;
+  }
+
+  /**
+   * Determine if UnixGlob should include the given path in a {@code List<Path>} result. Defaults to
+   * true;
+   */
+  default boolean shouldIncludePathInResult(Path path, boolean isDirectory) {
+    return true;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/BUILD b/src/test/java/com/google/devtools/build/lib/packages/BUILD
index 61ef029..12a0a08 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/packages/BUILD
@@ -70,6 +70,7 @@
         "//src/main/java/com/google/devtools/build/lib/exec:test_policy",
         "//src/main/java/com/google/devtools/build/lib/packages",
         "//src/main/java/com/google/devtools/build/lib/packages:exec_group",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber",
         "//src/main/java/com/google/devtools/build/lib/pkgcache",
         "//src/main/java/com/google/devtools/build/lib/runtime/commands",
         "//src/main/java/com/google/devtools/build/lib/skyframe:tests_for_target_pattern_value",
diff --git a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
index b830b661..786942f 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
@@ -142,7 +142,7 @@
   @Test
   public void testIgnoredDirectory() throws Exception {
     createCache(PathFragment.create("isolated/foo"));
-    List<Path> paths = cache.safeGlobUnsorted("**/*.js", true).get();
+    List<Path> paths = cache.safeGlobUnsorted("**/*.js", Globber.Operation.FILES).get();
     assertPathsAre(
         paths,
         "/workspace/isolated/first.js",
@@ -153,7 +153,7 @@
 
   @Test
   public void testSafeGlob() throws Exception {
-    List<Path> paths = cache.safeGlobUnsorted("*.js", false).get();
+    List<Path> paths = cache.safeGlobUnsorted("*.js", Globber.Operation.FILES_AND_DIRS).get();
     assertPathsAre(paths,
         "/workspace/isolated/first.js", "/workspace/isolated/second.js");
   }
@@ -161,7 +161,9 @@
   @Test
   public void testSafeGlobInvalidPattern() throws Exception {
     String invalidPattern = "Foo?.txt";
-    assertThrows(BadGlobException.class, () -> cache.safeGlobUnsorted(invalidPattern, false).get());
+    assertThrows(
+        BadGlobException.class,
+        () -> cache.safeGlobUnsorted(invalidPattern, Globber.Operation.FILES_AND_DIRS).get());
   }
 
   @Test
@@ -181,38 +183,53 @@
     assertThat(cache.getKeySet()).isEmpty();
 
     cache.getGlobUnsorted("*.java");
-    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false));
+    assertThat(cache.getKeySet())
+        .containsExactly(Pair.of("*.java", Globber.Operation.FILES_AND_DIRS));
 
     cache.getGlobUnsorted("*.java");
-    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false));
+    assertThat(cache.getKeySet())
+        .containsExactly(Pair.of("*.java", Globber.Operation.FILES_AND_DIRS));
 
     cache.getGlobUnsorted("*.js");
-    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false));
+    assertThat(cache.getKeySet())
+        .containsExactly(
+            Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+            Pair.of("*.js", Globber.Operation.FILES_AND_DIRS));
 
-    cache.getGlobUnsorted("*.java", true);
-    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false),
-        Pair.of("*.java", true));
+    cache.getGlobUnsorted("*.java", Globber.Operation.FILES);
+    assertThat(cache.getKeySet())
+        .containsExactly(
+            Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+            Pair.of("*.js", Globber.Operation.FILES_AND_DIRS),
+            Pair.of("*.java", Globber.Operation.FILES));
 
     assertThrows(BadGlobException.class, () -> cache.getGlobUnsorted("invalid?"));
-    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.js", false),
-        Pair.of("*.java", true));
+    assertThat(cache.getKeySet())
+        .containsExactly(
+            Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+            Pair.of("*.js", Globber.Operation.FILES_AND_DIRS),
+            Pair.of("*.java", Globber.Operation.FILES));
 
     cache.getGlobUnsorted("foo/first.*");
-    assertThat(cache.getKeySet()).containsExactly(Pair.of("*.java", false), Pair.of("*.java", true),
-        Pair.of("*.js", false), Pair.of("foo/first.*", false));
+    assertThat(cache.getKeySet())
+        .containsExactly(
+            Pair.of("*.java", Globber.Operation.FILES_AND_DIRS),
+            Pair.of("*.java", Globber.Operation.FILES),
+            Pair.of("*.js", Globber.Operation.FILES_AND_DIRS),
+            Pair.of("foo/first.*", Globber.Operation.FILES_AND_DIRS));
   }
 
   @Test
   public void testGlob() throws Exception {
-    assertEmpty(cache.globUnsorted(list("*.java"), NONE, false, true));
+    assertEmpty(cache.globUnsorted(list("*.java"), NONE, Globber.Operation.FILES, true));
 
-    assertThat(cache.globUnsorted(list("*.*"), NONE, false, true))
+    assertThat(cache.globUnsorted(list("*.*"), NONE, Globber.Operation.FILES, true))
         .containsExactly("first.js", "first.txt", "second.js", "second.txt");
 
-    assertThat(cache.globUnsorted(list("*.*"), list("first.js"), false, true))
+    assertThat(cache.globUnsorted(list("*.*"), list("first.js"), Globber.Operation.FILES, true))
         .containsExactly("first.txt", "second.js", "second.txt");
 
-    assertThat(cache.globUnsorted(list("*.txt", "first.*"), NONE, false, true))
+    assertThat(cache.globUnsorted(list("*.txt", "first.*"), NONE, Globber.Operation.FILES, true))
         .containsExactly("first.txt", "second.txt", "first.js");
   }
 
@@ -225,13 +242,17 @@
 
   @Test
   public void testSingleFileExclude_star() throws Exception {
-    assertThat(cache.globUnsorted(list("*"), list("first.txt"), false, true))
+    assertThat(
+            cache.globUnsorted(
+                list("*"), list("first.txt"), Globber.Operation.FILES_AND_DIRS, true))
         .containsExactly("BUILD", "bar", "first.js", "foo", "second.js", "second.txt");
   }
 
   @Test
   public void testSingleFileExclude_starStar() throws Exception {
-    assertThat(cache.globUnsorted(list("**"), list("first.txt"), false, true))
+    assertThat(
+            cache.globUnsorted(
+                list("**"), list("first.txt"), Globber.Operation.FILES_AND_DIRS, true))
         .containsExactly(
             "BUILD",
             "bar",
@@ -247,48 +268,78 @@
 
   @Test
   public void testExcludeAll_star() throws Exception {
-    assertThat(cache.globUnsorted(list("*"), list("*"), false, true)).isEmpty();
+    assertThat(cache.globUnsorted(list("*"), list("*"), Globber.Operation.FILES_AND_DIRS, true))
+        .isEmpty();
   }
 
   @Test
   public void testExcludeAll_star_noMatchesAnyway() throws Exception {
-    assertThat(cache.globUnsorted(list("nope"), list("*"), false, true)).isEmpty();
+    assertThat(cache.globUnsorted(list("nope"), list("*"), Globber.Operation.FILES_AND_DIRS, true))
+        .isEmpty();
   }
 
   @Test
   public void testExcludeAll_starStar() throws Exception {
-    assertThat(cache.globUnsorted(list("**"), list("**"), false, true)).isEmpty();
+    assertThat(cache.globUnsorted(list("**"), list("**"), Globber.Operation.FILES_AND_DIRS, true))
+        .isEmpty();
   }
 
   @Test
   public void testExcludeAll_manual() throws Exception {
-    assertThat(cache.globUnsorted(list("**"), list("*", "*/*", "*/*/*"), false, true)).isEmpty();
+    assertThat(
+            cache.globUnsorted(
+                list("**"), list("*", "*/*", "*/*/*"), Globber.Operation.FILES_AND_DIRS, true))
+        .isEmpty();
   }
 
   @Test
   public void testSingleFileExcludeDoesntMatch() throws Exception {
-    assertThat(cache.globUnsorted(list("first.txt"), list("nope.txt"), false, true))
+    assertThat(
+            cache.globUnsorted(
+                list("first.txt"), list("nope.txt"), Globber.Operation.FILES_AND_DIRS, true))
         .containsExactly("first.txt");
   }
 
   @Test
   public void testExcludeDirectory() throws Exception {
-    assertThat(cache.globUnsorted(list("foo/*"), NONE, true, true))
+    assertThat(cache.globUnsorted(list("foo/*"), NONE, Globber.Operation.FILES, true))
         .containsExactly("foo/first.js", "foo/second.js");
-    assertThat(cache.globUnsorted(list("foo/*"), list("foo"), false, true))
+    assertThat(
+            cache.globUnsorted(list("foo/*"), list("foo"), Globber.Operation.FILES_AND_DIRS, true))
         .containsExactly("foo/first.js", "foo/second.js");
   }
 
   @Test
   public void testChildGlobWithChildExclude() throws Exception {
-    assertThat(cache.globUnsorted(list("foo/*"), list("foo/*"), false, true)).isEmpty();
     assertThat(
-            cache.globUnsorted(list("foo/first.js", "foo/second.js"), list("foo/*"), false, true))
+            cache.globUnsorted(
+                list("foo/*"), list("foo/*"), Globber.Operation.FILES_AND_DIRS, true))
         .isEmpty();
-    assertThat(cache.globUnsorted(list("foo/first.js"), list("foo/first.js"), false, true))
+    assertThat(
+            cache.globUnsorted(
+                list("foo/first.js", "foo/second.js"),
+                list("foo/*"),
+                Globber.Operation.FILES_AND_DIRS,
+                true))
         .isEmpty();
-    assertThat(cache.globUnsorted(list("foo/first.js"), list("*/first.js"), false, true)).isEmpty();
-    assertThat(cache.globUnsorted(list("foo/first.js"), list("*/*"), false, true)).isEmpty();
+    assertThat(
+            cache.globUnsorted(
+                list("foo/first.js"), list("foo/first.js"), Globber.Operation.FILES_AND_DIRS, true))
+        .isEmpty();
+    assertThat(
+            cache.globUnsorted(
+                list("foo/first.js"), list("*/first.js"), Globber.Operation.FILES_AND_DIRS, true))
+        .isEmpty();
+    assertThat(
+            cache.globUnsorted(
+                list("foo/first.js"), list("*/*"), Globber.Operation.FILES_AND_DIRS, true))
+        .isEmpty();
+  }
+
+  @Test
+  public void testSubpackages() throws Exception {
+    assertThat(cache.globUnsorted(list("**"), list(), Globber.Operation.SUBPACKAGES, true))
+        .containsExactly("sub");
   }
 
   private void assertEmpty(Collection<?> glob) {
diff --git a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
index d3df2e4..ee56bb8 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
@@ -1264,7 +1264,7 @@
               executorService,
               -1,
               ThreadStateReceiver.NULL_INSTANCE);
-      assertThat(globCache.globUnsorted(include, exclude, false, true))
+      assertThat(globCache.globUnsorted(include, exclude, Globber.Operation.FILES_AND_DIRS, true))
           .containsExactlyElementsIn(expected);
     } finally {
       executorService.shutdownNow();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index 9a591cf..969be1e 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -170,6 +170,7 @@
         "//src/main/java/com/google/devtools/build/lib/exec:single_build_file_cache",
         "//src/main/java/com/google/devtools/build/lib/io:inconsistent_filesystem_exception",
         "//src/main/java/com/google/devtools/build/lib/packages",
+        "//src/main/java/com/google/devtools/build/lib/packages:globber",
         "//src/main/java/com/google/devtools/build/lib/packages/semantics",
         "//src/main/java/com/google/devtools/build/lib/pkgcache",
         "//src/main/java/com/google/devtools/build/lib/pkgcache:QueryTransitivePackagePreloader",
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java
index 7eac47f..d2e7753 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/GlobDescriptorTest.java
@@ -17,6 +17,7 @@
 
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Globber;
 import com.google.devtools.build.lib.skyframe.serialization.testutils.FsUtils;
 import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -38,13 +39,13 @@
                     Root.fromPath(FsUtils.TEST_FILESYSTEM.getPath("/packageRoot")),
                     PathFragment.create("subdir"),
                     "pattern",
-                    /*excludeDirs=*/ false),
+                    Globber.Operation.FILES_AND_DIRS),
                 GlobDescriptor.create(
                     PackageIdentifier.create("@bar", PathFragment.create("//foo")),
                     Root.fromPath(FsUtils.TEST_FILESYSTEM.getPath("/anotherPackageRoot")),
                     PathFragment.create("anotherSubdir"),
                     "pattern",
-                    /*excludeDirs=*/ true))
+                    Globber.Operation.FILES))
             .setVerificationFunction(GlobDescriptorTest::verifyEquivalent);
     FsUtils.addDependencies(serializationTester);
     serializationTester.runTests();
@@ -62,22 +63,24 @@
             Root.fromPath(FsUtils.TEST_FILESYSTEM.getPath("/packageRoot")),
             PathFragment.create("subdir"),
             "pattern",
-            /*excludeDirs=*/ false);
+            Globber.Operation.FILES_AND_DIRS);
 
-    GlobDescriptor sameCopy = GlobDescriptor.create(
-        original.getPackageId(),
-        original.getPackageRoot(),
-        original.getSubdir(),
-        original.getPattern(),
-        original.excludeDirs());
+    GlobDescriptor sameCopy =
+        GlobDescriptor.create(
+            original.getPackageId(),
+            original.getPackageRoot(),
+            original.getSubdir(),
+            original.getPattern(),
+            original.globberOperation());
     assertThat(sameCopy).isSameInstanceAs(original);
 
-    GlobDescriptor diffCopy = GlobDescriptor.create(
-        original.getPackageId(),
-        original.getPackageRoot(),
-        original.getSubdir(),
-        original.getPattern(),
-        !original.excludeDirs());
+    GlobDescriptor diffCopy =
+        GlobDescriptor.create(
+            original.getPackageId(),
+            original.getPackageRoot(),
+            original.getSubdir(),
+            original.getPattern(),
+            Globber.Operation.FILES);
     assertThat(diffCopy).isNotEqualTo(original);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java
index e451622..c66ec8a 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/GlobFunctionTest.java
@@ -32,6 +32,7 @@
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.events.NullEventHandler;
 import com.google.devtools.build.lib.io.InconsistentFilesystemException;
+import com.google.devtools.build.lib.packages.Globber;
 import com.google.devtools.build.lib.packages.RuleClassProvider;
 import com.google.devtools.build.lib.packages.WorkspaceFileValue;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
@@ -332,12 +333,17 @@
     // Each "equality group" forms a set of elements that are all equals() to one another,
     // and also produce the same hashCode.
     new EqualsTester()
-        .addEqualityGroup(runGlob(false, "no-such-file")) // Matches nothing.
-        .addEqualityGroup(runGlob(false, "BUILD"), runGlob(true, "BUILD")) // Matches BUILD.
-        .addEqualityGroup(runGlob(false, "**")) // Matches lots of things.
         .addEqualityGroup(
-            runGlob(false, "f*o/bar*"),
-            runGlob(false, "foo/bar*")) // Matches foo/bar and foo/barnacle.
+            runGlob("no-such-file", Globber.Operation.FILES_AND_DIRS)) // Matches nothing.
+        .addEqualityGroup(
+            runGlob("BUILD", Globber.Operation.FILES_AND_DIRS),
+            runGlob("BUILD", Globber.Operation.FILES)) // Matches BUILD.
+        .addEqualityGroup(
+            runGlob("**", Globber.Operation.FILES_AND_DIRS)) // Matches lots of things.
+        .addEqualityGroup(
+            runGlob("f*o/bar*", Globber.Operation.FILES_AND_DIRS),
+            runGlob(
+                "foo/bar*", Globber.Operation.FILES_AND_DIRS)) // Matches foo/bar and foo/barnacle.
         .testEquals();
   }
 
@@ -417,11 +423,11 @@
   }
 
   private void assertGlobMatches(String pattern, String... expecteds) throws Exception {
-    assertGlobMatches(false, pattern, expecteds);
+    assertGlobMatches(pattern, Globber.Operation.FILES_AND_DIRS, expecteds);
   }
 
-  private void assertGlobMatches(boolean excludeDirs, String pattern, String... expecteds)
-      throws Exception {
+  private void assertGlobMatches(
+      String pattern, Globber.Operation globberOperation, String... expecteds) throws Exception {
     // The order requirement is not strictly necessary -- a change to GlobFunction semantics that
     // changes the output order is fine, but we require that the order be the same here to detect
     // potential non-determinism in output order, which would be bad.
@@ -430,27 +436,28 @@
     // directories.
     assertThat(
             Iterables.transform(
-                runGlob(excludeDirs, pattern).getMatches().toList(), Functions.toStringFunction()))
+                runGlob(pattern, globberOperation).getMatches().toList(),
+                Functions.toStringFunction()))
         .containsExactlyElementsIn(ImmutableList.copyOf(expecteds))
         .inOrder();
   }
 
   private void assertGlobWithoutDirsMatches(String pattern, String... expecteds) throws Exception {
-    assertGlobMatches(true, pattern, expecteds);
+    assertGlobMatches(pattern, Globber.Operation.FILES, expecteds);
   }
 
   private void assertGlobsEqual(String pattern1, String pattern2) throws Exception {
-    GlobValue value1 = runGlob(false, pattern1);
-    GlobValue value2 = runGlob(false, pattern2);
+    GlobValue value1 = runGlob(pattern1, Globber.Operation.FILES_AND_DIRS);
+    GlobValue value2 = runGlob(pattern2, Globber.Operation.FILES_AND_DIRS);
     new EqualsTester()
         .addEqualityGroup(value1, value2)
         .testEquals();
   }
 
-  private GlobValue runGlob(boolean excludeDirs, String pattern) throws Exception {
+  private GlobValue runGlob(String pattern, Globber.Operation globberOperation) throws Exception {
     SkyKey skyKey =
         GlobValue.key(
-            PKG_ID, Root.fromPath(root), pattern, excludeDirs, PathFragment.EMPTY_FRAGMENT);
+            PKG_ID, Root.fromPath(root), pattern, globberOperation, PathFragment.EMPTY_FRAGMENT);
     EvaluationResult<SkyValue> result =
         evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
     if (result.hasError()) {
@@ -533,7 +540,11 @@
         InvalidGlobPatternException.class,
         () ->
             GlobValue.key(
-                PKG_ID, Root.fromPath(root), pattern, false, PathFragment.EMPTY_FRAGMENT));
+                PKG_ID,
+                Root.fromPath(root),
+                pattern,
+                Globber.Operation.FILES_AND_DIRS,
+                PathFragment.EMPTY_FRAGMENT));
   }
 
   /**
@@ -681,7 +692,12 @@
     differencer.inject(ImmutableMap.of(FileValue.key(pkgRootedPath), pkgDirValue));
     String expectedMessage = "/root/workspace/pkg is no longer an existing directory";
     SkyKey skyKey =
-        GlobValue.key(PKG_ID, Root.fromPath(root), "*/foo", false, PathFragment.EMPTY_FRAGMENT);
+        GlobValue.key(
+            PKG_ID,
+            Root.fromPath(root),
+            "*/foo",
+            Globber.Operation.FILES_AND_DIRS,
+            PathFragment.EMPTY_FRAGMENT);
     EvaluationResult<GlobValue> result =
         evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
     assertThat(result.hasError()).isTrue();
@@ -705,7 +721,12 @@
             DirectoryListingStateValue.key(fooBarDirRootedPath), fooBarDirListingValue));
     String expectedMessage = "/root/workspace/pkg/foo/bar/wiz is no longer an existing directory.";
     SkyKey skyKey =
-        GlobValue.key(PKG_ID, Root.fromPath(root), "**/wiz", false, PathFragment.EMPTY_FRAGMENT);
+        GlobValue.key(
+            PKG_ID,
+            Root.fromPath(root),
+            "**/wiz",
+            Globber.Operation.FILES_AND_DIRS,
+            PathFragment.EMPTY_FRAGMENT);
     EvaluationResult<GlobValue> result =
         evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
     assertThat(result.hasError()).isTrue();
@@ -776,7 +797,11 @@
         "readdir and stat disagree about whether " + fileRootedPath.asPath() + " is a symlink";
     SkyKey skyKey =
         GlobValue.key(
-            PKG_ID, Root.fromPath(root), "foo/bar/wiz/*", false, PathFragment.EMPTY_FRAGMENT);
+            PKG_ID,
+            Root.fromPath(root),
+            "foo/bar/wiz/*",
+            Globber.Operation.FILES_AND_DIRS,
+            PathFragment.EMPTY_FRAGMENT);
     EvaluationResult<GlobValue> result =
         evaluator.evaluate(ImmutableList.of(skyKey), EVALUATION_OPTIONS);
     assertThat(result.hasError()).isTrue();
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/BUILD b/src/test/java/com/google/devtools/build/lib/vfs/BUILD
index 76fdd0d..f764760 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/vfs/BUILD
@@ -67,6 +67,7 @@
         "//src/test/java/com/google/devtools/build/lib/testutil:TestThread",
         "//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
         "//src/test/java/com/google/devtools/build/lib/vfs/util",
+        "//src/test/java/com/google/devtools/build/lib/vfs/util:test_glob_path_discriminator",
         "//third_party:guava",
         "//third_party:guava-testlib",
         "//third_party:junit4",
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java b/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java
index 28f6ab5..0463f57 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/GlobTest.java
@@ -16,12 +16,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
 
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Uninterruptibles;
 import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import com.google.devtools.build.lib.vfs.util.TestUnixGlobPathDiscriminator;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -37,15 +37,14 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/**
- * Tests {@link UnixGlob}
- */
+/** Tests {@link UnixGlob} */
 @RunWith(JUnit4.class)
 public class GlobTest {
 
@@ -55,7 +54,7 @@
   private Path throwOnStat = null;
 
   @Before
-  public final void initializeFileSystem() throws Exception  {
+  public final void initializeFileSystem() throws Exception {
     fs =
         new InMemoryFileSystem(DigestHashFunction.SHA256) {
           @Override
@@ -77,10 +76,12 @@
           }
         };
     tmpPath = fs.getPath("/globtmp");
-    for (String dir : ImmutableList.of("foo/bar/wiz",
-                         "foo/barnacle/wiz",
-                         "food/barnacle/wiz",
-                         "fool/barnacle/wiz")) {
+
+    final ImmutableList<String> directories =
+        ImmutableList.of(
+            "foo/bar/wiz", "foo/barnacle/wiz", "food/barnacle/wiz", "fool/barnacle/wiz");
+
+    for (String dir : directories) {
       FileSystemUtils.createDirectoryAndParents(tmpPath.getRelative(dir));
     }
     FileSystemUtils.createEmptyFile(tmpPath.getRelative("foo/bar/wiz/file"));
@@ -93,7 +94,7 @@
 
   @Test
   public void testQuestionMarkMatch() throws Exception {
-    assertGlobMatches("foo?", /* => */"food", "fool");
+    assertGlobMatches("foo?", /* => */ "food", "fool");
   }
 
   @Test
@@ -103,48 +104,48 @@
 
   @Test
   public void testStartsWithStar() throws Exception {
-    assertGlobMatches("*oo", /* => */"foo");
+    assertGlobMatches("*oo", /* => */ "foo");
   }
 
   @Test
   public void testStartsWithStarWithMiddleStar() throws Exception {
-    assertGlobMatches("*f*o", /* => */"foo");
+    assertGlobMatches("*f*o", /* => */ "foo");
   }
 
   @Test
   public void testEndsWithStar() throws Exception {
-    assertGlobMatches("foo*", /* => */"foo", "food", "fool");
+    assertGlobMatches("foo*", /* => */ "foo", "food", "fool");
   }
 
   @Test
   public void testEndsWithStarWithMiddleStar() throws Exception {
-    assertGlobMatches("f*oo*", /* => */"foo", "food", "fool");
+    assertGlobMatches("f*oo*", /* => */ "foo", "food", "fool");
   }
 
   @Test
   public void testMiddleStar() throws Exception {
-    assertGlobMatches("f*o", /* => */"foo");
+    assertGlobMatches("f*o", /* => */ "foo");
   }
 
   @Test
   public void testTwoMiddleStars() throws Exception {
-    assertGlobMatches("f*o*o", /* => */"foo");
+    assertGlobMatches("f*o*o", /* => */ "foo");
   }
 
   @Test
   public void testSingleStarPatternWithNamedChild() throws Exception {
-    assertGlobMatches("*/bar", /* => */"foo/bar");
+    assertGlobMatches("*/bar", /* => */ "foo/bar");
   }
 
   @Test
   public void testSingleStarPatternWithChildGlob() throws Exception {
-    assertGlobMatches("*/bar*", /* => */
-        "foo/bar", "foo/barnacle", "food/barnacle", "fool/barnacle");
+    assertGlobMatches(
+        "*/bar*", /* => */ "foo/bar", "foo/barnacle", "food/barnacle", "fool/barnacle");
   }
 
   @Test
   public void testSingleStarAsChildGlob() throws Exception {
-    assertGlobMatches("foo/*/wiz", /* => */"foo/bar/wiz", "foo/barnacle/wiz");
+    assertGlobMatches("foo/*/wiz", /* => */ "foo/bar/wiz", "foo/barnacle/wiz");
   }
 
   @Test
@@ -160,10 +161,93 @@
   }
 
   @Test
+  public void testFilteredResults_noDirs() throws Exception {
+
+    assertThat(
+            new UnixGlob.Builder(tmpPath)
+                .addPatterns("**")
+                .setPathDiscriminator(
+                    new TestUnixGlobPathDiscriminator(
+                        p -> /*traversalPredicate=*/ true,
+                        /*resultPredicate=*/ (p, isDir) -> !isDir))
+                .globInterruptible())
+        .containsExactlyElementsIn(resolvePaths("foo/bar/wiz/file"));
+  }
+
+  @Test
+  public void testFilteredResults_noFiles() throws Exception {
+    assertThat(
+            new UnixGlob.Builder(tmpPath)
+                .addPatterns("**")
+                .setPathDiscriminator(
+                    new TestUnixGlobPathDiscriminator(
+                        /*traversalPredicate=*/ p -> true,
+                        /*resultPredicate=*/ (p, isDir) -> isDir))
+                .globInterruptible())
+        .containsExactlyElementsIn(
+            resolvePaths(
+                "",
+                "foo",
+                "foo/bar",
+                "foo/bar/wiz",
+                "foo/barnacle",
+                "foo/barnacle/wiz",
+                "food",
+                "food/barnacle",
+                "food/barnacle/wiz",
+                "fool",
+                "fool/barnacle",
+                "fool/barnacle/wiz"));
+  }
+
+  @Test
+  public void testFilteredResults_pathMatch() throws Exception {
+
+    Path wanted = tmpPath.getRelative("food/barnacle/wiz");
+
+    assertThat(
+            new UnixGlob.Builder(tmpPath)
+                .addPatterns("**")
+                .setPathDiscriminator(
+                    new TestUnixGlobPathDiscriminator(
+                        /*traversalPredicate=*/ p -> true,
+                        /*resultPredicate=*/ (path, isDir) -> path.equals(wanted)))
+                .globInterruptible())
+        .containsExactly(wanted);
+  }
+
+  @Test
+  public void testTraversal_onlyFoo() throws Exception {
+    // Use a directory traversal filter to only walk the root dir and "foo", but not "fool or "food"
+    // So we'll end up the directories, "fool" and "food", but not sub-dirs.
+    assertThat(
+            new UnixGlob.Builder(tmpPath)
+                .addPatterns("**")
+                .setPathDiscriminator(
+                    new TestUnixGlobPathDiscriminator(
+                        /*traversalPredicate=*/ path ->
+                            path.equals(tmpPath)
+                                || path.getPathString().contains("foo/")
+                                || path.getPathString().endsWith("foo"),
+                        /*resultPredicate=*/ (x, isDir) -> true))
+                .globInterruptible())
+        .containsExactlyElementsIn(
+            resolvePaths(
+                "",
+                "foo",
+                "foo/bar",
+                "foo/bar/wiz",
+                "foo/bar/wiz/file",
+                "foo/barnacle",
+                "foo/barnacle/wiz",
+                "fool",
+                "food"));
+  }
+
+  @Test
   public void testGlobWithNonExistentBase() throws Exception {
-    Collection<Path> globResult = UnixGlob.forPath(fs.getPath("/does/not/exist"))
-        .addPattern("*.txt")
-        .globInterruptible();
+    Collection<Path> globResult =
+        UnixGlob.forPath(fs.getPath("/does/not/exist")).addPattern("*.txt").globInterruptible();
     assertThat(globResult).isEmpty();
   }
 
@@ -172,27 +256,19 @@
     assertGlobMatches("foo/bar/wiz/file/*" /* => nothing */);
   }
 
-  private void assertGlobMatches(String pattern, String... expecteds)
-      throws Exception {
+  private void assertGlobMatches(String pattern, String... expecteds) throws Exception {
     assertGlobMatches(Collections.singleton(pattern), expecteds);
   }
 
-  private void assertGlobMatches(Collection<String> pattern,
-                                 String... expecteds)
-      throws Exception {
-    assertThat(
-        new UnixGlob.Builder(tmpPath)
-            .addPatterns(pattern)
-            .globInterruptible())
-    .containsExactlyElementsIn(resolvePaths(expecteds));
+  private void assertGlobMatches(Collection<String> pattern, String... expecteds) throws Exception {
+    assertThat(new UnixGlob.Builder(tmpPath).addPatterns(pattern).globInterruptible())
+        .containsExactlyElementsIn(resolvePaths(expecteds));
   }
 
   private Set<Path> resolvePaths(String... relativePaths) {
     Set<Path> expectedFiles = new HashSet<>();
     for (String expected : relativePaths) {
-      Path file = expected.equals(".")
-          ? tmpPath
-          : tmpPath.getRelative(expected);
+      Path file = expected.equals(".") ? tmpPath : tmpPath.getRelative(expected);
       expectedFiles.add(file);
     }
     return expectedFiles;
@@ -270,9 +346,7 @@
     assertIllegalPattern("foo//bar");
   }
 
-  /**
-   * Tests that globs can contain Java regular expression special characters
-   */
+  /** Tests that globs can contain Java regular expression special characters */
   @Test
   public void testSpecialRegexCharacter() throws Exception {
     Path tmpPath2 = fs.getPath("/globtmp2");
@@ -357,19 +431,18 @@
 
   @Test
   public void testMultiplePatternsWithOverlap() throws Exception {
-    assertGlobMatchesAnyOrder(Lists.newArrayList("food", "foo?"),
-                              "food", "fool");
-    assertGlobMatchesAnyOrder(Lists.newArrayList("food", "?ood", "f??d"),
-                              "food");
-    assertThat(resolvePaths("food", "fool", "foo")).containsExactlyElementsIn(
-        new UnixGlob.Builder(tmpPath).addPatterns("food", "xxx", "*").glob());
-
+    assertGlobMatchesAnyOrder(Lists.newArrayList("food", "foo?"), "food", "fool");
+    assertGlobMatchesAnyOrder(Lists.newArrayList("food", "?ood", "f??d"), "food");
+    assertThat(resolvePaths("food", "fool", "foo"))
+        .containsExactlyElementsIn(
+            new UnixGlob.Builder(tmpPath).addPatterns("food", "xxx", "*").glob());
   }
 
-  private void assertGlobMatchesAnyOrder(ArrayList<String> patterns,
-                                         String... paths) throws Exception {
-    assertThat(resolvePaths(paths)).containsExactlyElementsIn(
-        new UnixGlob.Builder(tmpPath).addPatterns(patterns).globInterruptible());
+  private void assertGlobMatchesAnyOrder(ArrayList<String> patterns, String... paths)
+      throws Exception {
+    assertThat(resolvePaths(paths))
+        .containsExactlyElementsIn(
+            new UnixGlob.Builder(tmpPath).addPatterns(patterns).globInterruptible());
   }
 
   private void assertIllegalPattern(String pattern) throws Exception {
@@ -419,16 +492,20 @@
     Predicate<Path> interrupterPredicate =
         new Predicate<Path>() {
           @Override
-          public boolean apply(Path input) {
+          public boolean test(Path input) {
             mainThread.interrupt();
             return true;
           }
         };
 
+    UnixGlobPathDiscriminator interrupterDiscriminator =
+        new TestUnixGlobPathDiscriminator(
+            /*traversalPredicate=*/ interrupterPredicate, /*resultPredicate=*/ (x, isDir) -> true);
+
     Future<?> globResult =
         new UnixGlob.Builder(tmpPath)
             .addPattern("**")
-            .setDirectoryFilter(interrupterPredicate)
+            .setPathDiscriminator(interrupterDiscriminator)
             .setExecutor(executor)
             .globAsync();
     assertThrows(InterruptedException.class, () -> globResult.get());
@@ -450,20 +527,25 @@
     final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
     final AtomicBoolean sentInterrupt = new AtomicBoolean(false);
 
-    Predicate<Path> interrupterPredicate = new Predicate<Path>() {
-      @Override
-      public boolean apply(Path input) {
-        if (!sentInterrupt.getAndSet(true)) {
-          mainThread.interrupt();
-        }
-        return true;
-      }
-    };
+    Predicate<Path> interrupterPredicate =
+        new Predicate<Path>() {
+          @Override
+          public boolean test(Path input) {
+            if (!sentInterrupt.getAndSet(true)) {
+              mainThread.interrupt();
+            }
+            return true;
+          }
+        };
+
+    UnixGlobPathDiscriminator interrupterDiscriminator =
+        new TestUnixGlobPathDiscriminator(
+            /*traversalPredicate=*/ interrupterPredicate, /*resultPredicate=*/ (x, isDir) -> true);
 
     List<Path> result =
         new UnixGlob.Builder(tmpPath)
             .addPatterns("**", "*")
-            .setDirectoryFilter(interrupterPredicate)
+            .setPathDiscriminator(interrupterDiscriminator)
             .setExecutor(executor)
             .glob();
 
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
index ab515c0..d7e0bd3 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
@@ -20,6 +20,7 @@
 import com.google.common.testing.EqualsTester;
 import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.vfs.util.FileSystems;
+import com.google.devtools.build.lib.vfs.util.TestUnixGlobPathDiscriminator;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -133,16 +134,17 @@
 
     Collection<Path> onlyFiles =
         UnixGlob.forPath(fs.getPath(tmpDir.getPath()))
-        .addPattern("*")
-        .setExcludeDirectories(true)
-        .globInterruptible();
+            .addPattern("*")
+            .setPathDiscriminator(
+                new TestUnixGlobPathDiscriminator(p -> true, (p, isDir) -> !isDir))
+            .globInterruptible();
     assertPathSet(onlyFiles, aFile.getPath());
 
     Collection<Path> directoriesToo =
         UnixGlob.forPath(fs.getPath(tmpDir.getPath()))
-        .addPattern("*")
-        .setExcludeDirectories(false)
-        .globInterruptible();
+            .addPattern("*")
+            .setPathDiscriminator(new TestUnixGlobPathDiscriminator(p -> true, (p, isDir) -> true))
+            .globInterruptible();
     assertPathSet(directoriesToo, aFile.getPath(), aDirectory.getPath());
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java b/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java
index 1c2ac8a..07abb57 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/RecursiveGlobTest.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Lists;
 import com.google.devtools.build.lib.clock.BlazeClock;
 import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import com.google.devtools.build.lib.vfs.util.TestUnixGlobPathDiscriminator;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
@@ -136,10 +137,12 @@
 
   @Test
   public void testRecursiveGlobsAreOptimized() throws Exception {
-    long numGlobTasks = new UnixGlob.Builder(tmpPath)
-        .addPattern("**")
-        .setExcludeDirectories(false)
-        .globInterruptibleAndReturnNumGlobTasksForTesting();
+    long numGlobTasks =
+        new UnixGlob.Builder(tmpPath)
+            .addPattern("**")
+            .setPathDiscriminator(
+                new TestUnixGlobPathDiscriminator(p -> true, (p, isDir) -> !isDir))
+            .globInterruptibleAndReturnNumGlobTasksForTesting();
 
     // The old glob implementation used to use 41 total glob tasks.
     // Yes, checking for an exact value here is super brittle, but it lets us catch performance
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD b/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD
index 379d2ea..30ef7d8 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/vfs/util/BUILD
@@ -26,7 +26,10 @@
 java_library(
     name = "util_internal",
     testonly = 1,
-    srcs = glob(["*.java"]),
+    srcs = [
+        "FileSystems.java",
+        "FsApparatus.java",
+    ],
     deps = [
         "//src/main/java/com/google/devtools/build/lib/clock",
         "//src/main/java/com/google/devtools/build/lib/util:os",
@@ -38,3 +41,13 @@
         "//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
     ],
 )
+
+java_library(
+    name = "test_glob_path_discriminator",
+    testonly = 1,
+    srcs = ["TestUnixGlobPathDiscriminator.java"],
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//third_party:error_prone_annotations",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/TestUnixGlobPathDiscriminator.java b/src/test/java/com/google/devtools/build/lib/vfs/util/TestUnixGlobPathDiscriminator.java
new file mode 100644
index 0000000..50d81d6
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/util/TestUnixGlobPathDiscriminator.java
@@ -0,0 +1,47 @@
+// Copyright 2022 The Bazel 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 com.google.devtools.build.lib.vfs.util;
+
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.UnixGlobPathDiscriminator;
+import com.google.errorprone.annotations.CheckReturnValue;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+
+/**
+ * Test version of UnixGlobPathDiscriminator that accepts predicate/bipredicate for handling
+ * specific use-cases without creating a new class.
+ */
+@CheckReturnValue
+public final class TestUnixGlobPathDiscriminator implements UnixGlobPathDiscriminator {
+
+  private final Predicate<Path> traversalPredicate;
+  private final BiPredicate<Path, Boolean> resultPredicate;
+
+  public TestUnixGlobPathDiscriminator(
+      Predicate<Path> traversalPredicate, BiPredicate<Path, Boolean> resultPredicate) {
+    this.traversalPredicate = traversalPredicate;
+    this.resultPredicate = resultPredicate;
+  }
+
+  @Override
+  public boolean shouldTraverseDirectory(Path path) {
+    return traversalPredicate.test(path);
+  }
+
+  @Override
+  public boolean shouldIncludePathInResult(Path path, boolean isDirectory) {
+    return resultPredicate.test(path, isDirectory);
+  }
+}