Merge package:glob into the tools monorepo
diff --git a/pkgs/glob/.github/dependabot.yml b/pkgs/glob/.github/dependabot.yml
new file mode 100644
index 0000000..bf6b38a
--- /dev/null
+++ b/pkgs/glob/.github/dependabot.yml
@@ -0,0 +1,14 @@
+# Dependabot configuration file.
+version: 2
+
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ labels:
+ - autosubmit
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/pkgs/glob/.github/workflows/publish.yml b/pkgs/glob/.github/workflows/publish.yml
new file mode 100644
index 0000000..1af8f47
--- /dev/null
+++ b/pkgs/glob/.github/workflows/publish.yml
@@ -0,0 +1,17 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+ pull_request:
+ branches: [ master ]
+ types: [opened, synchronize, reopened, labeled, unlabeled]
+ push:
+ tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ]
+
+jobs:
+ publish:
+ if: ${{ github.repository_owner == 'dart-lang' }}
+ uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
+ with:
+ sdk: dev
diff --git a/pkgs/glob/.github/workflows/test-package.yml b/pkgs/glob/.github/workflows/test-package.yml
new file mode 100644
index 0000000..15a480a
--- /dev/null
+++ b/pkgs/glob/.github/workflows/test-package.yml
@@ -0,0 +1,72 @@
+name: Dart CI
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: "0 0 * * 0"
+
+permissions: read-all
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ channel: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.3, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ channel: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Node tests
+ run: dart test --platform node
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests - wasm
+ run: dart test --platform chrome --compiler dart2wasm
+ if: always() && steps.install.outcome == 'success' && matrix.sdk == 'dev'
diff --git a/pkgs/glob/.gitignore b/pkgs/glob/.gitignore
new file mode 100644
index 0000000..ab3cb76
--- /dev/null
+++ b/pkgs/glob/.gitignore
@@ -0,0 +1,16 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/glob/CHANGELOG.md b/pkgs/glob/CHANGELOG.md
new file mode 100644
index 0000000..7263fc9
--- /dev/null
+++ b/pkgs/glob/CHANGELOG.md
@@ -0,0 +1,114 @@
+## 2.1.3-wip
+
+- Require Dart 3.3.
+
+## 2.1.2
+
+- Allow `file` version `7.x`.
+- Require Dart 2.19.
+
+## 2.1.1
+
+- Updated the dependency on `package:file` to require at least `6.1.3`.
+
+## 2.1.0
+
+- Return empty results instead of throwing when trying to list a path that does
+ not exist.
+
+## 2.0.2
+
+- Drop package:pedantic dependency, use package:lints instead.
+- Update SDK lower bound to `2.15.0`
+
+## 2.0.1
+
+- Update example in README for new import.
+
+## 2.0.0
+
+- Stable null safety release.
+
+### Breaking Change
+
+The `list*` apis on `Glob` have been renamed to `listFileSystem*` and they now
+require a `FileSystem` object from `package:file`.
+
+There is a new convenience import, `package:glob/list_local_fs.dart` which
+provides the old methods as extensions, and automatically passes a
+`LocalFileSystem`.
+
+## 1.2.1
+
+- Add an empty list_local_fs.dart to ease upgrade from 1x to 2x
+
+## 1.2.0
+
+- Support running on Node.js.
+
+## 1.1.7
+
+- Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.1.6
+
+- Improve support for Dart 2 runtime semantics.
+
+## 1.1.5
+
+- Declare support for `async` 2.0.0.
+
+- Require Dart 1.23.0.
+
+## 1.1.4
+
+- Throw an exception when listing globs whose initial paths don't exist in
+ case-insensitive mode. This matches the case-sensitive behavior.
+
+## 1.1.3
+
+- Support `string_scanner` 1.0.0.
+
+## 1.1.2
+
+- Fix all strong mode errors and warnings.
+
+## 1.1.1
+
+- Fix a bug where listing an absolute glob with `caseInsensitive: false` failed.
+
+## 1.1.0
+
+- Add a `caseSensitive` named parameter to `new Glob()` that controls whether
+ the glob is case-sensitive. This defaults to `false` on Windows and `true`
+ elsewhere.
+
+ Matching case-insensitively on Windows is a behavioral change, but since it
+ more closely matches the semantics of Windows paths it's considered a bug fix
+ rather than a breaking change.
+
+## 1.0.5
+
+- Narrow the dependency on `path`. Previously, this allowed versions that didn't
+ support all the functionality this package needs.
+
+- Upgrade to the new test runner.
+
+## 1.0.4
+
+- Added overlooked `collection` dependency.
+
+## 1.0.3
+
+- Fix a bug where `Glob.list()` and `Glob.listSync()` would incorrectly throw
+ exceptions when a directory didn't exist on the filesystem.
+
+## 1.0.2
+
+- Fixed `Glob.list()` on Windows.
+
+## 1.0.1
+
+- Fix several analyzer warnings.
+
+- Fix the tests on Windows.
diff --git a/pkgs/glob/LICENSE b/pkgs/glob/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/glob/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/glob/README.md b/pkgs/glob/README.md
new file mode 100644
index 0000000..ef6a745
--- /dev/null
+++ b/pkgs/glob/README.md
@@ -0,0 +1,132 @@
+[](https://github.com/dart-lang/glob/actions/workflows/test-package.yml)
+[](https://pub.dev/packages/glob)
+[](https://pub.dev/packages/glob/publisher)
+
+`glob` is a file and directory globbing library that supports both checking
+whether a path matches a glob and listing all entities that match a glob.
+
+A "glob" is a pattern designed specifically to match files and directories. Most
+shells support globs natively.
+
+## Usage
+
+To construct a glob, just use `Glob()`. As with `RegExp`s, it's a good idea to
+keep around a glob if you'll be using it more than once so that it doesn't have
+to be compiled over and over. You can check whether a path matches the glob
+using `Glob.matches()`:
+
+```dart
+import 'package:glob/glob.dart';
+
+final dartFile = Glob("**.dart");
+
+// Print all command-line arguments that are Dart files.
+void main(List<String> arguments) {
+ for (var argument in arguments) {
+ if (dartFile.matches(argument)) print(argument);
+ }
+}
+```
+
+You can also list all files that match a glob using `Glob.list()` or
+`Glob.listSync()`:
+
+```dart
+import 'package:glob/glob.dart';
+import 'package:glob/list_local_fs.dart';
+
+final dartFile = Glob("**.dart");
+
+// Recursively list all Dart files in the current directory.
+void main(List<String> arguments) {
+ for (var entity in dartFile.listSync()) {
+ print(entity.path);
+ }
+}
+```
+
+## Syntax
+
+The glob syntax hews closely to the widely-known Bash glob syntax, with a few
+exceptions that are outlined below.
+
+In order to be as cross-platform and as close to the Bash syntax as possible,
+all globs use POSIX path syntax, including using `/` as a directory separator
+regardless of which platform they're on. This is true even for Windows roots;
+for example, a glob matching all files in the C drive would be `C:/*`.
+
+Globs are case-sensitive by default on Posix systems and browsers, and
+case-insensitive by default on Windows.
+
+### Match any characters in a filename: `*`
+
+The `*` character matches zero or more of any character other than `/`. This
+means that it can be used to match all files in a given directory that match a
+pattern without also matching files in a subdirectory. For example, `lib/*.dart`
+will match `lib/glob.dart` but not `lib/src/utils.dart`.
+
+### Match any characters across directories: `**`
+
+`**` is like `*`, but matches `/` as well. It's useful for matching files or
+listing directories recursively. For example, `lib/**.dart` will match both
+`lib/glob.dart` and `lib/src/utils.dart`.
+
+If `**` appears at the beginning of a glob, it won't match absolute paths or
+paths beginning with `../`. For example, `**.dart` won't match `/foo.dart`,
+although `/**.dart` will. This is to ensure that listing a bunch of paths and
+checking whether they match a glob produces the same results as listing that
+glob. In the previous example, `/foo.dart` wouldn't be listed for `**.dart`, so
+it shouldn't be matched by it either.
+
+This is an extension to Bash glob syntax that's widely supported by other glob
+implementations.
+
+### Match any single character: `?`
+
+The `?` character matches a single character other than `/`. Unlike `*`, it
+won't match any more or fewer than one character. For example, `test?.dart` will
+match `test1.dart` but not `test10.dart` or `test.dart`.
+
+### Match a range of characters: `[...]`
+
+The `[...]` construction matches one of several characters. It can contain
+individual characters, such as `[abc]`, in which case it will match any of those
+characters; it can contain ranges, such as `[a-zA-Z]`, in which case it will
+match any characters that fall within the range; or it can contain a mix of
+both. It will only ever match a single character. For example,
+`test[a-zA-Z_].dart` will match `testx.dart`, `testA.dart`, and `test_.dart`,
+but not `test-.dart`.
+
+If it starts with `^` or `!`, the construction will instead match all characters
+_not_ mentioned. For example, `test[^a-z].dart` will match `test1.dart` but not
+`testa.dart`.
+
+This construction never matches `/`.
+
+### Match one of several possibilities: `{...,...}`
+
+The `{...,...}` construction matches one of several options, each of which is a
+glob itself. For example, `lib/{*.dart,src/*}` matches `lib/glob.dart` and
+`lib/src/data.txt`. It can contain any number of options greater than one, and
+can even contain nested options.
+
+This is an extension to Bash glob syntax, although it is supported by other
+layers of Bash and is often used in conjunction with globs.
+
+### Escaping a character: `\`
+
+The `\` character can be used in any context to escape a character that would
+otherwise be semantically meaningful. For example, `\*.dart` matches `*.dart`
+but not `test.dart`.
+
+### Syntax errors
+
+Because they're used as part of the shell, almost all strings are valid Bash
+globs. This implementation is more picky, and performs some validation to ensure
+that globs are meaningful. For instance, unclosed `{` and `[` are disallowed.
+
+### Reserved syntax: `(...)`
+
+Parentheses are reserved in case this package adds support for Bash extended
+globbing in the future. For the time being, using them will throw an error
+unless they're escaped.
diff --git a/pkgs/glob/analysis_options.yaml b/pkgs/glob/analysis_options.yaml
new file mode 100644
index 0000000..813cc96
--- /dev/null
+++ b/pkgs/glob/analysis_options.yaml
@@ -0,0 +1,10 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+
+linter:
+ rules:
+ - avoid_unused_constructor_parameters
+ - cancel_subscriptions
diff --git a/pkgs/glob/lib/glob.dart b/pkgs/glob/lib/glob.dart
new file mode 100644
index 0000000..f5e4bb4
--- /dev/null
+++ b/pkgs/glob/lib/glob.dart
@@ -0,0 +1,203 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:path/path.dart' as p;
+
+import 'src/ast.dart';
+import 'src/list_tree.dart';
+import 'src/parser.dart';
+import 'src/utils.dart';
+
+/// Regular expression used to quote globs.
+final _quoteRegExp = RegExp(r'[*{[?\\}\],\-()]');
+
+/// A glob for matching and listing files and directories.
+///
+/// A glob matches an entire string as a path. Although the glob pattern uses
+/// POSIX syntax, it can match against POSIX, Windows, or URL paths. The format
+/// it expects paths to use is based on the `context` parameter to [Glob.new];
+/// it defaults to the current system's syntax.
+///
+/// Paths are normalized before being matched against a glob, so for example the
+/// glob `foo/bar` matches the path `foo/./bar`. A relative glob can match an
+/// absolute path and vice versa; globs and paths are both interpreted as
+/// relative to `context.current`, which defaults to the current working
+/// directory.
+///
+/// When used as a [Pattern], a glob will return either one or zero matches for
+/// a string depending on whether the entire string matches the glob. These
+/// matches don't currently have capture groups, although this may change in the
+/// future.
+class Glob implements Pattern {
+ /// The pattern used to create this glob.
+ final String pattern;
+
+ /// The context in which paths matched against this glob are interpreted.
+ final p.Context context;
+
+ /// If true, a path matches if it matches the glob itself or is recursively
+ /// contained within a directory that matches.
+ final bool recursive;
+
+ /// Whether the glob matches paths case-sensitively.
+ bool get caseSensitive => _ast.caseSensitive;
+
+ /// The parsed AST of the glob.
+ final AstNode _ast;
+
+ /// The underlying object used to implement [listFileSystem] and
+ /// [listFileSystemSync].
+ ///
+ /// This should not be read directly outside of [_listTreeForFileSystem].
+ ListTree? _listTree;
+
+ /// Keeps track of the previous file system used. If this changes then the
+ /// [_listTree] must be invalidated.
+ ///
+ /// This is handled inside of [_listTreeForFileSystem].
+ FileSystem? _previousFileSystem;
+
+ /// Whether [context]'s current directory is absolute.
+ bool get _contextIsAbsolute =>
+ _contextIsAbsoluteCache ??= context.isAbsolute(context.current);
+
+ bool? _contextIsAbsoluteCache;
+
+ /// Whether [pattern] could match absolute paths.
+ bool get _patternCanMatchAbsolute =>
+ _patternCanMatchAbsoluteCache ??= _ast.canMatchAbsolute;
+
+ bool? _patternCanMatchAbsoluteCache;
+
+ /// Whether [pattern] could match relative paths.
+ bool get _patternCanMatchRelative =>
+ _patternCanMatchRelativeCache ??= _ast.canMatchRelative;
+
+ bool? _patternCanMatchRelativeCache;
+
+ /// Returns [contents] with characters that are meaningful in globs
+ /// backslash-escaped.
+ static String quote(String contents) =>
+ contents.replaceAllMapped(_quoteRegExp, (match) => '\\${match[0]}');
+
+ /// Creates a new glob with [pattern].
+ ///
+ /// Paths matched against the glob are interpreted according to [context]. It
+ /// defaults to the system context.
+ ///
+ /// If [recursive] is true, this glob matches and lists not only the files and
+ /// directories it explicitly matches, but anything beneath those as well.
+ ///
+ /// If [caseSensitive] is true, this glob matches and lists only files whose
+ /// case matches that of the characters in the glob. Otherwise, it matches
+ /// regardless of case. This defaults to `false` when [context] is Windows and
+ /// `true` otherwise.
+ factory Glob(String pattern,
+ {p.Context? context, bool recursive = false, bool? caseSensitive}) {
+ context ??= p.context;
+ caseSensitive ??= context.style == p.Style.windows ? false : true;
+ if (recursive) pattern += '{,/**}';
+
+ var parser = Parser(pattern, context, caseSensitive: caseSensitive);
+ return Glob._(pattern, context, parser.parse(), recursive);
+ }
+
+ Glob._(this.pattern, this.context, this._ast, this.recursive);
+
+ /// Lists all [FileSystemEntity]s beneath [root] that match the glob in the
+ /// provided [fileSystem].
+ ///
+ /// This works much like [Directory.list], but it only lists directories that
+ /// could contain entities that match the glob. It provides no guarantees
+ /// about the order of the returned entities, although it does guarantee that
+ /// only one entity with a given path will be returned.
+ ///
+ /// [root] defaults to the current working directory.
+ ///
+ /// [followLinks] works the same as for [Directory.list].
+ Stream<FileSystemEntity> listFileSystem(FileSystem fileSystem,
+ {String? root, bool followLinks = true}) {
+ if (context.style != p.style) {
+ throw StateError("Can't list glob \"$this\"; it matches "
+ '${context.style} paths, but this platform uses ${p.style} paths.');
+ }
+
+ return _listTreeForFileSystem(fileSystem)
+ .list(root: root, followLinks: followLinks);
+ }
+
+ /// Synchronously lists all [FileSystemEntity]s beneath [root] that match the
+ /// glob in the provided [fileSystem].
+ ///
+ /// This works much like [Directory.listSync], but it only lists directories
+ /// that could contain entities that match the glob. It provides no guarantees
+ /// about the order of the returned entities, although it does guarantee that
+ /// only one entity with a given path will be returned.
+ ///
+ /// [root] defaults to the current working directory.
+ ///
+ /// [followLinks] works the same as for [Directory.list].
+ List<FileSystemEntity> listFileSystemSync(FileSystem fileSystem,
+ {String? root, bool followLinks = true}) {
+ if (context.style != p.style) {
+ throw StateError("Can't list glob \"$this\"; it matches "
+ '${context.style} paths, but this platform uses ${p.style} paths.');
+ }
+
+ return _listTreeForFileSystem(fileSystem)
+ .listSync(root: root, followLinks: followLinks);
+ }
+
+ /// Returns whether this glob matches [path].
+ bool matches(String path) => matchAsPrefix(path) != null;
+
+ @override
+ Match? matchAsPrefix(String path, [int start = 0]) {
+ // Globs are like anchored RegExps in that they only match entire paths, so
+ // if the match starts anywhere after the first character it can't succeed.
+ if (start != 0) return null;
+
+ if (_patternCanMatchAbsolute &&
+ (_contextIsAbsolute || context.isAbsolute(path))) {
+ var absolutePath = context.normalize(context.absolute(path));
+ if (_ast.matches(toPosixPath(context, absolutePath))) {
+ return GlobMatch(path, this);
+ }
+ }
+
+ if (_patternCanMatchRelative) {
+ var relativePath = context.relative(path);
+ if (_ast.matches(toPosixPath(context, relativePath))) {
+ return GlobMatch(path, this);
+ }
+ }
+
+ return null;
+ }
+
+ @override
+ Iterable<Match> allMatches(String path, [int start = 0]) {
+ var match = matchAsPrefix(path, start);
+ return match == null ? [] : [match];
+ }
+
+ @override
+ String toString() => pattern;
+
+ /// Handles getting a possibly cached [ListTree] for a [fileSystem].
+ ListTree _listTreeForFileSystem(FileSystem fileSystem) {
+ // Don't use cached trees for in memory file systems to avoid memory leaks.
+ if (fileSystem is MemoryFileSystem) return ListTree(_ast, fileSystem);
+
+ // Throw away our cached `_listTree` if the file system is different.
+ if (fileSystem != _previousFileSystem) {
+ _listTree = null;
+ _previousFileSystem = fileSystem;
+ }
+
+ return _listTree ??= ListTree(_ast, fileSystem);
+ }
+}
diff --git a/pkgs/glob/lib/list_local_fs.dart b/pkgs/glob/lib/list_local_fs.dart
new file mode 100644
index 0000000..6ccbde0
--- /dev/null
+++ b/pkgs/glob/lib/list_local_fs.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+
+import 'glob.dart';
+
+/// Platform specific extensions for where `dart:io` exists, which use the
+/// local file system.
+extension ListLocalFileSystem on Glob {
+ /// Convenience method for [Glob.listFileSystem] which uses the local file
+ /// system.
+ Stream<FileSystemEntity> list({String? root, bool followLinks = true}) =>
+ listFileSystem(const LocalFileSystem(),
+ root: root, followLinks: followLinks);
+
+ /// Convenience method for [Glob.listFileSystemSync] which uses the local
+ /// file system.
+ List<FileSystemEntity> listSync({String? root, bool followLinks = true}) =>
+ listFileSystemSync(const LocalFileSystem(),
+ root: root, followLinks: followLinks);
+}
diff --git a/pkgs/glob/lib/src/ast.dart b/pkgs/glob/lib/src/ast.dart
new file mode 100644
index 0000000..179b081
--- /dev/null
+++ b/pkgs/glob/lib/src/ast.dart
@@ -0,0 +1,439 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:collection/collection.dart';
+import 'package:path/path.dart' as p;
+
+import 'utils.dart';
+
+const _separator = 0x2F; // "/"
+
+/// A node in the abstract syntax tree for a glob.
+abstract class AstNode {
+ /// The cached regular expression that this AST was compiled into.
+ RegExp? _regExp;
+
+ /// Whether this node matches case-sensitively or not.
+ final bool caseSensitive;
+
+ /// Whether this glob could match an absolute path.
+ ///
+ /// Either this or [canMatchRelative] or both will be true.
+ bool get canMatchAbsolute => false;
+
+ /// Whether this glob could match a relative path.
+ ///
+ /// Either this or [canMatchRelative] or both will be true.
+ bool get canMatchRelative => true;
+
+ AstNode._(this.caseSensitive);
+
+ /// Returns a new glob with all the options bubbled to the top level.
+ ///
+ /// In particular, this returns a glob AST with two guarantees:
+ ///
+ /// 1. There are no [OptionsNode]s other than the one at the top level.
+ /// 2. It matches the same set of paths as `this`.
+ ///
+ /// For example, given the glob `{foo,bar}/{click/clack}`, this would return
+ /// `{foo/click,foo/clack,bar/click,bar/clack}`.
+ OptionsNode flattenOptions() => OptionsNode([
+ SequenceNode([this], caseSensitive: caseSensitive)
+ ], caseSensitive: caseSensitive);
+
+ /// Returns whether this glob matches [string].
+ bool matches(String string) =>
+ (_regExp ??= RegExp('^${_toRegExp()}\$', caseSensitive: caseSensitive))
+ .hasMatch(string);
+
+ /// Subclasses should override this to return a regular expression component.
+ String _toRegExp();
+}
+
+/// A sequence of adjacent AST nodes.
+class SequenceNode extends AstNode {
+ /// The nodes in the sequence.
+ final List<AstNode> nodes;
+
+ @override
+ bool get canMatchAbsolute => nodes.first.canMatchAbsolute;
+
+ @override
+ bool get canMatchRelative => nodes.first.canMatchRelative;
+
+ SequenceNode(Iterable<AstNode> nodes, {bool caseSensitive = true})
+ : nodes = nodes.toList(),
+ super._(caseSensitive);
+
+ @override
+ OptionsNode flattenOptions() {
+ if (nodes.isEmpty) {
+ return OptionsNode([this], caseSensitive: caseSensitive);
+ }
+
+ var sequences =
+ nodes.first.flattenOptions().options.map((sequence) => sequence.nodes);
+ for (var node in nodes.skip(1)) {
+ // Concatenate all sequences in the next options node ([nextSequences])
+ // onto all previous sequences ([sequences]).
+ var nextSequences = node.flattenOptions().options;
+ sequences = sequences.expand((sequence) {
+ return nextSequences.map((nextSequence) {
+ return sequence.toList()..addAll(nextSequence.nodes);
+ });
+ });
+ }
+
+ return OptionsNode(sequences.map((sequence) {
+ // Combine any adjacent LiteralNodes in [sequence].
+ return SequenceNode(
+ sequence.fold<List<AstNode>>([], (combined, node) {
+ if (combined.isEmpty ||
+ combined.last is! LiteralNode ||
+ node is! LiteralNode) {
+ return combined..add(node);
+ }
+
+ combined[combined.length - 1] = LiteralNode(
+ (combined.last as LiteralNode).text + node.text,
+ caseSensitive: caseSensitive);
+ return combined;
+ }),
+ caseSensitive: caseSensitive);
+ }), caseSensitive: caseSensitive);
+ }
+
+ /// Splits this glob into components along its path separators.
+ ///
+ /// For example, given the glob `foo/*/*.dart`, this would return three globs:
+ /// `foo`, `*`, and `*.dart`.
+ ///
+ /// Path separators within options nodes are not split. For example,
+ /// `foo/{bar,baz/bang}/qux` will return three globs: `foo`, `{bar,baz/bang}`,
+ /// and `qux`.
+ ///
+ /// [context] is used to determine what absolute roots look like for this
+ /// glob.
+ List<SequenceNode> split(p.Context context) {
+ var componentsToReturn = <SequenceNode>[];
+ List<AstNode>? currentComponent;
+
+ void addNode(AstNode node) {
+ (currentComponent ??= []).add(node);
+ }
+
+ void finishComponent() {
+ if (currentComponent == null) return;
+ componentsToReturn
+ .add(SequenceNode(currentComponent!, caseSensitive: caseSensitive));
+ currentComponent = null;
+ }
+
+ for (var node in nodes) {
+ if (node is! LiteralNode) {
+ addNode(node);
+ continue;
+ }
+
+ if (!node.text.contains('/')) {
+ addNode(node);
+ continue;
+ }
+
+ var text = node.text;
+ if (context.style == p.Style.windows) text = text.replaceAll('/', '\\');
+ Iterable<String> components = context.split(text);
+
+ // If the first component is absolute, that means it's a separator (on
+ // Windows some non-separator things are also absolute, but it's invalid
+ // to have "C:" show up in the middle of a path anyway).
+ if (context.isAbsolute(components.first)) {
+ // If this is the first component, it's the root.
+ if (componentsToReturn.isEmpty && currentComponent == null) {
+ var root = components.first;
+ if (context.style == p.Style.windows) {
+ // Above, we switched to backslashes to make [context.split] handle
+ // roots properly. That means that if there is a root, it'll still
+ // have backslashes, where forward slashes are required for globs.
+ // So we switch it back here.
+ root = root.replaceAll('\\', '/');
+ }
+ addNode(LiteralNode(root, caseSensitive: caseSensitive));
+ }
+ finishComponent();
+ components = components.skip(1);
+ if (components.isEmpty) continue;
+ }
+
+ // For each component except the last one, add a separate sequence to
+ // [sequences] containing only that component.
+ for (var component in components.take(components.length - 1)) {
+ addNode(LiteralNode(component, caseSensitive: caseSensitive));
+ finishComponent();
+ }
+
+ // For the final component, only end its sequence (by adding a new empty
+ // sequence) if it ends with a separator.
+ addNode(LiteralNode(components.last, caseSensitive: caseSensitive));
+ if (node.text.endsWith('/')) finishComponent();
+ }
+
+ finishComponent();
+ return componentsToReturn;
+ }
+
+ @override
+ String _toRegExp() => nodes.map((node) => node._toRegExp()).join();
+
+ @override
+ bool operator ==(Object other) =>
+ other is SequenceNode &&
+ const IterableEquality<AstNode>().equals(nodes, other.nodes);
+
+ @override
+ int get hashCode => const IterableEquality<AstNode>().hash(nodes);
+
+ @override
+ String toString() => nodes.join();
+}
+
+/// A node matching zero or more non-separator characters.
+class StarNode extends AstNode {
+ StarNode({bool caseSensitive = true}) : super._(caseSensitive);
+
+ @override
+ String _toRegExp() => '[^/]*';
+
+ @override
+ bool operator ==(Object other) => other is StarNode;
+
+ @override
+ int get hashCode => 0;
+
+ @override
+ String toString() => '*';
+}
+
+/// A node matching zero or more characters that may be separators.
+class DoubleStarNode extends AstNode {
+ /// The path context for the glob.
+ ///
+ /// This is used to determine what absolute paths look like.
+ final p.Context _context;
+
+ DoubleStarNode(this._context, {bool caseSensitive = true})
+ : super._(caseSensitive);
+
+ @override
+ String _toRegExp() {
+ // Double star shouldn't match paths with a leading "../", since these paths
+ // wouldn't be listed with this glob. We only check for "../" at the
+ // beginning since the paths are normalized before being checked against the
+ // glob.
+ var buffer = StringBuffer()..write(r'(?!^(?:\.\./|');
+
+ // A double star at the beginning of the glob also shouldn't match absolute
+ // paths, since those also wouldn't be listed. Which root patterns we look
+ // for depends on the style of path we're matching.
+ if (_context.style == p.Style.posix) {
+ buffer.write(r'/');
+ } else if (_context.style == p.Style.windows) {
+ buffer.write(r'//|[A-Za-z]:/');
+ } else {
+ assert(_context.style == p.Style.url);
+ buffer.write(r'[a-zA-Z][-+.a-zA-Z\d]*://|/');
+ }
+
+ // Use `[^]` rather than `.` so that it matches newlines as well.
+ buffer.write(r'))[^]*');
+
+ return buffer.toString();
+ }
+
+ @override
+ bool operator ==(Object other) => other is DoubleStarNode;
+
+ @override
+ int get hashCode => 1;
+
+ @override
+ String toString() => '**';
+}
+
+/// A node matching a single non-separator character.
+class AnyCharNode extends AstNode {
+ AnyCharNode({bool caseSensitive = true}) : super._(caseSensitive);
+
+ @override
+ String _toRegExp() => '[^/]';
+
+ @override
+ bool operator ==(Object other) => other is AnyCharNode;
+
+ @override
+ int get hashCode => 2;
+
+ @override
+ String toString() => '?';
+}
+
+/// A node matching a single character in a range of options.
+class RangeNode extends AstNode {
+ /// The ranges matched by this node.
+ ///
+ /// The ends of the ranges are unicode code points.
+ final Set<Range> ranges;
+
+ /// Whether this range was negated.
+ final bool negated;
+
+ RangeNode(Iterable<Range> ranges,
+ {required this.negated, bool caseSensitive = true})
+ : ranges = ranges.toSet(),
+ super._(caseSensitive);
+
+ @override
+ OptionsNode flattenOptions() {
+ if (negated || ranges.any((range) => !range.isSingleton)) {
+ return super.flattenOptions();
+ }
+
+ // If a range explicitly lists a set of characters, return each character as
+ // a separate expansion.
+ return OptionsNode(ranges.map((range) {
+ return SequenceNode([
+ LiteralNode(String.fromCharCodes([range.min]),
+ caseSensitive: caseSensitive)
+ ], caseSensitive: caseSensitive);
+ }), caseSensitive: caseSensitive);
+ }
+
+ @override
+ String _toRegExp() {
+ var buffer = StringBuffer();
+
+ var containsSeparator = ranges.any((range) => range.contains(_separator));
+ if (!negated && containsSeparator) {
+ // Add `(?!/)` because ranges are never allowed to match separators.
+ buffer.write('(?!/)');
+ }
+
+ buffer.write('[');
+ if (negated) {
+ buffer.write('^');
+ // If the range doesn't itself exclude separators, exclude them ourselves,
+ // since ranges are never allowed to match them.
+ if (!containsSeparator) buffer.write('/');
+ }
+
+ for (var range in ranges) {
+ var start = String.fromCharCodes([range.min]);
+ buffer.write(regExpQuote(start));
+ if (range.isSingleton) continue;
+ buffer.write('-');
+ buffer.write(regExpQuote(String.fromCharCodes([range.max])));
+ }
+
+ buffer.write(']');
+ return buffer.toString();
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is RangeNode &&
+ other.negated == negated &&
+ const SetEquality<Range>().equals(ranges, other.ranges);
+
+ @override
+ int get hashCode =>
+ (negated ? 1 : 3) * const SetEquality<Range>().hash(ranges);
+
+ @override
+ String toString() {
+ var buffer = StringBuffer()..write('[');
+ for (var range in ranges) {
+ buffer.writeCharCode(range.min);
+ if (range.isSingleton) continue;
+ buffer.write('-');
+ buffer.writeCharCode(range.max);
+ }
+ buffer.write(']');
+ return buffer.toString();
+ }
+}
+
+/// A node that matches one of several options.
+class OptionsNode extends AstNode {
+ /// The options to match.
+ final List<SequenceNode> options;
+
+ @override
+ bool get canMatchAbsolute => options.any((node) => node.canMatchAbsolute);
+
+ @override
+ bool get canMatchRelative => options.any((node) => node.canMatchRelative);
+
+ OptionsNode(Iterable<SequenceNode> options, {bool caseSensitive = true})
+ : options = options.toList(),
+ super._(caseSensitive);
+
+ @override
+ OptionsNode flattenOptions() =>
+ OptionsNode(options.expand((option) => option.flattenOptions().options),
+ caseSensitive: caseSensitive);
+
+ @override
+ String _toRegExp() =>
+ '(?:${options.map((option) => option._toRegExp()).join("|")})';
+
+ @override
+ bool operator ==(Object other) =>
+ other is OptionsNode &&
+ const UnorderedIterableEquality<SequenceNode>()
+ .equals(options, other.options);
+
+ @override
+ int get hashCode =>
+ const UnorderedIterableEquality<SequenceNode>().hash(options);
+
+ @override
+ String toString() => '{${options.join(',')}}';
+}
+
+/// A node that matches a literal string.
+class LiteralNode extends AstNode {
+ /// The string to match.
+ final String text;
+
+ /// The path context for the glob.
+ ///
+ /// This is used to determine whether this could match an absolute path.
+ final p.Context? _context;
+
+ @override
+ bool get canMatchAbsolute {
+ var nativeText =
+ _context!.style == p.Style.windows ? text.replaceAll('/', '\\') : text;
+ return _context.isAbsolute(nativeText);
+ }
+
+ @override
+ bool get canMatchRelative => !canMatchAbsolute;
+
+ LiteralNode(this.text, {p.Context? context, bool caseSensitive = true})
+ : _context = context,
+ super._(caseSensitive);
+
+ @override
+ String _toRegExp() => regExpQuote(text);
+
+ @override
+ bool operator ==(Object other) => other is LiteralNode && other.text == text;
+
+ @override
+ int get hashCode => text.hashCode;
+
+ @override
+ String toString() => text;
+}
diff --git a/pkgs/glob/lib/src/list_tree.dart b/pkgs/glob/lib/src/list_tree.dart
new file mode 100644
index 0000000..2a6c020
--- /dev/null
+++ b/pkgs/glob/lib/src/list_tree.dart
@@ -0,0 +1,514 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:file/file.dart';
+import 'package:path/path.dart' as p;
+
+import 'ast.dart';
+import 'utils.dart';
+
+/// The errno for a file or directory not existing on Mac and Linux.
+const _enoent = 2;
+
+/// Another errno we see on Windows when trying to list a non-existent
+/// directory.
+const _enoentWin = 3;
+
+/// A structure built from a glob that efficiently lists filesystem entities
+/// that match that glob.
+///
+/// This structure is designed to list the minimal number of physical
+/// directories necessary to find everything that matches the glob. For example,
+/// for the glob `foo/{bar,baz}/*`, there's no need to list the working
+/// directory or even `foo/`; only `foo/bar` and `foo/baz` should be listed.
+///
+/// This works by creating a tree of [_ListTreeNode]s, each of which corresponds
+/// to a single of directory nesting in the source glob. Each node has child
+/// nodes associated with globs ([_ListTreeNode.children]), as well as its own
+/// glob ([_ListTreeNode._validator]) that indicates which entities within that
+/// node's directory should be returned.
+///
+/// For example, the glob `foo/{*.dart,b*/*.txt}` creates the following tree:
+///
+/// .
+/// '-- "foo" (validator: "*.dart")
+/// '-- "b*" (validator: "*.txt"
+///
+/// If a node doesn't have a validator, we know we don't have to list it
+/// explicitly.
+///
+/// Nodes can also be marked as "recursive", which means they need to be listed
+/// recursively (usually to support `**`). In this case, they will have no
+/// children; instead, their validator will just encompass the globs that would
+/// otherwise be in their children. For example, the glob
+/// `foo/{**.dart,bar/*.txt}` creates a recursive node for `foo` with the
+/// validator `**.dart,bar/*.txt`.
+///
+/// If the glob contains multiple filesystem roots (e.g. `{C:/,D:/}*.dart`),
+/// each root will have its own tree of nodes. Relative globs use `.` as their
+/// root instead.
+class ListTree {
+ /// A map from filesystem roots to the list tree for those roots.
+ ///
+ /// A relative glob will use `.` as its root.
+ final Map<String, _ListTreeNode> _trees;
+
+ /// Whether paths listed might overlap.
+ ///
+ /// If they do, we need to filter out overlapping paths.
+ final bool _canOverlap;
+
+ /// The file system to operate on.
+ final FileSystem _fileSystem;
+
+ ListTree._(this._trees, this._fileSystem)
+ : _canOverlap = _computeCanOverlap(_trees);
+
+ factory ListTree(AstNode glob, FileSystem fileSystem) {
+ // The first step in constructing a tree from the glob is to simplify the
+ // problem by eliminating options. [glob.flattenOptions] bubbles all options
+ // (and certain ranges) up to the top level of the glob so we can deal with
+ // them one at a time.
+ var options = glob.flattenOptions();
+ var trees = <String, _ListTreeNode>{};
+
+ for (var option in options.options) {
+ // Since each option doesn't include its own options, we can safely split
+ // it into path components.
+ var components = option.split(p.context);
+ var firstNode = components.first.nodes.first;
+ var root = '.';
+
+ // Determine the root for this option, if it's absolute. If it's not, the
+ // root's just ".".
+ if (firstNode is LiteralNode) {
+ var text = firstNode.text;
+ // Platform agnostic way of checking for Windows without `dart:io`.
+ if (p.context == p.windows) text.replaceAll('/', '\\');
+ if (p.isAbsolute(text)) {
+ // If the path is absolute, the root should be the only thing in the
+ // first component.
+ assert(components.first.nodes.length == 1);
+ root = firstNode.text;
+ components.removeAt(0);
+ }
+ }
+
+ _addGlob(root, components, trees);
+ }
+
+ return ListTree._(trees, fileSystem);
+ }
+
+ /// Add the glob represented by [components] to the tree under [root].
+ static void _addGlob(String root, List<SequenceNode> components,
+ Map<String, _ListTreeNode> trees) {
+ // The first [parent] represents the root directory itself. It may be null
+ // here if this is the first option with this particular [root]. If so,
+ // we'll create it below.
+ //
+ // As we iterate through [components], [parent] will be set to
+ // progressively more nested nodes.
+ var parent = trees[root];
+ for (var i = 0; i < components.length; i++) {
+ var component = components[i];
+ var recursive = component.nodes.any((node) => node is DoubleStarNode);
+ var complete = i == components.length - 1;
+
+ // If the parent node for this level of nesting already exists, the new
+ // option will be added to it as additional validator options and/or
+ // additional children.
+ //
+ // If the parent doesn't exist, we'll create it in one of the else
+ // clauses below.
+ if (parent != null) {
+ if (parent.isRecursive || recursive) {
+ // If [component] is recursive, mark [parent] as recursive. This
+ // will cause all of its children to be folded into its validator.
+ // If [parent] was already recursive, this is a no-op.
+ parent.makeRecursive();
+
+ // Add [component] and everything nested beneath it as an option to
+ // [parent]. Since [parent] is recursive, it will recursively list
+ // everything beneath it and filter them with one big glob.
+ parent.addOption(_join(components.sublist(i)));
+ return;
+ } else if (complete) {
+ // If [component] is the last component, add it to [parent]'s
+ // validator but not to its children.
+ parent.addOption(component);
+ } else {
+ // On the other hand if there are more components, add [component]
+ // to [parent]'s children and not its validator. Since we process
+ // each option's components separately, the same component is never
+ // both a validator and a child.
+ var children = parent.children!;
+ if (!children.containsKey(component)) {
+ children[component] = _ListTreeNode();
+ }
+ parent = children[component];
+ }
+ } else if (recursive) {
+ trees[root] = _ListTreeNode.recursive(_join(components.sublist(i)));
+ return;
+ } else if (complete) {
+ trees[root] = _ListTreeNode()..addOption(component);
+ } else {
+ var rootNode = _ListTreeNode();
+ trees[root] = rootNode;
+ var rootChildren = rootNode.children!;
+ rootChildren[component] = _ListTreeNode();
+ parent = rootChildren[component];
+ }
+ }
+ }
+
+ /// Computes the value for [_canOverlap].
+ static bool _computeCanOverlap(Map<String, _ListTreeNode> trees) {
+ // If this can list a relative path and an absolute path, the former may be
+ // contained within the latter.
+ if (trees.length > 1 && trees.containsKey('.')) return true;
+
+ // Otherwise, this can only overlap if the tree beneath any given root could
+ // overlap internally.
+ return trees.values.any((node) => node.canOverlap);
+ }
+
+ /// List all entities that match this glob beneath [root].
+ Stream<FileSystemEntity> list({String? root, bool followLinks = true}) {
+ root ??= '.';
+ var group = StreamGroup<FileSystemEntity>();
+ for (var rootDir in _trees.keys) {
+ var dir = rootDir == '.' ? root : rootDir;
+ group.add(
+ _trees[rootDir]!.list(dir, _fileSystem, followLinks: followLinks));
+ }
+ group.close();
+
+ if (!_canOverlap) return group.stream;
+
+ // TODO: Rather than filtering here, avoid double-listing directories
+ // in the first place.
+ var seen = <String>{};
+ return group.stream.where((entity) => seen.add(entity.path));
+ }
+
+ /// Synchronosuly list all entities that match this glob beneath [root].
+ List<FileSystemEntity> listSync({String? root, bool followLinks = true}) {
+ root ??= '.';
+ var result = _trees.keys.expand((rootDir) {
+ var dir = rootDir == '.' ? root! : rootDir;
+ return _trees[rootDir]!
+ .listSync(dir, _fileSystem, followLinks: followLinks);
+ });
+
+ if (!_canOverlap) return result.toList();
+
+ // TODO: Rather than filtering here, avoid double-listing directories
+ // in the first place.
+ var seen = <String>{};
+ return result.where((entity) => seen.add(entity.path)).toList();
+ }
+}
+
+/// A single node in a [ListTree].
+class _ListTreeNode {
+ /// This node's child nodes, by their corresponding globs.
+ ///
+ /// Each child node will only be listed on directories that match its glob.
+ ///
+ /// This may be `null`, indicating that this node should be listed
+ /// recursively.
+ Map<SequenceNode, _ListTreeNode>? children;
+
+ /// This node's validator.
+ ///
+ /// This determines which entities will ultimately be emitted when [list] is
+ /// called.
+ OptionsNode? _validator;
+
+ /// Whether this node is recursive.
+ ///
+ /// A recursive node has no children and is listed recursively.
+ bool get isRecursive => children == null;
+
+ bool get _caseSensitive {
+ if (_validator != null) return _validator!.caseSensitive;
+ if (children?.isEmpty != false) return true;
+ return children!.keys.first.caseSensitive;
+ }
+
+ /// Whether this node doesn't itself need to be listed.
+ ///
+ /// If a node has no validator and all of its children are literal filenames,
+ /// there's no need to list its contents. We can just directly traverse into
+ /// its children.
+ bool get _isIntermediate {
+ if (_validator != null) return false;
+ return children!.keys.every((sequence) =>
+ sequence.nodes.length == 1 && sequence.nodes.first is LiteralNode);
+ }
+
+ /// Returns whether listing this node might return overlapping results.
+ bool get canOverlap {
+ // A recusive node can never overlap with itself, because it will only ever
+ // involve a single call to [Directory.list] that's then filtered with
+ // [_validator].
+ if (isRecursive) return false;
+
+ // If there's more than one child node and at least one of the children is
+ // dynamic (that is, matches more than just a literal string), there may be
+ // overlap.
+ if (children!.length > 1) {
+ // Case-insensitivity means that even literals may match multiple entries.
+ if (!_caseSensitive) return true;
+
+ if (children!.keys.any((sequence) =>
+ sequence.nodes.length > 1 || sequence.nodes.single is! LiteralNode)) {
+ return true;
+ }
+ }
+
+ return children!.values.any((node) => node.canOverlap);
+ }
+
+ /// Creates a node with no children and no validator.
+ _ListTreeNode()
+ : children = <SequenceNode, _ListTreeNode>{},
+ _validator = null;
+
+ /// Creates a recursive node the given [validator].
+ _ListTreeNode.recursive(SequenceNode validator)
+ : children = null,
+ _validator =
+ OptionsNode([validator], caseSensitive: validator.caseSensitive);
+
+ /// Transforms this into recursive node, folding all its children into its
+ /// validator.
+ void makeRecursive() {
+ if (isRecursive) return;
+ var children = this.children!;
+ _validator = OptionsNode(children.entries.map((entry) {
+ entry.value.makeRecursive();
+ return _join([entry.key, entry.value._validator!]);
+ }), caseSensitive: _caseSensitive);
+ this.children = null;
+ }
+
+ /// Adds [validator] to this node's existing validator.
+ void addOption(SequenceNode validator) {
+ if (_validator == null) {
+ _validator =
+ OptionsNode([validator], caseSensitive: validator.caseSensitive);
+ } else {
+ _validator!.options.add(validator);
+ }
+ }
+
+ /// Lists all entities within [dir] matching this node or its children.
+ ///
+ /// This may return duplicate entities. These will be filtered out in
+ /// [ListTree.list].
+ Stream<FileSystemEntity> list(String dir, FileSystem fileSystem,
+ {bool followLinks = true}) {
+ if (isRecursive) {
+ return fileSystem
+ .directory(dir)
+ .list(recursive: true, followLinks: followLinks)
+ .ignoreMissing()
+ .where((entity) => _matches(p.relative(entity.path, from: dir)));
+ }
+
+ // Don't spawn extra [Directory.list] calls when we already know exactly
+ // which subdirectories we're interested in.
+ if (_isIntermediate && _caseSensitive) {
+ var resultGroup = StreamGroup<FileSystemEntity>();
+ children!.forEach((sequence, child) {
+ resultGroup.add(child.list(
+ p.join(dir, (sequence.nodes.single as LiteralNode).text),
+ fileSystem,
+ followLinks: followLinks));
+ });
+ resultGroup.close();
+ return resultGroup.stream;
+ }
+
+ return StreamCompleter.fromFuture(() async {
+ var entities = await fileSystem
+ .directory(dir)
+ .list(followLinks: followLinks)
+ .ignoreMissing()
+ .toList();
+ await _validateIntermediateChildrenAsync(dir, entities, fileSystem);
+
+ var resultGroup = StreamGroup<FileSystemEntity>();
+ var resultController = StreamController<FileSystemEntity>(sync: true);
+ unawaited(resultGroup.add(resultController.stream));
+ for (var entity in entities) {
+ var basename = p.relative(entity.path, from: dir);
+ if (_matches(basename)) resultController.add(entity);
+
+ children!.forEach((sequence, child) {
+ if (entity is! Directory) return;
+ if (!sequence.matches(basename)) return;
+ var stream = child.list(p.join(dir, basename), fileSystem,
+ followLinks: followLinks);
+ resultGroup.add(stream);
+ });
+ }
+ unawaited(resultController.close());
+ unawaited(resultGroup.close());
+ return resultGroup.stream;
+ }());
+ }
+
+ /// If this is a case-insensitive list, validates that all intermediate
+ /// children (according to [_isIntermediate]) match at least one entity in
+ /// [entities].
+ ///
+ /// This ensures that listing "foo/bar/*" fails on case-sensitive systems if
+ /// "foo/bar" doesn't exist.
+ Future _validateIntermediateChildrenAsync(String dir,
+ List<FileSystemEntity> entities, FileSystem fileSystem) async {
+ if (_caseSensitive) return;
+
+ for (var entry in children!.entries) {
+ var child = entry.value;
+ var sequence = entry.key;
+ if (!child._isIntermediate) continue;
+ if (entities.any(
+ (entity) => sequence.matches(p.relative(entity.path, from: dir)))) {
+ continue;
+ }
+
+ // We know this will fail, we're just doing it to force dart:io to emit
+ // the exception it would if we were listing case-sensitively.
+ await child
+ .list(p.join(dir, (sequence.nodes.single as LiteralNode).text),
+ fileSystem)
+ .toList();
+ }
+ }
+
+ /// Synchronously lists all entities within [dir] matching this node or its
+ /// children.
+ ///
+ /// This may return duplicate entities. These will be filtered out in
+ /// [ListTree.listSync].
+ Iterable<FileSystemEntity> listSync(String dir, FileSystem fileSystem,
+ {bool followLinks = true}) {
+ if (isRecursive) {
+ try {
+ return fileSystem
+ .directory(dir)
+ .listSync(recursive: true, followLinks: followLinks)
+ .where((entity) => _matches(p.relative(entity.path, from: dir)));
+ } on FileSystemException catch (error) {
+ if (error.isMissing) return const [];
+ rethrow;
+ }
+ }
+
+ // Don't spawn extra [Directory.listSync] calls when we already know exactly
+ // which subdirectories we're interested in.
+ if (_isIntermediate && _caseSensitive) {
+ return children!.entries.expand((entry) {
+ var sequence = entry.key;
+ var child = entry.value;
+ return child.listSync(
+ p.join(dir, (sequence.nodes.single as LiteralNode).text),
+ fileSystem,
+ followLinks: followLinks);
+ });
+ }
+
+ List<FileSystemEntity> entities;
+ try {
+ entities = fileSystem.directory(dir).listSync(followLinks: followLinks);
+ } on FileSystemException catch (error) {
+ if (error.isMissing) return const [];
+ rethrow;
+ }
+ _validateIntermediateChildrenSync(dir, entities, fileSystem);
+
+ return entities.expand((entity) {
+ var entities = <FileSystemEntity>[];
+ var basename = p.relative(entity.path, from: dir);
+ if (_matches(basename)) entities.add(entity);
+ if (entity is! Directory) return entities;
+
+ entities.addAll(children!.keys
+ .where((sequence) => sequence.matches(basename))
+ .expand((sequence) {
+ return children![sequence]!
+ .listSync(p.join(dir, basename), fileSystem,
+ followLinks: followLinks)
+ .toList();
+ }));
+
+ return entities;
+ });
+ }
+
+ /// If this is a case-insensitive list, validates that all intermediate
+ /// children (according to [_isIntermediate]) match at least one entity in
+ /// [entities].
+ ///
+ /// This ensures that listing "foo/bar/*" fails on case-sensitive systems if
+ /// "foo/bar" doesn't exist.
+ void _validateIntermediateChildrenSync(
+ String dir, List<FileSystemEntity> entities, FileSystem fileSystem) {
+ if (_caseSensitive) return;
+
+ children!.forEach((sequence, child) {
+ if (!child._isIntermediate) return;
+ if (entities.any(
+ (entity) => sequence.matches(p.relative(entity.path, from: dir)))) {
+ return;
+ }
+
+ // If there are no [entities] that match [sequence], manually list the
+ // directory to force `dart:io` to throw an error. This allows us to
+ // ensure that listing "foo/bar/*" fails on case-sensitive systems if
+ // "foo/bar" doesn't exist.
+ child.listSync(
+ p.join(dir, (sequence.nodes.single as LiteralNode).text), fileSystem);
+ });
+ }
+
+ /// Returns whether the native [path] matches [_validator].
+ bool _matches(String path) =>
+ _validator?.matches(toPosixPath(p.context, path)) ?? false;
+
+ @override
+ String toString() => '($_validator) $children';
+}
+
+/// Joins each [components] into a new glob where each component is separated by
+/// a path separator.
+SequenceNode _join(Iterable<AstNode> components) {
+ var componentsList = components.toList();
+ var first = componentsList.removeAt(0);
+ var nodes = [first];
+ for (var component in componentsList) {
+ nodes.add(LiteralNode('/', caseSensitive: first.caseSensitive));
+ nodes.add(component);
+ }
+ return SequenceNode(nodes, caseSensitive: first.caseSensitive);
+}
+
+extension on Stream<FileSystemEntity> {
+ Stream<FileSystemEntity> ignoreMissing() => handleError((_) {},
+ test: (error) => error is FileSystemException && error.isMissing);
+}
+
+extension on FileSystemException {
+ bool get isMissing {
+ final errorCode = osError?.errorCode;
+ return errorCode == _enoent || errorCode == _enoentWin;
+ }
+}
diff --git a/pkgs/glob/lib/src/parser.dart b/pkgs/glob/lib/src/parser.dart
new file mode 100644
index 0000000..e76f7c5
--- /dev/null
+++ b/pkgs/glob/lib/src/parser.dart
@@ -0,0 +1,176 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+import 'package:string_scanner/string_scanner.dart';
+
+import 'ast.dart';
+import 'utils.dart';
+
+const _hyphen = 0x2D;
+const _slash = 0x2F;
+
+/// A parser for globs.
+class Parser {
+ /// The scanner used to scan the source.
+ final StringScanner _scanner;
+
+ /// The path context for the glob.
+ final p.Context _context;
+
+ /// Whether this glob is case-sensitive.
+ final bool _caseSensitive;
+
+ Parser(String component, this._context, {bool caseSensitive = true})
+ : _scanner = StringScanner(component),
+ _caseSensitive = caseSensitive;
+
+ /// Parses an entire glob.
+ SequenceNode parse() => _parseSequence();
+
+ /// Parses a [SequenceNode].
+ ///
+ /// If [inOptions] is true, this is parsing within an [OptionsNode].
+ SequenceNode _parseSequence({bool inOptions = false}) {
+ var nodes = <AstNode>[];
+
+ if (_scanner.isDone) {
+ _scanner.error('expected a glob.', position: 0, length: 0);
+ }
+
+ while (!_scanner.isDone) {
+ if (inOptions && (_scanner.matches(',') || _scanner.matches('}'))) break;
+ nodes.add(_parseNode(inOptions: inOptions));
+ }
+
+ return SequenceNode(nodes, caseSensitive: _caseSensitive);
+ }
+
+ /// Parses an [AstNode].
+ ///
+ /// If [inOptions] is true, this is parsing within an [OptionsNode].
+ AstNode _parseNode({bool inOptions = false}) {
+ var star = _parseStar();
+ if (star != null) return star;
+
+ var anyChar = _parseAnyChar();
+ if (anyChar != null) return anyChar;
+
+ var range = _parseRange();
+ if (range != null) return range;
+
+ var options = _parseOptions();
+ if (options != null) return options;
+
+ return _parseLiteral(inOptions: inOptions);
+ }
+
+ /// Tries to parse a [StarNode] or a [DoubleStarNode].
+ ///
+ /// Returns `null` if there's not one to parse.
+ AstNode? _parseStar() {
+ if (!_scanner.scan('*')) return null;
+ return _scanner.scan('*')
+ ? DoubleStarNode(_context, caseSensitive: _caseSensitive)
+ : StarNode(caseSensitive: _caseSensitive);
+ }
+
+ /// Tries to parse an [AnyCharNode].
+ ///
+ /// Returns `null` if there's not one to parse.
+ AstNode? _parseAnyChar() {
+ if (!_scanner.scan('?')) return null;
+ return AnyCharNode(caseSensitive: _caseSensitive);
+ }
+
+ /// Tries to parse an [RangeNode].
+ ///
+ /// Returns `null` if there's not one to parse.
+ AstNode? _parseRange() {
+ if (!_scanner.scan('[')) return null;
+ if (_scanner.matches(']')) _scanner.error('unexpected "]".');
+ var negated = _scanner.scan('!') || _scanner.scan('^');
+
+ int readRangeChar() {
+ var char = _scanner.readChar();
+ if (negated || char != _slash) return char;
+ _scanner.error('"/" may not be used in a range.',
+ position: _scanner.position - 1);
+ }
+
+ var ranges = <Range>[];
+ while (!_scanner.scan(']')) {
+ var start = _scanner.position;
+ // Allow a backslash to escape a character.
+ _scanner.scan('\\');
+ var char = readRangeChar();
+
+ if (_scanner.scan('-')) {
+ if (_scanner.matches(']')) {
+ ranges.add(Range.singleton(char));
+ ranges.add(Range.singleton(_hyphen));
+ continue;
+ }
+
+ // Allow a backslash to escape a character.
+ _scanner.scan('\\');
+
+ var end = readRangeChar();
+
+ if (end < char) {
+ _scanner.error('Range out of order.',
+ position: start, length: _scanner.position - start);
+ }
+ ranges.add(Range(char, end));
+ } else {
+ ranges.add(Range.singleton(char));
+ }
+ }
+
+ return RangeNode(ranges, negated: negated, caseSensitive: _caseSensitive);
+ }
+
+ /// Tries to parse an [OptionsNode].
+ ///
+ /// Returns `null` if there's not one to parse.
+ AstNode? _parseOptions() {
+ if (!_scanner.scan('{')) return null;
+ if (_scanner.matches('}')) _scanner.error('unexpected "}".');
+
+ var options = <SequenceNode>[];
+ do {
+ options.add(_parseSequence(inOptions: true));
+ } while (_scanner.scan(','));
+
+ // Don't allow single-option blocks.
+ if (options.length == 1) _scanner.expect(',');
+ _scanner.expect('}');
+
+ return OptionsNode(options, caseSensitive: _caseSensitive);
+ }
+
+ /// Parses a [LiteralNode].
+ AstNode _parseLiteral({bool inOptions = false}) {
+ // If we're in an options block, we want to stop parsing as soon as we hit a
+ // comma. Otherwise, commas are fair game for literals.
+ var regExp = RegExp(inOptions ? r'[^*{[?\\}\],()]*' : r'[^*{[?\\}\]()]*');
+
+ _scanner.scan(regExp);
+ var buffer = StringBuffer()..write(_scanner.lastMatch![0]);
+
+ while (_scanner.scan('\\')) {
+ buffer.writeCharCode(_scanner.readChar());
+ _scanner.scan(regExp);
+ buffer.write(_scanner.lastMatch![0]);
+ }
+
+ for (var char in const [']', '(', ')']) {
+ if (_scanner.matches(char)) _scanner.error('unexpected "$char"');
+ }
+ if (!inOptions && _scanner.matches('}')) _scanner.error('unexpected "}"');
+
+ return LiteralNode(buffer.toString(),
+ context: _context, caseSensitive: _caseSensitive);
+ }
+}
diff --git a/pkgs/glob/lib/src/stream_pool.dart b/pkgs/glob/lib/src/stream_pool.dart
new file mode 100644
index 0000000..8317ae6
--- /dev/null
+++ b/pkgs/glob/lib/src/stream_pool.dart
@@ -0,0 +1,73 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+/// A pool of streams whose events are unified and emitted through a central
+/// stream.
+class StreamPool<T> {
+ /// The stream through which all events from streams in the pool are emitted.
+ Stream<T> get stream => _controller.stream;
+ final StreamController<T> _controller;
+
+ /// Subscriptions to the streams that make up the pool.
+ final _subscriptions = <Stream<T>, StreamSubscription<T>>{};
+
+ /// Whether this pool should be closed when it becomes empty.
+ bool _closeWhenEmpty = false;
+
+ /// Creates a new stream pool that only supports a single subscriber.
+ ///
+ /// Any events from broadcast streams in the pool will be buffered until a
+ /// listener is subscribed.
+ StreamPool()
+ // Create the controller as sync so that any sync input streams will be
+ // forwarded synchronously. Async input streams will have their asynchrony
+ // preserved, since _controller.add will be called asynchronously.
+ : _controller = StreamController<T>(sync: true);
+
+ /// Creates a new stream pool where [stream] can be listened to more than
+ /// once.
+ ///
+ /// Any events from buffered streams in the pool will be emitted immediately,
+ /// regardless of whether [stream] has any subscribers.
+ StreamPool.broadcast()
+ // Create the controller as sync so that any sync input streams will be
+ // forwarded synchronously. Async input streams will have their asynchrony
+ // preserved, since _controller.add will be called asynchronously.
+ : _controller = StreamController<T>.broadcast(sync: true);
+
+ /// Adds [stream] as a member of this pool.
+ ///
+ /// Any events from [stream] will be emitted through [this.stream]. If
+ /// [stream] is sync, they'll be emitted synchronously; if [stream] is async,
+ /// they'll be emitted asynchronously.
+ void add(Stream<T> stream) {
+ if (_subscriptions.containsKey(stream)) return;
+ _subscriptions[stream] = stream.listen(_controller.add,
+ onError: _controller.addError, onDone: () => remove(stream));
+ }
+
+ /// Removes [stream] as a member of this pool.
+ void remove(Stream<T> stream) {
+ var subscription = _subscriptions.remove(stream);
+ if (subscription != null) subscription.cancel();
+ if (_closeWhenEmpty && _subscriptions.isEmpty) close();
+ }
+
+ /// Removes all streams from this pool and closes [stream].
+ void close() {
+ for (var subscription in _subscriptions.values) {
+ subscription.cancel();
+ }
+ _subscriptions.clear();
+ _controller.close();
+ }
+
+ /// The next time this pool becomes empty, close it.
+ void closeWhenEmpty() {
+ if (_subscriptions.isEmpty) close();
+ _closeWhenEmpty = true;
+ }
+}
diff --git a/pkgs/glob/lib/src/utils.dart b/pkgs/glob/lib/src/utils.dart
new file mode 100644
index 0000000..64bc974
--- /dev/null
+++ b/pkgs/glob/lib/src/utils.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+
+/// A range from [min] to [max], inclusive.
+class Range {
+ /// The minimum value included by the range.
+ final int min;
+
+ /// The maximum value included by the range.
+ final int max;
+
+ /// Whether this range covers only a single number.
+ bool get isSingleton => min == max;
+
+ Range(this.min, this.max);
+
+ /// Returns a range that covers only [value].
+ Range.singleton(int value) : this(value, value);
+
+ /// Whether `this` contains [value].
+ bool contains(int value) => value >= min && value <= max;
+
+ @override
+ bool operator ==(Object other) =>
+ other is Range && other.min == min && other.max == max;
+
+ @override
+ int get hashCode => 3 * min + 7 * max;
+}
+
+/// An implementation of [Match] constructed by `Glob`s.
+class GlobMatch implements Match {
+ @override
+ final String input;
+ @override
+ final Pattern pattern;
+ @override
+ final int start = 0;
+
+ @override
+ int get end => input.length;
+ @override
+ int get groupCount => 0;
+
+ GlobMatch(this.input, this.pattern);
+
+ @override
+ String operator [](int group) => this.group(group);
+
+ @override
+ String group(int group) {
+ if (group != 0) throw RangeError.range(group, 0, 0);
+ return input;
+ }
+
+ @override
+ List<String> groups(List<int> groupIndices) =>
+ groupIndices.map(group).toList();
+}
+
+final _quote = RegExp(r'[+*?{}|[\]\\().^$-]');
+
+/// Returns [contents] with characters that are meaningful in regular
+/// expressions backslash-escaped.
+String regExpQuote(String contents) =>
+ contents.replaceAllMapped(_quote, (char) => '\\${char[0]}');
+
+/// Returns [path] with all its separators replaced with forward slashes.
+///
+/// This is useful when converting from Windows paths to globs.
+String separatorToForwardSlash(String path) {
+ if (p.style != p.Style.windows) return path;
+ return path.replaceAll('\\', '/');
+}
+
+/// Returns [path] which follows [context] converted to the POSIX format that
+/// globs match against.
+String toPosixPath(p.Context context, String path) {
+ if (context.style == p.Style.windows) return path.replaceAll('\\', '/');
+ if (context.style == p.Style.url) return Uri.decodeFull(path);
+ return path;
+}
diff --git a/pkgs/glob/pubspec.yaml b/pkgs/glob/pubspec.yaml
new file mode 100644
index 0000000..4da2c9f
--- /dev/null
+++ b/pkgs/glob/pubspec.yaml
@@ -0,0 +1,19 @@
+name: glob
+version: 2.1.3-wip
+description: A library to perform Bash-style file and directory globbing.
+repository: https://github.com/dart-lang/glob
+
+environment:
+ sdk: ^3.3.0
+
+dependencies:
+ async: ^2.5.0
+ collection: ^1.15.0
+ file: '>=6.1.3 <8.0.0'
+ path: ^1.8.0
+ string_scanner: ^1.1.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.17.0
+ test_descriptor: ^2.0.0
diff --git a/pkgs/glob/test/glob_test.dart b/pkgs/glob/test/glob_test.dart
new file mode 100644
index 0000000..85c3fac
--- /dev/null
+++ b/pkgs/glob/test/glob_test.dart
@@ -0,0 +1,111 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:glob/glob.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+ group('Glob.quote()', () {
+ test('quotes all active characters', () {
+ expect(Glob.quote('*{[?\\}],-'), equals(r'\*\{\[\?\\\}\]\,\-'));
+ });
+
+ test("doesn't quote inactive characters", () {
+ expect(Glob.quote('abc~`_+='), equals('abc~`_+='));
+ });
+ });
+
+ group('Glob.matches()', () {
+ test('returns whether the path matches the glob', () {
+ var glob = Glob('foo*');
+ expect(glob.matches('foobar'), isTrue);
+ expect(glob.matches('baz'), isFalse);
+ });
+
+ test('only matches the entire path', () {
+ var glob = Glob('foo');
+ expect(glob.matches('foo/bar'), isFalse);
+ expect(glob.matches('bar/foo'), isFalse);
+ });
+ });
+
+ group('Glob.matchAsPrefix()', () {
+ test('returns a match if the path matches the glob', () {
+ var glob = Glob('foo*');
+ expect(glob.matchAsPrefix('foobar'), isA<Match>());
+ expect(glob.matchAsPrefix('baz'), isNull);
+ });
+
+ test('returns null for start > 0', () {
+ var glob = Glob('*');
+ expect(glob.matchAsPrefix('foobar', 1), isNull);
+ });
+ });
+
+ group('Glob.allMatches()', () {
+ test('returns a single match if the path matches the glob', () {
+ var matches = Glob('foo*').allMatches('foobar');
+ expect(matches, hasLength(1));
+ expect(matches.first, isA<Match>());
+ });
+
+ test("returns an empty list if the path doesn't match the glob", () {
+ expect(Glob('foo*').allMatches('baz'), isEmpty);
+ });
+
+ test('returns no matches for start > 0', () {
+ var glob = Glob('*');
+ expect(glob.allMatches('foobar', 1), isEmpty);
+ });
+ });
+
+ group('GlobMatch', () {
+ var glob = Glob('foo*');
+ var match = glob.matchAsPrefix('foobar')!;
+
+ test('returns the string as input', () {
+ expect(match.input, equals('foobar'));
+ });
+
+ test('returns the glob as the pattern', () {
+ expect(match.pattern, equals(glob));
+ });
+
+ test('returns the span of the string for start and end', () {
+ expect(match.start, equals(0));
+ expect(match.end, equals('foobar'.length));
+ });
+
+ test('has a single group that contains the whole string', () {
+ expect(match.groupCount, equals(0));
+ expect(match[0], equals('foobar'));
+ expect(match.group(0), equals('foobar'));
+ expect(match.groups([0]), equals(['foobar']));
+ });
+
+ test('throws a range error for an invalid group', () {
+ expect(() => match[1], throwsRangeError);
+ expect(() => match[-1], throwsRangeError);
+ expect(() => match.group(1), throwsRangeError);
+ expect(() => match.groups([1]), throwsRangeError);
+ });
+ });
+
+ test('globs are case-sensitive by default for Posix and URL contexts', () {
+ expect('foo', contains(Glob('foo', context: p.posix)));
+ expect('FOO', isNot(contains(Glob('foo', context: p.posix))));
+ expect('foo', isNot(contains(Glob('FOO', context: p.posix))));
+
+ expect('foo', contains(Glob('foo', context: p.url)));
+ expect('FOO', isNot(contains(Glob('foo', context: p.url))));
+ expect('foo', isNot(contains(Glob('FOO', context: p.url))));
+ });
+
+ test('globs are case-insensitive by default for Windows contexts', () {
+ expect('foo', contains(Glob('foo', context: p.windows)));
+ expect('FOO', contains(Glob('foo', context: p.windows)));
+ expect('foo', contains(Glob('FOO', context: p.windows)));
+ });
+}
diff --git a/pkgs/glob/test/list_test.dart b/pkgs/glob/test/list_test.dart
new file mode 100644
index 0000000..e008d71
--- /dev/null
+++ b/pkgs/glob/test/list_test.dart
@@ -0,0 +1,349 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:glob/glob.dart';
+import 'package:glob/list_local_fs.dart';
+import 'package:glob/src/utils.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+void main() {
+ setUp(() async {
+ await d.dir('foo', [
+ d.file('bar'),
+ d.dir('baz', [d.file('bang'), d.file('qux')])
+ ]).create();
+ });
+
+ group('list()', () {
+ test("fails if the context doesn't match the system context", () {
+ expect(Glob('*', context: p.url).list, throwsStateError);
+ });
+
+ test('returns empty list for non-existent case-sensitive directories',
+ () async {
+ expect(await Glob('non/existent/**', caseSensitive: true).list().toList(),
+ <Never>[]);
+ });
+
+ test('returns empty list for non-existent case-insensitive directories',
+ () async {
+ expect(
+ await Glob('non/existent/**', caseSensitive: false).list().toList(),
+ <Never>[]);
+ });
+ });
+
+ group('listSync()', () {
+ test("fails if the context doesn't match the system context", () {
+ expect(Glob('*', context: p.url).listSync, throwsStateError);
+ });
+
+ test('returns empty list for non-existent case-sensitive directories', () {
+ expect(
+ Glob('non/existent/**', caseSensitive: true).listSync(), <Never>[]);
+ });
+
+ test('returns empty list for non-existent case-insensitive directories',
+ () {
+ expect(
+ Glob('non/existent/**', caseSensitive: false).listSync(), <Never>[]);
+ });
+ });
+
+ group('when case-sensitive', () {
+ test('lists literals case-sensitively', () {
+ expect(Glob('foo/BAZ/qux', caseSensitive: true).listSync(), <Never>[]);
+ });
+
+ test('lists ranges case-sensitively', () {
+ expect(Glob('foo/[BX][A-Z]z/qux', caseSensitive: true).listSync(),
+ <Never>[]);
+ });
+
+ test('options preserve case-sensitivity', () {
+ expect(
+ Glob('foo/{BAZ,ZAP}/qux', caseSensitive: true).listSync(), <Never>[]);
+ });
+ });
+
+ syncAndAsync((ListFn list) {
+ group('literals', () {
+ test('lists a single literal', () async {
+ expect(
+ await list('foo/baz/qux'), equals([p.join('foo', 'baz', 'qux')]));
+ });
+
+ test('lists a non-matching literal', () async {
+ expect(await list('foo/baz/nothing'), isEmpty);
+ });
+ });
+
+ group('star', () {
+ test('lists within filenames but not across directories', () async {
+ expect(await list('foo/b*'),
+ unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
+ });
+
+ test('lists the empy string', () async {
+ expect(await list('foo/bar*'), equals([p.join('foo', 'bar')]));
+ });
+ });
+
+ group('double star', () {
+ test('lists within filenames', () async {
+ expect(
+ await list('foo/baz/**'),
+ unorderedEquals(
+ [p.join('foo', 'baz', 'qux'), p.join('foo', 'baz', 'bang')]));
+ });
+
+ test('lists the empty string', () async {
+ expect(await list('foo/bar**'), equals([p.join('foo', 'bar')]));
+ });
+
+ test('lists recursively', () async {
+ expect(
+ await list('foo/**'),
+ unorderedEquals([
+ p.join('foo', 'bar'),
+ p.join('foo', 'baz'),
+ p.join('foo', 'baz', 'qux'),
+ p.join('foo', 'baz', 'bang')
+ ]));
+ });
+
+ test('combines with literals', () async {
+ expect(
+ await list('foo/ba**'),
+ unorderedEquals([
+ p.join('foo', 'bar'),
+ p.join('foo', 'baz'),
+ p.join('foo', 'baz', 'qux'),
+ p.join('foo', 'baz', 'bang')
+ ]));
+ });
+
+ test('lists recursively in the middle of a glob', () async {
+ await d.dir('deep', [
+ d.dir('a', [
+ d.dir('b', [
+ d.dir('c', [d.file('d'), d.file('long-file')]),
+ d.dir('long-dir', [d.file('x')])
+ ])
+ ])
+ ]).create();
+
+ expect(
+ await list('deep/**/?/?'),
+ unorderedEquals([
+ p.join('deep', 'a', 'b', 'c'),
+ p.join('deep', 'a', 'b', 'c', 'd')
+ ]));
+ });
+ });
+
+ group('any char', () {
+ test('matches a character', () async {
+ expect(await list('foo/ba?'),
+ unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
+ });
+
+ test("doesn't match a separator", () async {
+ expect(await list('foo?bar'), isEmpty);
+ });
+ });
+
+ group('range', () {
+ test('matches a range of characters', () async {
+ expect(await list('foo/ba[a-z]'),
+ unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
+ });
+
+ test('matches a specific list of characters', () async {
+ expect(await list('foo/ba[rz]'),
+ unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
+ });
+
+ test("doesn't match outside its range", () async {
+ expect(
+ await list('foo/ba[a-x]'), unorderedEquals([p.join('foo', 'bar')]));
+ });
+
+ test("doesn't match outside its specific list", () async {
+ expect(
+ await list('foo/ba[rx]'), unorderedEquals([p.join('foo', 'bar')]));
+ });
+ });
+
+ test("the same file shouldn't be non-recursively listed multiple times",
+ () async {
+ await d.dir('multi', [
+ d.dir('start-end', [d.file('file')])
+ ]).create();
+
+ expect(await list('multi/{start-*/f*,*-end/*e}'),
+ equals([p.join('multi', 'start-end', 'file')]));
+ });
+
+ test("the same file shouldn't be recursively listed multiple times",
+ () async {
+ await d.dir('multi', [
+ d.dir('a', [
+ d.dir('b', [
+ d.file('file'),
+ d.dir('c', [d.file('file')])
+ ]),
+ d.dir('x', [
+ d.dir('y', [d.file('file')])
+ ])
+ ])
+ ]).create();
+
+ expect(
+ await list('multi/{*/*/*/file,a/**/file}'),
+ unorderedEquals([
+ p.join('multi', 'a', 'b', 'file'),
+ p.join('multi', 'a', 'b', 'c', 'file'),
+ p.join('multi', 'a', 'x', 'y', 'file')
+ ]));
+ });
+
+ group('with symlinks', () {
+ setUp(() async {
+ await Link(p.join(d.sandbox, 'dir', 'link'))
+ .create(p.join(d.sandbox, 'foo', 'baz'), recursive: true);
+ });
+
+ test('follows symlinks by default', () async {
+ expect(
+ await list('dir/**'),
+ unorderedEquals([
+ p.join('dir', 'link'),
+ p.join('dir', 'link', 'bang'),
+ p.join('dir', 'link', 'qux')
+ ]));
+ });
+
+ test("doesn't follow symlinks with followLinks: false", () async {
+ expect(await list('dir/**', followLinks: false),
+ equals([p.join('dir', 'link')]));
+ });
+
+ test("shouldn't crash on broken symlinks", () async {
+ await Directory(p.join(d.sandbox, 'foo')).delete(recursive: true);
+
+ expect(await list('dir/**'), equals([p.join('dir', 'link')]));
+ });
+ });
+
+ test('always lists recursively with recursive: true', () async {
+ expect(
+ await list('foo', recursive: true),
+ unorderedEquals([
+ 'foo',
+ p.join('foo', 'bar'),
+ p.join('foo', 'baz'),
+ p.join('foo', 'baz', 'qux'),
+ p.join('foo', 'baz', 'bang')
+ ]));
+ });
+
+ test('lists an absolute glob', () async {
+ var pattern =
+ separatorToForwardSlash(p.absolute(p.join(d.sandbox, 'foo/baz/**')));
+
+ var result = await list(pattern);
+
+ expect(
+ result,
+ unorderedEquals(
+ [p.join('foo', 'baz', 'bang'), p.join('foo', 'baz', 'qux')]));
+ });
+
+ // Regression test for #4.
+ test('lists an absolute case-insensitive glob', () async {
+ var pattern =
+ separatorToForwardSlash(p.absolute(p.join(d.sandbox, 'foo/Baz/**')));
+
+ expect(
+ await list(pattern, caseSensitive: false),
+ unorderedEquals(
+ [p.join('foo', 'baz', 'bang'), p.join('foo', 'baz', 'qux')]));
+ });
+
+ test('lists a subdirectory that sometimes exists', () async {
+ await d.dir('top', [
+ d.dir('dir1', [
+ d.dir('subdir', [d.file('file')])
+ ]),
+ d.dir('dir2', [])
+ ]).create();
+
+ expect(await list('top/*/subdir/**'),
+ equals([p.join('top', 'dir1', 'subdir', 'file')]));
+ });
+
+ group('when case-insensitive', () {
+ test('lists literals case-insensitively', () async {
+ expect(await list('foo/baz/qux', caseSensitive: false),
+ equals([p.join('foo', 'baz', 'qux')]));
+ expect(await list('foo/BAZ/qux', caseSensitive: false),
+ equals([p.join('foo', 'baz', 'qux')]));
+ });
+
+ test('lists ranges case-insensitively', () async {
+ expect(await list('foo/[bx][a-z]z/qux', caseSensitive: false),
+ equals([p.join('foo', 'baz', 'qux')]));
+ expect(await list('foo/[BX][A-Z]z/qux', caseSensitive: false),
+ equals([p.join('foo', 'baz', 'qux')]));
+ });
+
+ test('options preserve case-insensitivity', () async {
+ expect(await list('foo/{bar,baz}/qux', caseSensitive: false),
+ equals([p.join('foo', 'baz', 'qux')]));
+ expect(await list('foo/{BAR,BAZ}/qux', caseSensitive: false),
+ equals([p.join('foo', 'baz', 'qux')]));
+ });
+ });
+ });
+}
+
+typedef ListFn = FutureOr<List<String>> Function(String glob,
+ {bool recursive, bool followLinks, bool? caseSensitive});
+
+/// Runs [callback] in two groups with two values of [ListFn]: one that uses
+/// `Glob.list`, one that uses `Glob.listSync`.
+void syncAndAsync(FutureOr<void> Function(ListFn) callback) {
+ group('async', () {
+ callback((pattern, {recursive = false, followLinks = true, caseSensitive}) {
+ var glob =
+ Glob(pattern, recursive: recursive, caseSensitive: caseSensitive);
+
+ return glob
+ .list(root: d.sandbox, followLinks: followLinks)
+ .map((entity) => p.relative(entity.path, from: d.sandbox))
+ .toList();
+ });
+ });
+
+ group('sync', () {
+ callback((pattern, {recursive = false, followLinks = true, caseSensitive}) {
+ var glob =
+ Glob(pattern, recursive: recursive, caseSensitive: caseSensitive);
+
+ return glob
+ .listSync(root: d.sandbox, followLinks: followLinks)
+ .map((entity) => p.relative(entity.path, from: d.sandbox))
+ .toList();
+ });
+ });
+}
diff --git a/pkgs/glob/test/match_test.dart b/pkgs/glob/test/match_test.dart
new file mode 100644
index 0000000..925d41f
--- /dev/null
+++ b/pkgs/glob/test/match_test.dart
@@ -0,0 +1,341 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:glob/glob.dart';
+import 'package:glob/src/utils.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+const _rawAsciiWithoutSlash = "\t\n\r !\"#\$%&'()*+`-.0123456789:;<=>?@ABCDEF"
+ 'GHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~';
+
+// URL-encode the path for a URL context.
+final asciiWithoutSlash = p.style == p.Style.url
+ ? Uri.encodeFull(_rawAsciiWithoutSlash)
+ : _rawAsciiWithoutSlash;
+
+void main() {
+ test('literals match exactly', () {
+ expect('foo', contains(Glob('foo')));
+ expect('foo/bar', contains(Glob('foo/bar')));
+ expect('foo*', contains(Glob(r'foo\*')));
+ });
+
+ test('backslashes match nothing on Windows', () {
+ expect(r'foo\bar', isNot(contains(Glob(r'foo\\bar', context: p.windows))));
+ });
+
+ group('star', () {
+ test('matches non-separator characters', () {
+ var glob = Glob('*');
+ expect(asciiWithoutSlash, contains(glob));
+ });
+
+ test('matches the empty string', () {
+ expect('foo', contains(Glob('foo*')));
+ expect('', contains(Glob('*')));
+ });
+
+ test("doesn't match separators", () {
+ var glob = Glob('*');
+ expect('foo/bar', isNot(contains(glob)));
+ });
+ });
+
+ group('double star', () {
+ test('matches non-separator characters', () {
+ var glob = Glob('**');
+ expect(asciiWithoutSlash, contains(glob));
+ });
+
+ test('matches the empty string', () {
+ var glob = Glob('foo**');
+ expect('foo', contains(glob));
+ });
+
+ test('matches any level of nesting', () {
+ var glob = Glob('**');
+ expect('a', contains(glob));
+ expect('a/b/c/d/e/f', contains(glob));
+ });
+
+ test("doesn't match unresolved dot dots", () {
+ expect('../foo/bar', isNot(contains(Glob('**'))));
+ });
+
+ test('matches entities containing dot dots', () {
+ expect('..foo/bar', contains(Glob('**')));
+ expect('foo../bar', contains(Glob('**')));
+ expect('foo/..bar', contains(Glob('**')));
+ expect('foo/bar..', contains(Glob('**')));
+ });
+ });
+
+ group('any char', () {
+ test('matches any non-separator character', () {
+ var glob = Glob('foo?');
+ for (var char in _rawAsciiWithoutSlash.split('')) {
+ if (p.style == p.Style.url) char = Uri.encodeFull(char);
+ expect('foo$char', contains(glob));
+ }
+ });
+
+ test("doesn't match a separator", () {
+ expect('foo/bar', isNot(contains(Glob('foo?bar'))));
+ });
+ });
+
+ group('range', () {
+ test('can match individual characters', () {
+ var glob = Glob('foo[a<.*]');
+ expect('fooa', contains(glob));
+ expect('foo<', contains(glob));
+ expect('foo.', contains(glob));
+ expect('foo*', contains(glob));
+ expect('foob', isNot(contains(glob)));
+ expect('foo>', isNot(contains(glob)));
+ });
+
+ test('can match a range of characters', () {
+ var glob = Glob('foo[a-z]');
+ expect('fooa', contains(glob));
+ expect('foon', contains(glob));
+ expect('fooz', contains(glob));
+ expect('foo`', isNot(contains(glob)));
+ expect('foo{', isNot(contains(glob)));
+ });
+
+ test('can match multiple ranges of characters', () {
+ var glob = Glob('foo[a-zA-Z]');
+ expect('fooa', contains(glob));
+ expect('foon', contains(glob));
+ expect('fooz', contains(glob));
+ expect('fooA', contains(glob));
+ expect('fooN', contains(glob));
+ expect('fooZ', contains(glob));
+ expect('foo?', isNot(contains(glob)));
+ expect('foo{', isNot(contains(glob)));
+ });
+
+ test('can match individual characters and ranges of characters', () {
+ var glob = Glob('foo[a-z_A-Z]');
+ expect('fooa', contains(glob));
+ expect('foon', contains(glob));
+ expect('fooz', contains(glob));
+ expect('fooA', contains(glob));
+ expect('fooN', contains(glob));
+ expect('fooZ', contains(glob));
+ expect('foo_', contains(glob));
+ expect('foo?', isNot(contains(glob)));
+ expect('foo{', isNot(contains(glob)));
+ });
+
+ test('can be negated', () {
+ var glob = Glob('foo[^a<.*]');
+ expect('fooa', isNot(contains(glob)));
+ expect('foo<', isNot(contains(glob)));
+ expect('foo.', isNot(contains(glob)));
+ expect('foo*', isNot(contains(glob)));
+ expect('foob', contains(glob));
+ expect('foo>', contains(glob));
+ });
+
+ test('never matches separators', () {
+ // "\t-~" contains "/".
+ expect('foo/bar', isNot(contains(Glob('foo[\t-~]bar'))));
+ expect('foo/bar', isNot(contains(Glob('foo[^a]bar'))));
+ });
+
+ test('allows dangling -', () {
+ expect('-', contains(Glob(r'[-]')));
+
+ var glob = Glob(r'[a-]');
+ expect('-', contains(glob));
+ expect('a', contains(glob));
+
+ glob = Glob(r'[-b]');
+ expect('-', contains(glob));
+ expect('b', contains(glob));
+ });
+
+ test('allows multiple -s', () {
+ expect('-', contains(Glob(r'[--]')));
+ expect('-', contains(Glob(r'[---]')));
+
+ var glob = Glob(r'[--a]');
+ expect('-', contains(glob));
+ expect('a', contains(glob));
+ });
+
+ test('allows negated /', () {
+ expect('foo-bar', contains(Glob('foo[^/]bar')));
+ });
+
+ test("doesn't choke on RegExp-active characters", () {
+ var glob = Glob(r'foo[\]].*');
+ expect('foobar', isNot(contains(glob)));
+ expect('foo].*', contains(glob));
+ });
+ });
+
+ group('options', () {
+ test('match if any of the options match', () {
+ var glob = Glob('foo/{bar,baz,bang}');
+ expect('foo/bar', contains(glob));
+ expect('foo/baz', contains(glob));
+ expect('foo/bang', contains(glob));
+ expect('foo/qux', isNot(contains(glob)));
+ });
+
+ test('can contain nested operators', () {
+ var glob = Glob('foo/{ba?,*az,ban{g,f}}');
+ expect('foo/bar', contains(glob));
+ expect('foo/baz', contains(glob));
+ expect('foo/bang', contains(glob));
+ expect('foo/qux', isNot(contains(glob)));
+ });
+
+ test('can conditionally match separators', () {
+ var glob = Glob('foo/{bar,baz/bang}');
+ expect('foo/bar', contains(glob));
+ expect('foo/baz/bang', contains(glob));
+ expect('foo/baz', isNot(contains(glob)));
+ expect('foo/bar/bang', isNot(contains(glob)));
+ });
+ });
+
+ group('normalization', () {
+ test('extra slashes are ignored', () {
+ expect('foo//bar', contains(Glob('foo/bar')));
+ expect('foo/', contains(Glob('*')));
+ });
+
+ test('dot directories are ignored', () {
+ expect('foo/./bar', contains(Glob('foo/bar')));
+ expect('foo/.', contains(Glob('foo')));
+ });
+
+ test('dot dot directories are resolved', () {
+ expect('foo/../bar', contains(Glob('bar')));
+ expect('../foo/bar', contains(Glob('../foo/bar')));
+ expect('foo/../../bar', contains(Glob('../bar')));
+ });
+
+ test('Windows separators are converted in a Windows context', () {
+ expect(r'foo\bar', contains(Glob('foo/bar', context: p.windows)));
+ expect(r'foo\bar/baz', contains(Glob('foo/bar/baz', context: p.windows)));
+ });
+ });
+
+ test('an absolute path can be matched by a relative glob', () {
+ var path = p.absolute('foo/bar');
+ expect(path, contains(Glob('foo/bar')));
+ });
+
+ test('a relative path can be matched by an absolute glob', () {
+ var pattern = separatorToForwardSlash(p.absolute('foo/bar'));
+ expect('foo/bar', contains(Glob(pattern)));
+ }, testOn: 'vm');
+
+ group('with recursive: true', () {
+ var glob = Glob('foo/bar', recursive: true);
+
+ test('still matches basic files', () {
+ expect('foo/bar', contains(glob));
+ });
+
+ test('matches subfiles', () {
+ expect('foo/bar/baz', contains(glob));
+ expect('foo/bar/baz/bang', contains(glob));
+ });
+
+ test("doesn't match suffixes", () {
+ expect('foo/barbaz', isNot(contains(glob)));
+ expect('foo/barbaz/bang', isNot(contains(glob)));
+ });
+ });
+
+ test('absolute POSIX paths', () {
+ expect('/foo/bar', contains(Glob('/foo/bar', context: p.posix)));
+ expect('/foo/bar', isNot(contains(Glob('**', context: p.posix))));
+ expect('/foo/bar', contains(Glob('/**', context: p.posix)));
+ });
+
+ test('absolute Windows paths', () {
+ expect(r'C:\foo\bar', contains(Glob('C:/foo/bar', context: p.windows)));
+ expect(r'C:\foo\bar', isNot(contains(Glob('**', context: p.windows))));
+ expect(r'C:\foo\bar', contains(Glob('C:/**', context: p.windows)));
+
+ expect(
+ r'\\foo\bar\baz', contains(Glob('//foo/bar/baz', context: p.windows)));
+ expect(r'\\foo\bar\baz', isNot(contains(Glob('**', context: p.windows))));
+ expect(r'\\foo\bar\baz', contains(Glob('//**', context: p.windows)));
+ expect(r'\\foo\bar\baz', contains(Glob('//foo/**', context: p.windows)));
+ });
+
+ test('absolute URL paths', () {
+ expect(r'http://foo.com/bar',
+ contains(Glob('http://foo.com/bar', context: p.url)));
+ expect(r'http://foo.com/bar', isNot(contains(Glob('**', context: p.url))));
+ expect(r'http://foo.com/bar', contains(Glob('http://**', context: p.url)));
+ expect(r'http://foo.com/bar',
+ contains(Glob('http://foo.com/**', context: p.url)));
+
+ expect('/foo/bar', contains(Glob('/foo/bar', context: p.url)));
+ expect('/foo/bar', isNot(contains(Glob('**', context: p.url))));
+ expect('/foo/bar', contains(Glob('/**', context: p.url)));
+ });
+
+ group('when case-sensitive', () {
+ test('literals match case-sensitively', () {
+ expect('foo', contains(Glob('foo', caseSensitive: true)));
+ expect('FOO', isNot(contains(Glob('foo', caseSensitive: true))));
+ expect('foo', isNot(contains(Glob('FOO', caseSensitive: true))));
+ });
+
+ test('ranges match case-sensitively', () {
+ expect('foo', contains(Glob('[fx][a-z]o', caseSensitive: true)));
+ expect('FOO', isNot(contains(Glob('[fx][a-z]o', caseSensitive: true))));
+ expect('foo', isNot(contains(Glob('[FX][A-Z]O', caseSensitive: true))));
+ });
+
+ test('sequences preserve case-sensitivity', () {
+ expect('foo/bar', contains(Glob('foo/bar', caseSensitive: true)));
+ expect('FOO/BAR', isNot(contains(Glob('foo/bar', caseSensitive: true))));
+ expect('foo/bar', isNot(contains(Glob('FOO/BAR', caseSensitive: true))));
+ });
+
+ test('options preserve case-sensitivity', () {
+ expect('foo', contains(Glob('{foo,bar}', caseSensitive: true)));
+ expect('FOO', isNot(contains(Glob('{foo,bar}', caseSensitive: true))));
+ expect('foo', isNot(contains(Glob('{FOO,BAR}', caseSensitive: true))));
+ });
+ });
+
+ group('when case-insensitive', () {
+ test('literals match case-insensitively', () {
+ expect('foo', contains(Glob('foo', caseSensitive: false)));
+ expect('FOO', contains(Glob('foo', caseSensitive: false)));
+ expect('foo', contains(Glob('FOO', caseSensitive: false)));
+ });
+
+ test('ranges match case-insensitively', () {
+ expect('foo', contains(Glob('[fx][a-z]o', caseSensitive: false)));
+ expect('FOO', contains(Glob('[fx][a-z]o', caseSensitive: false)));
+ expect('foo', contains(Glob('[FX][A-Z]O', caseSensitive: false)));
+ });
+
+ test('sequences preserve case-insensitivity', () {
+ expect('foo/bar', contains(Glob('foo/bar', caseSensitive: false)));
+ expect('FOO/BAR', contains(Glob('foo/bar', caseSensitive: false)));
+ expect('foo/bar', contains(Glob('FOO/BAR', caseSensitive: false)));
+ });
+
+ test('options preserve case-insensitivity', () {
+ expect('foo', contains(Glob('{foo,bar}', caseSensitive: false)));
+ expect('FOO', contains(Glob('{foo,bar}', caseSensitive: false)));
+ expect('foo', contains(Glob('{FOO,BAR}', caseSensitive: false)));
+ });
+ });
+}
diff --git a/pkgs/glob/test/parse_test.dart b/pkgs/glob/test/parse_test.dart
new file mode 100644
index 0000000..5a075e7
--- /dev/null
+++ b/pkgs/glob/test/parse_test.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:glob/glob.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+ test('supports backslash-escaped characters', () {
+ expect(r'*[]{,}?()', contains(Glob(r'\*\[\]\{\,\}\?\(\)')));
+ if (p.style != p.Style.windows) {
+ expect(r'foo\bar', contains(Glob(r'foo\\bar')));
+ }
+ });
+
+ test('disallows an empty glob', () {
+ expect(() => Glob(''), throwsFormatException);
+ });
+
+ group('range', () {
+ test('supports either ^ or ! for negated ranges', () {
+ var bang = Glob('fo[!a-z]');
+ expect('foo', isNot(contains(bang)));
+ expect('fo2', contains(bang));
+
+ var caret = Glob('fo[^a-z]');
+ expect('foo', isNot(contains(caret)));
+ expect('fo2', contains(caret));
+ });
+
+ test('supports backslash-escaped characters', () {
+ var glob = Glob(r'fo[\*\--\]]');
+ expect('fo]', contains(glob));
+ expect('fo-', contains(glob));
+ expect('fo*', contains(glob));
+ });
+
+ test('disallows inverted ranges', () {
+ expect(() => Glob(r'[z-a]'), throwsFormatException);
+ });
+
+ test('disallows empty ranges', () {
+ expect(() => Glob(r'[]'), throwsFormatException);
+ });
+
+ test('disallows unclosed ranges', () {
+ expect(() => Glob(r'[abc'), throwsFormatException);
+ expect(() => Glob(r'[-'), throwsFormatException);
+ });
+
+ test('disallows dangling ]', () {
+ expect(() => Glob(r'abc]'), throwsFormatException);
+ });
+
+ test('disallows explicit /', () {
+ expect(() => Glob(r'[/]'), throwsFormatException);
+ expect(() => Glob(r'[ -/]'), throwsFormatException);
+ expect(() => Glob(r'[/-~]'), throwsFormatException);
+ });
+ });
+
+ group('options', () {
+ test('allows empty branches', () {
+ var glob = Glob('foo{,bar}');
+ expect('foo', contains(glob));
+ expect('foobar', contains(glob));
+ });
+
+ test('disallows empty options', () {
+ expect(() => Glob('{}'), throwsFormatException);
+ });
+
+ test('disallows single options', () {
+ expect(() => Glob('{foo}'), throwsFormatException);
+ });
+
+ test('disallows unclosed options', () {
+ expect(() => Glob('{foo,bar'), throwsFormatException);
+ expect(() => Glob('{foo,'), throwsFormatException);
+ });
+
+ test('disallows dangling }', () {
+ expect(() => Glob('foo}'), throwsFormatException);
+ });
+
+ test('disallows dangling ] in options', () {
+ expect(() => Glob(r'{abc]}'), throwsFormatException);
+ });
+ });
+
+ test('disallows unescaped parens', () {
+ expect(() => Glob('foo(bar'), throwsFormatException);
+ expect(() => Glob('foo)bar'), throwsFormatException);
+ });
+}