All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sbt.nio.file.Glob.scala Maven / Gradle / Ivy

There is a newer version: 1.10.0
Show newest version
/*
 * sbt IO
 * Copyright Scala Center, Lightbend, and Mark Harrah
 *
 * Licensed under Apache License 2.0
 * SPDX-License-Identifier: Apache-2.0
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package sbt.nio.file

import java.io.File
import java.nio.file.{ FileSystems, Path, Paths }
import java.util

import sbt.io._
import sbt.nio.file.PathFilter.PathFilterExtensions
import sbt.nio.file.RelativeGlob.{ PathComponent, SingleComponentMatcher }

import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.util.Properties
import scala.util.matching.Regex

/**
 * Represents a query for a path on the file system. Instances of `Glob` can be combined together
 * to build a more complicated query. Similar to the path name components of a `Path`, a `Glob` has
 * components and matching is done from left to right component-wise. Each component is matched
 * using a
 * [[https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-
 * PathMatcher]].
 * For example,
 * {{{
 *   val baseGlob = Glob("/foo/bar")
 *   baseGlob.matches(Paths.get("/foo/bar")) // true
 *   baseGlob.matches(Paths.get("/foo")) // false
 *   baseGlob.matches(Paths.get("/foo/bar/baz")) // false
 *
 *   val children = baseGlob / AnyPath
 *   children.matches(Paths.get("/foo/bar")) // false
 *   children.matches(Paths.get("/foo/bar/baz")) // true
 *   children.matches(Paths.get("/foo/bar/baz/buzz")) false
 *
 *   val secondGenerationChildren = children / AnyPath
 *   secondGeneration.matches(Paths.get("/foo/bar/baz")) // false
 *   secondGeneration.matches(Paths.get("/foo/bar/baz/buzz")) // true
 *   secondGeneration.matches(Paths.get("/foo/bar/baz/buzz/fizz")) // false
 * }}}
 * where [[AnyPath]] matches any path name.
 *
 * Globs can be recursive and are constructed using the [[RecursiveGlob]]:
 * {{{
 *   val baseGlob = Glob("/foo/bar")
 *   val allDescendants = baseGlob / RecursiveGlob
 *   allDescendants.matches(Paths.get("/foo/bar")) // false
 *   allDescendants.matches(Paths.get("/foo/bar/baz/buzz.txt")) // true
 * }}}
 *
 * They can also filter on the basis file names:
 * {{{
 *   val baseGlob = Glob("/foo/bar")
 *   val allScalaSources = baseGlob / RecursiveGlob / "*.scala"
 *   allScalaSources.matches(Paths.get("/foo/bar/Foo.scala")) // true
 *   allScalaSources.matches(Paths.get("/foo/bar/baz/fizz/buzz/Bar.scala")) // true
 *   allScalaSources.matches(Paths.get("/foo/bar/baz/fizz/buzz/Buzz.java")) // false
 *
 *   val allScalaAndJavaSources = baseGlob / RecursiveGlob / "*.{java,scala}"
 *   allScalaAndJavaSources.matches(Paths.get("/foo/bar/baz/fizz/buzz/Bar.scala")) // true
 *   allScalaAndJavaSources.matches(Paths.get("/foo/bar/baz/fizz/buzz/Buzz.java")) // true
 * }}}
 */
sealed trait Glob {

  /**
   * Indicates whether a path matches the pattern specified by this [[Glob]].
   *
   * @param path the path to match
   * @return true it the path matches.
   */
  def matches(path: Path): Boolean
}

object Glob {
  private[this] def comp[T](left: T, right: T)(implicit ordering: Ordering[T]): Int =
    ordering.compare(left, right)

  /**
   * Construct a single file [[Glob]]. The resulting [[Glob]] will only match the input file.
   * @param file the single path to match.
   * @return a Glob whose [[Glob.matches]] method only accepts the input path.
   */
  def apply(file: File): Glob = if (file.isAbsolute) Root(file.toPath) else apply(file.toString)

  /**
   * Construct a Glob with a file prefix and relativeGlob glob string. For example:
   * {{{
   *   val glob = Glob(file("foo/bar"), "*.txt") // matches all txt files in foo/bar
   *   val recursiveGlob = Glob(file("foo/bar"), "**/*.txt") // recursively matches all text files in foo/bar
   * }}}
   * @param file the prefix of the glob pattern. This can be an absolute or relativeGlob file.
   * @param relative the relativeGlob glob string
   * @return a Glob that matches the file prefix with
   */
  def apply(file: File, relative: String): Glob =
    Glob(file, RelativeGlob.parse(relative, isRegex = false))

  /**
   * Construct a [[Glob]] with a file prefix and a [[RelativeGlob]] suffix. In order for a path to
   * match the output [[Glob]], it must start with `file` and its relative path with respect to
   * `file` must match the `relativeGlob`. For example:
   * {{{
   *   val txtFiles = Glob(file("foo/bar"), * / "*.txt") // matches all text files in any subdirectory of foo/bar
   *   val allChildren = Glob(file("/foo/bar"), **) // matches all descendants of /foo/bar
   *   val scalaSources = Glob(file("src/main/scala", ** / "*.scala")) // matches scala source file
   * }}}
   *
   * @param file the prefix of the glob pattern. This can be an absolute or relativeGlob file.
   * @param relativeGlob the [[RelativeGlob]] to append to the file prefix
   * @return a Glob that matches a path with the same prefix as `file` and whose suffix matches
   *         `relativeGlob`.
   */
  def apply(file: File, relativeGlob: RelativeGlob): Glob = Glob(file.toPath) / relativeGlob

  /**
   * Appends a relative glob specified by a regex pattern to an existing [[Glob]]. For example:
   * {{{
   *   val scalaSources = Glob(file("foo/bar"), "^[^.].*\\.scala".r
   *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/.hidden.scala")) // false
   * }}}
   *
   * Note that on Windows, `\` will be treated as a regex escape character but `/` will be treated
   * as a path separator.
   *
   * @param file the file prefix of the pattern
   * @param regex the pattern to apply
   * @return a Glob with the new [[RelativeGlob]] appended to the query.
   */
  def apply(file: File, regex: Regex): Glob =
    Glob(file, RelativeGlob.parse(regex.regex, isRegex = true))

  /**
   * Construct a single path Glob. The resulting glob
   * @param path the single path to match.
   * @return a Glob whose [[Glob.matches]] method only accepts the input path.
   */
  def apply(path: Path): Glob = if (path.isAbsolute) Root(path) else apply(path.toString)

  /**
   * Construct a Glob with a path prefix and a `RelativeGlob`. For example:
   * {{{
   *   val txtFiles = Glob(Paths.get("foo/bar"), * / "*.txt") // matches all text files in any subdirectory of foo/bar
   *   val allChildren = Glob(Paths.get("/foo/bar"), **) // matches all descendants of /foo/bar
   *   val scalaSources = Glob(Paths.get("src/main/scala", ** / "*.scala")) // matches scala source file
   * }}}
   * @param path the prefix of the glob pattern. This can be an absolute or relativeGlob file.
   * @param relativeGlob the [[RelativeGlob]] to append to the file prefix
   * @return a Glob that matches the file prefix with
   */
  def apply(path: Path, relativeGlob: RelativeGlob): Glob = Glob(path) / relativeGlob

  /**
   * Construct a Glob with a file prefix and relativeGlob glob string. For example:
   * {{{
   *   val glob = Glob(file("foo/bar"), "*.txt") // matches all text files in foo/bar
   * }}}
   * @param path the prefix of the glob pattern. This can be an absolute or relativeGlob file.
   * @param relative the relativeGlob glob string
   * @return a Glob that matches the file prefix with
   */
  def apply(path: Path, relative: String): Glob = Glob(path) / relative

  /**
   * Appends a relative glob specified by a regex pattern to an existing [[Glob]]. For example:
   * {{{
   *   val scalaSources = Glob(file("foo/bar"), "^[^.].*\\.scala".r
   *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/.hidden.scala")) // false
   * }}}
   *
   * Note that on Windows, `\` will be treated as a regex escape character but `/` will be treated
   * as a path separator.
   *
   * @param path the file path prefix of the pattern
   * @param regex the pattern to apply
   * @return a Glob with the new [[RelativeGlob]] appended to the query.
   */
  def apply(path: Path, regex: Regex): Glob =
    Glob(path, RelativeGlob.parse(regex.regex, isRegex = true))

  /**
   * Construct a glob from a string. It can represent a full path. Any `/` characters in a pathname
   * are automatically converted to `\\` on windows.
   * {{{
   *   val absoluteGlob = Glob("/foo/bar/**/*.txt")
   *   val relativeGlob = Glob("*/*.txt")
   * }}}
   * @param glob the string to convert to a glob
   * @return a parsed `Glob` for the input. May throw if the input cannot be parsed into a `Glob`.
   */
  def apply(glob: String): Glob = apply(glob, isRegex = false)

  private[sbt] def apply(glob: String, isRegex: Boolean): Glob = {
    val parts = splitter(glob, isRegex)
    @tailrec def fullGlob(path: Path, rest: List[String]): Glob = {
      rest match {
        case component :: tail if !hasMeta(component) => fullGlob(path.resolve(component), tail)
        case Nil                                      => Root(path)
        case _                                        => Pattern(path, RelativeGlob(rest: _*))
      }
    }
    def addRegexPrefix(parts: List[String]): List[String] =
      if (isRegex) parts.map(p => s"regex:$p") else parts
    parts match {
      case h :: rest if !hasMeta(h) =>
        val base = if (isWin) {
          if (h.endsWith(":")) Paths.get(h + File.separator) else Paths.get(h)
        } else {
          if (h.isEmpty) Paths.get("/") else Paths.get(h)
        }
        base match {
          case p if p.isAbsolute => fullGlob(p, addRegexPrefix(rest))
          case _                 => RelativeGlob(addRegexPrefix(parts): _*)
        }
      case _ => RelativeGlob(addRegexPrefix(parts): _*)
    }
  }

  /**
   * Provides a default ordering for glob. Globs with a path prefix precede relative globs and are
   * sorted by their path prefix.
   */
  implicit object ordering extends Ordering[Glob] {
    override def compare(left: Glob, right: Glob): Int = left match {
      case Pattern(leftRoot, leftRelative) =>
        right match {
          case Pattern(rightRoot, rightRelative) =>
            leftRoot.compareTo(rightRoot) match {
              case 0 => compare(leftRelative, rightRelative)
              case i => i
            }
          case Root(rightRoot) =>
            leftRoot.compareTo(rightRoot) match {
              case 0 => -1
              case i => i
            }
          case FullFileGlob(base, _, _) =>
            leftRoot.compareTo(base) match {
              case 0 => -1
              case i => i
            }
          case _: RelativeGlob => -1
        }
      case l: RelativeGlob =>
        right match {
          case r: RelativeGlob => comp(l.matchers, r.matchers)
          case _: Pattern      => 1
          case _: Root         => 1
          case _: FullFileGlob => 1
        }
      case Root(leftRoot) =>
        right match {
          case Root(rightRoot) => leftRoot.compareTo(rightRoot)
          case FullFileGlob(leftBase, _, _) =>
            leftRoot.compareTo(leftBase) match {
              case 0 => -1
              case i => i
            }
          case _ => -compare(right, left)
        }
      case FullFileGlob(leftBase, _, _) =>
        right match {
          case FullFileGlob(rightBase, _, _) => leftBase.compareTo(rightBase)
          case _                             => -compare(right, left)
        }
    }
  }
  private object Root {
    implicit val ordering: Ordering[Root] = Ordering.by(_.root)
  }
  private[file] final case object Empty extends RelativeGlob {
    override private[file] def matchers: List[RelativeGlob.Matcher] = Nil
    override def matches(path: Path): Boolean = false
  }
  private[file] final case class Root(root: Path) extends Glob {
    require(root.isAbsolute, s"Tried to construct absolute glob from relative path $root")
    override def matches(path: Path): Boolean = root == path
    override def toString: String = root.toString
    override def equals(o: Any): Boolean = o match {
      case that: Root => this.root == that.root
      case _          => false
    }
    override def hashCode: Int = root.hashCode
  }

  private[sbt] final case class FullFileGlob(base: Path, recursive: Boolean, filter: FileFilter)
      extends Glob {
    override def matches(path: Path): Boolean = {
      path.startsWith(base) && {
        if (recursive) filter.accept(path.toFile)
        else path.getParent == base && filter.accept(path.toFile)
      }
    }
  }

  private[nio] object Pattern {
    implicit val ordering: Ordering[Pattern] = Ordering.by(p => (p.root, p.relative))
  }
  private[nio] final case class Pattern(root: Path, relative: RelativeGlob) extends Glob {
    override def matches(path: Path): Boolean =
      path.startsWith(root) && ((path != root) && relative.matches(root.relativize(path)))
    override def equals(o: Any): Boolean = o match {
      case that: Pattern =>
        this.root == that.root && this.relative == that.relative
      case _ => false
    }
    override def hashCode: Int = (root.hashCode * 31) ^ relative.hashCode
    override def toString: String = s"$root${File.separatorChar}$relative"
  }

  private[sbt] sealed trait RelativeGlobViewOption
  private[sbt] object RelativeGlobViewOption {
    private[sbt] val propName = "sbt.io.implicit.relative.glob.conversion"
    implicit val default: RelativeGlobViewOption = System.getProperty(propName) match {
      case "allow"       => Ignore
      case "error"       => Error
      case "warn" | null => Warn
      case p =>
        val message = s"Unrecognized option $p passed in for $propName. " +
          "Valid values are: {'allow', 'warn', 'error'}. Setting default to: 'warn'."
        System.err.println(message)
        Warn
    }
    private[sbt] case object Warn extends RelativeGlobViewOption
    private[sbt] case object Error extends RelativeGlobViewOption
    private[sbt] case object Ignore extends RelativeGlobViewOption
  }
  private[this] def errorMessage(relative: Glob, warn: Boolean): String = {
    val prefix = if (warn) "Warning" else "Error"
    val action =
      if (warn) "To disable this warning, "
      else "To allow implicit conversions using the current jvm working directory, "
    s"$prefix: Tried to extract the base path for relative glob $relative. $action" +
      s"re-run the program with java option, -D${RelativeGlobViewOption.propName}=allow"
  }

  /**
   * Provides extension methods for [[Glob]].
   * @param glob the [[Glob]] to augment.
   */
  implicit class GlobOps(val glob: Glob) extends AnyVal {
    private[sbt] def descendentMatches(path: Path): Boolean = glob match {
      case Pattern(p, relative) if path.startsWith(p) => relative.matches(p.getFileName)
      case g                                          => g.matches(path)
    }

    private[sbt] def toFileFilter: FileFilter = pathname => glob.matches(pathname.toPath)

    private[sbt] def toAbsolutePath(path: Path)(implicit option: RelativeGlobViewOption): Path = {
      import RelativeGlobViewOption._
      if (!path.isAbsolute) {
        option match {
          case Error  => throw new IllegalArgumentException(errorMessage(glob, warn = false))
          case Ignore => path.toAbsolutePath
          case Warn =>
            System.err.println(errorMessage(glob, warn = true))
            path.toAbsolutePath
        }
      } else path
    }

    private[sbt] def fileTreeViewListParameters(implicit
        option: RelativeGlobViewOption
    ): (Path, Int, Glob) = {
      val b = base
      val r = range._2
      val g = glob match {
        case r: RelativeGlob =>
          r.tail match {
            case Nil => Root(b)
            case t   => Pattern(b, RelativeGlob(t))
          }
        case _ => glob
      }
      (b, r, g)
    }

    private[sbt] def base(implicit option: RelativeGlobViewOption): Path =
      toAbsolutePath(glob match {
        case Pattern(root, r)         => r.prefix.map(root.resolve).getOrElse(root)
        case Root(root)               => root
        case r: RelativeGlob          => r.prefix.getOrElse(Paths.get(""))
        case FullFileGlob(base, _, _) => base
      })

    private[sbt] def range: (Int, Int) = glob match {
      case Pattern(_, relative: RelativeGlob) => RelativeGlob.range(relative)
      case Root(_)                            => (0, 0)
      case relative: RelativeGlob             => RelativeGlob.range(relative)
      case FullFileGlob(_, recursive, _)      => if (recursive) (1, Int.MaxValue) else (1, 1)
    }

    /**
     * Appends a relative glob path to an existing [[Glob]]. For example:
     * {{{
     *   val glob = Glob("foo/bar")
     *   val scalaSources = glob / "**/*.scala"
     *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
     *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Bar.scala")) // true
     *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Baz.java")) // false
     * }}}
     *
     * @param relativeGlob the string that will be parsed into a [[RelativeGlob]] and appended to
     *                     the base glob
     * @return a Glob with the new [[RelativeGlob]] appended to the query.
     */
    def /(relativeGlob: String): Glob = /(RelativeGlob.parse(relativeGlob, isRegex = false))

    /**
     * Appends a relative glob specified by a regex pattern to an existing [[Glob]]. For example:
     * {{{
     *   val glob = Glob("foo/bar")
     *   val scalaSources = glob / "^[^.].*\\.scala".r
     *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
     *   scalaSources.matches(Paths.get("foo/bar/.hidden.scala")) // false
     * }}}
     *
     * Note that it is not possible to specify a recursive glob this way because `**` is not valid
     * in regex. To make a recursive glob, combine with [[RecursiveGlob]]:
     * {{{
     *   val scalaSources = Glob("foo/bar") / ** / "^[^.].*\\.scala".r
     *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Foo.scala")) // true
     *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/.hidden.scala")) // false
     * }}}
     *
     * On Windows, `\` will be treated as a regex escape character but `/` will be treated
     * as a path separator.
     *
     * @param regex the pattern to apply
     * @return a Glob with the new [[RelativeGlob]] appended to the query.
     */
    def /(regex: Regex): Glob = /(RelativeGlob.parse(regex.regex, isRegex = true))

    /**
     * Appends a relative glob path to an existing [[Glob]]. For example:
     * {{{
     *   val glob = Glob("foo/bar")
     *   val scalaSources = glob / "**/*.scala"
     *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
     *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Bar.scala")) // true
     *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Baz.java")) // false
     * }}}
     *
     * @param relativeGlob the [[RelativeGlob]] to be appended to the query
     * @return a Glob with the new [[RelativeGlob]] appended to the query.
     */
    def /(relativeGlob: RelativeGlob): Glob = glob match {
      case Pattern(root, relative) => Pattern(root, relative / relativeGlob)
      case Root(path) =>
        val newRoot = relativeGlob.prefix.map(path.resolve).getOrElse(path)
        relativeGlob.tail match {
          case Nil => Root(newRoot)
          case t   => Pattern(newRoot, RelativeGlob(t))
        }
      case f: FullFileGlob =>
        throw new IllegalArgumentException(s"Can't call / on legacy glob $f")
      case r: RelativeGlob => r / relativeGlob
    }
  }

  /**
   * Provides extension methods to [[Glob]] that allows it to create combined
   * [[sbt.nio.file.PathFilter]] instances with `&&` and `||`.
   * @param glob the Glob to extend
   */
  implicit class PathFilterOps(val glob: Glob) extends AnyVal with PathFilterExtensions {
    def &&(other: PathFilter): PathFilter = (glob: PathFilter) && other
    def ||(other: PathFilter): PathFilter = (glob: PathFilter) || other
    def unary_! : PathFilter = !(glob: PathFilter)
  }

  /**
   * Converts a string to a [[Glob]].
   * @param glob the string to convert
   * @return the converted [[Glob]].
   */
  implicit def stringToGlob(glob: String): Glob = Glob(glob)

  /**
   * Provides extension methods to `java.nio.file.Path`.
   * @param path the `Path` instance to extend.
   */
  final class PathOps(val path: Path) extends AnyVal {

    /**
     * Converts the path to a single path glob
     * @return the single path [[Glob]].
     */
    def toGlob: Glob =
      if (path.isAbsolute) Root(path)
      else {
        path.iterator.asScala.toList match {
          case Nil                                        => Empty
          case h :: Nil if h.getFileName.toString.isEmpty => Empty
          case parts => RelativeGlob(parts.map(p => PathComponent(p.getFileName.toString)))
        }
      }

    /**
     * Appends a path component to a path.
     * @param component the component to append
     * @return the appended path.
     */
    def /(component: String): Path = path.resolve(component)
  }

  /**
   * Provides extension methods to `java.nio.file.Path`
   * @param file the `File` instance to extend.
   */
  final class FileOps(val file: File) extends AnyVal {

    /**
     * Converts the file to a single path glob.
     * @return the single path [[Glob]].
     */
    def toGlob: Glob = new PathOps(file.toPath).toGlob
  }
  private[this] val windowsEscapable = "(){}"
  private[this] val allMeta = "*{([?"
  private[file] val hasMeta: String => Boolean = _.exists(allMeta.contains(_))
  private[file] val isWin = Properties.isWin
  private[this] val splitter: (String, Boolean) => List[String] = {
    if (Glob.isWin) { (glob, isRegex) =>
      {
        val stringBuilder = new StringBuilder(glob.length)
        val components = new util.ArrayList[String]
        val array = glob.toCharArray
        val separator = File.separatorChar
        @tailrec def fillComponents(index: Int): Unit = index match {
          case i if i < array.length =>
            array(i) match {
              case `separator` if !isRegex =>
                val nextIndex = i + 1
                if (nextIndex < array.length) {
                  array(nextIndex) match {
                    case c if windowsEscapable.contains(c) =>
                      stringBuilder.append(separator).append(c)
                    case c =>
                      components.add(stringBuilder.toString)
                      stringBuilder.clear()
                      stringBuilder.append(c)
                  }
                  fillComponents(nextIndex + 1)
                } else {
                  components.add(stringBuilder.toString)
                  ()
                }
              case '/' =>
                components.add(stringBuilder.toString)
                stringBuilder.clear()
                fillComponents(i + 1)
              case c =>
                stringBuilder.append(c)
                fillComponents(i + 1)
            }
          case _ =>
            components.add(stringBuilder.toString)
            ()
        }
        fillComponents(index = 0)
        components.asScala.toList
      }
    } else { (glob, _) =>
      glob.split(File.separatorChar).toList
    }
  }
}

/**
 * A specialization of [[Glob]] that applies only to relative paths. [[RelativeGlob]] instances
 * can always be combined with the `/` operator whereas general `Glob` instances cannot because they
 * may correspond to an absolute file path:
 * {{{
 *   val combined = Glob("/foo/bar") / RelativeGlob("
 * }}}
 */
sealed trait RelativeGlob extends Glob {
  private[file] def matchers: List[RelativeGlob.Matcher]
  private[file] def prefix: Option[Path] = matchers.takeWhile(_.isInstanceOf[PathComponent]) match {
    case Nil => None
    case (h: PathComponent) :: (t: List[PathComponent] @unchecked) =>
      Some(Paths.get(h.glob, t.map(_.glob): _*))
    case _ => None
  }
  private[file] def tail: List[RelativeGlob.Matcher] =
    matchers.dropWhile(_.isInstanceOf[PathComponent])

  /**
   * Appends additional components to this [[RelativeGlob]]:
   * {{{
   *   val glob = Glob("foo/bar")
   *   val scalaSources = glob / "**/*.scala"
   *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Bar.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Baz.java")) // false
   * }}}
   *
   * @param relativeGlob the additional query string to parse and append
   * @return a Glob with the new [[RelativeGlob]] appended to the query.
   */
  def /(relativeGlob: String): RelativeGlob = /(RelativeGlob.parse(relativeGlob, isRegex = false))

  /**
   * Appends an additional [[RelativeGlob]] to this [[RelativeGlob]]:
   * {{{
   *   val glob = Glob("foo/bar")
   *   val scalaSources = glob / ** / "*.scala"
   *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Bar.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Baz.java")) // false
   * }}}
   *
   * @param relativeGlob the additional query string to parse and append
   * @return a Glob with the new [[RelativeGlob]] appended to the query.
   */
  def /(relativeGlob: RelativeGlob): RelativeGlob =
    RelativeGlob(this.matchers ::: relativeGlob.matchers)

  /**
   * Appends a relative glob specified by a regex pattern to an existing [[Glob]]. For example:
   * {{{
   *   val glob = Glob("foo/bar")
   *   val scalaSources = glob / "^[^.].*\\.scala".r
   *   scalaSources.matches(Paths.get("foo/bar/Foo.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/.hidden.scala")) // false
   * }}}
   *
   * Note that it is not possible to specify a recursive glob this way because `**` is not valid
   * in regex. To make a recursive glob, combine with [[RecursiveGlob]]:
   * {{{
   *   val scalaSources = Glob("foo/bar") / ** / "^[^.].*\\.scala".r
   *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/Foo.scala")) // true
   *   scalaSources.matches(Paths.get("foo/bar/baz/fizz/buzz/.hidden.scala")) // false
   * }}}
   *
   * On Windows, `\` will be treated as a regex escape character but `/` will be treated
   * as a path separator.
   *
   * @param regex the pattern to apply
   * @return a Glob with the new [[RelativeGlob]] appended to the query.
   */
  def /(regex: Regex): RelativeGlob = /(RelativeGlob.parse(regex.regex, isRegex = true))
}

/**
 * A [[RelativeGlob]] that matches any path. Can be combined with other [[Glob]] instances to build
 * recursive queries:
 * {{{
 *   val scalaSources = Glob("/foo/bar") / RecursiveGlob / "*.scala"
 *   scalaSources.matches(Paths.get("/foo/bar/Foo.scala")) // true
 *   scalaSources.matches(Paths.get("/foo/bar/baz/fizz/buzz/Bar.scala")) // true
 * }}}
 */
case object RecursiveGlob extends SingleComponentMatcher with RelativeGlob {
  override private[file] def glob: String = "**"
  def matches(path: Path): Boolean = true
}

/**
 * A [[RelativeGlob]] that matches any single path name.
 * {{{
 *   AnyPath.matches(Paths.get("foo")) // true
 *   AnyPath.matches(Paths.get("foo/bar")) // false
 *   Glob("foo/bar", AnyPath).matches(Paths.get("foo/bar")) // false
 *   Glob("foo/bar", AnyPath).matches(Paths.get("foo/bar/baz")) // true
 *   Glob("foo/bar", AnyPath).matches(Paths.get("foo/bar/baz/buzz")) // false
 * }}}
 *
 * It can be used to specify the minimum depth of a query:
 * {{{
 *   val childScalaSources = Glob("/foo/bar") / AnyPath / RecursiveGlob / "*.scala"
 *   childScalaSources.matches(Paths.get("/foo/bar/Foo.scala")) // false
 *   childScalaSources.matches(Paths.get("/foo/bar/baz/Bar.scala")) // true
 *   childScalaSources.matches(Paths.get("/foo/bar/baz/fizz/buzz/Bar.scala")) // true
 * }}}
 */
case object AnyPath extends SingleComponentMatcher with RelativeGlob {
  override private[file] def glob: String = "*"
  override def matches(path: Path): Boolean = path.getNameCount == 1
}
object RelativeGlob {

  /**
   * Aliases [[RecursiveGlob]].
   */
  val ** = RecursiveGlob

  /**
   * Aliases [[AnyPath]].
   */
  val * = AnyPath
  private[file] def parse(glob: String, isRegex: Boolean): RelativeGlob =
    Glob(glob, isRegex) match {
      case r: RelativeGlob => r
      case _ =>
        val msg = s"Couldn't create relativeGlob glob from absolute glob: $glob"
        throw new IllegalArgumentException(msg)
    }
  private[sbt] def apply(matchers: String*): RelativeGlob =
    new RelativeGlobImpl(matchers.view.filterNot(_ == ".").map(Matcher.apply).toList)
  private[sbt] def apply(matchers: List[Matcher]): RelativeGlob = new RelativeGlobImpl(matchers)
  implicit val ordering: Ordering[RelativeGlob] = Ordering.by(_.matchers)
  private[file] def range(relative: RelativeGlob): (Int, Int) = {
    val res = relative.matchers.foldLeft((0, 0)) {
      case ((0, 0), RecursiveGlob)                => (1, Int.MaxValue)
      case (r @ (_, Int.MaxValue), RecursiveGlob) => r
      case ((min, Int.MaxValue), _)               => (min + 1, Int.MaxValue)
      case ((min, max), _)                        => (min + 1, max + 1)
    }
    res
  }

  private final class RelativeGlobImpl(val matchers: List[Matcher]) extends RelativeGlob {
    override def matches(path: Path): Boolean = {
      val count = path.getNameCount
      @tailrec def impl(currentIndex: Int, matchers: List[Matcher]): Boolean = matchers match {
        case RecursiveGlob :: Nil => count > 0
        case RecursiveGlob :: matchersTail =>
          currentIndex match {
            case i if i < count => recursiveMatches(matchersTail, i)
            case _              => false
          }
        case m :: Nil if currentIndex == count - 1 => m.matches(path.getFileName)
        case m :: matchersTail =>
          currentIndex match {
            case i if i < count && m.matches(path.getName(i)) => impl(i + 1, matchersTail)
            case _                                            => false
          }
        case Nil => currentIndex == count
      }
      def recursiveMatches(remaining: List[Matcher], currentIndex: Int): Boolean = {
        remaining match {
          case Nil                => true
          case nameMatcher :: Nil => nameMatcher.matches(path.getFileName)
          case _ =>
            @tailrec def recursiveImpl(index: Int): Boolean = index match {
              case i if i < count => impl(i, remaining) || recursiveImpl(i + 1)
              case _              => false
            }
            recursiveImpl(currentIndex)
        }
      }
      impl(currentIndex = 0, matchers)
    }
    override def hashCode: Int = matchers.hashCode
    override def toString: String = matchers.mkString(File.separator)
    override def equals(o: Any): Boolean = o match {
      case that: RelativeGlob => this.matchers == that.matchers
      case _                  => false
    }
  }

  private[sbt] sealed trait Matcher extends RelativeGlob
  private[sbt] object Matcher {
    private[sbt] implicit object ordering extends Ordering[Matcher] {
      override def compare(x: Matcher, y: Matcher): Int = x match {
        case RecursiveGlob =>
          y match {
            case RecursiveGlob => 0
            case _             => -1
          }
        case AnyPath =>
          y match {
            case AnyPath       => 0
            case RecursiveGlob => 1
            case _             => -1
          }
        case spm: SingleComponentMatcher =>
          y match {
            case that: SingleComponentMatcher => spm.glob.compareTo(that.glob)
            case _                            => -1
          }
        case _: FunctionNameFilter =>
          y match {
            case _: FunctionNameFilter                        => 0
            case _: NotMatcher | _: AndMatcher | _: OrMatcher => -1
            case _                                            => 1
          }
        case _: NotMatcher =>
          y match {
            case _: NotMatcher | _: AndMatcher | _: OrMatcher => 0
            case _                                            => 1
          }
        case _: AndMatcher =>
          y match {
            case _: NotMatcher | _: AndMatcher | _: OrMatcher => 0
            case _                                            => 1
          }
        case _: OrMatcher =>
          y match {
            case _: NotMatcher | _: AndMatcher | _: OrMatcher => 0
            case _                                            => 1
          }
      }
    }
    private[sbt] implicit val listOrdering: Ordering[List[Matcher]] = new Ordering[List[Matcher]] {
      override def compare(left: List[Matcher], right: List[Matcher]): Int = {
        val leftIt = left.iterator
        val rightIt = right.iterator
        while (leftIt.hasNext && rightIt.hasNext) {
          val res = ordering.compare(leftIt.next(), rightIt.next())
          if (res != 0) return res
        }
        Ordering.Boolean.compare(leftIt.hasNext, rightIt.hasNext)
      }
    }
    private[sbt] def and(left: Matcher, right: Matcher): Matcher = {
      if (left == NoPath || right == NoPath) NoPath
      else if (left == AnyPath) right
      else if (right == AnyPath) left
      else AndMatcher(left, right)
    }
    private[sbt] def or(left: Matcher, right: Matcher): Matcher = OrMatcher(left, right)
    private[sbt] def not(matcher: Matcher): Matcher = matcher match {
      case NoPath  => AnyPath
      case AnyPath => NoPath
      case m       => NotMatcher(m)
    }
    private[sbt] def apply(glob: String): Matcher = glob match {
      case "**"                                             => RecursiveGlob
      case "*"                                              => AnyPath
      case g if !g.startsWith("regex:") && !Glob.hasMeta(g) => PathComponent(g)
      case g                                                => new GlobMatcher(g)
    }
    private[sbt] def apply(f: String => Boolean): Matcher = FunctionNameFilter(f)
  }
  private[sbt] case object NoPath extends SingleComponentMatcher with RelativeGlob {
    override def glob: String = ""
    override def matches(path: Path): Boolean = false
  }
  private[file] sealed trait SingleComponentMatcher extends Matcher {
    private[file] def glob: String
    override private[file] final val matchers: List[Matcher] = this :: Nil
    override def toString: String = glob
  }
  private[file] object PathComponent {
    def apply(component: String): PathComponent = new PathComponent(component)
    def unapply(glob: Glob): Option[String] = glob match {
      case p: PathComponent => Some(p.glob)
      case _                => None
    }
  }
  private[file] final case class NotMatcher(matcher: Matcher) extends Matcher {
    override private[sbt] def matchers: List[Matcher] = this :: Nil

    /**
     * Indicates whether a path matches the pattern specified by this [[Glob]].
     *
     * @param path the path to match
     * @return true it the path matches.
     */
    override def matches(path: Path): Boolean = !matcher.matches(path)
    override def toString: String = s"!$matcher"
  }
  private[file] final case class OrMatcher(left: Matcher, right: Matcher) extends Matcher {
    override private[sbt] def matchers: List[Matcher] = this :: Nil

    /**
     * Indicates whether a path matches the pattern specified by this [[Glob]].
     *
     * @param path the path to match
     * @return true it the path matches.
     */
    override def matches(path: Path): Boolean = left.matches(path) || right.matches(path)
    override def toString: String = s"($left || $right)"
  }
  private[file] final case class AndMatcher(left: Matcher, right: Matcher) extends Matcher {
    override private[sbt] def matchers: List[Matcher] = this :: Nil

    /**
     * Indicates whether a path matches the pattern specified by this [[Glob]].
     *
     * @param path the path to match
     * @return true it the path matches.
     */
    override def matches(path: Path): Boolean = left.matches(path) && right.matches(path)
    override def toString: String = s"($left && $right)"
  }
  private[file] final case class FunctionNameFilter(f: String => Boolean) extends Matcher {
    override private[sbt] def matchers: List[Matcher] = this :: Nil
    override def matches(path: Path): Boolean = f(path.getFileName.toString)
  }
  private[file] final class PathComponent private (override val glob: String)
      extends SingleComponentMatcher {
    override def matches(path: Path): Boolean =
      path.getNameCount == 1 && path.getFileName.toString == glob
    override def equals(o: Any): Boolean = o match {
      case that: PathComponent => this.glob == that.glob
      case _                   => false
    }
    override def hashCode: Int = glob.hashCode
  }
  private final class GlobMatcher(override val glob: String) extends SingleComponentMatcher {
    private[this] val (prefixString, pattern) = glob.indexOf(":") match {
      case -1 => ("glob", glob)
      case i  => (glob.substring(0, i), glob.substring(i + 1))
    }
    private[this] val matcher = FileSystems.getDefault.getPathMatcher(s"$prefixString:$pattern")
    override def matches(path: Path): Boolean = matcher.matches(path)
    override def equals(o: Any): Boolean = o match {
      case that: GlobMatcher => this.glob == that.glob
      case _                 => false
    }
    override def hashCode(): Int = glob.hashCode
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy