Create a glob package.
BUG=17093
R=rnystrom@google.com
Review URL: https://codereview.chromium.org//506993004
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@39784 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/glob/LICENSE b/pkgs/glob/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/pkgs/glob/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+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 Inc. 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..5091a1e
--- /dev/null
+++ b/pkgs/glob/README.md
@@ -0,0 +1,124 @@
+`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 `new 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 = new 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';
+
+final dartFile = new 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:/*`.
+
+### 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/lib/glob.dart b/pkgs/glob/lib/glob.dart
new file mode 100644
index 0000000..1c7efda
--- /dev/null
+++ b/pkgs/glob/lib/glob.dart
@@ -0,0 +1,140 @@
+// 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.
+
+library glob;
+
+import 'package:path/path.dart' as p;
+
+import 'src/ast.dart';
+import 'src/parser.dart';
+import 'src/utils.dart';
+
+/// Regular expression used to quote globs.
+final _quoteRegExp = new RegExp(r'[*{[?\\}\],\-()]');
+
+// TODO(nweiz): Add [list] and [listSync] methods.
+/// 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 [new Glob];
+/// 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;
+
+ /// The parsed AST of the glob.
+ final AstNode _ast;
+
+ /// Whether [context]'s current directory is absolute.
+ bool get _contextIsAbsolute {
+ if (_contextIsAbsoluteCache == null) {
+ _contextIsAbsoluteCache = context.isAbsolute(context.current);
+ }
+ return _contextIsAbsoluteCache;
+ }
+ bool _contextIsAbsoluteCache;
+
+ /// Whether [pattern] could match absolute paths.
+ bool get _patternCanMatchAbsolute {
+ if (_patternCanMatchAbsoluteCache == null) {
+ _patternCanMatchAbsoluteCache = _ast.canMatchAbsolute;
+ }
+ return _patternCanMatchAbsoluteCache;
+ }
+ bool _patternCanMatchAbsoluteCache;
+
+ /// Whether [pattern] could match relative paths.
+ bool get _patternCanMatchRelative {
+ if (_patternCanMatchRelativeCache == null) {
+ _patternCanMatchRelativeCache = _ast.canMatchRelative;
+ }
+ return _patternCanMatchRelativeCache;
+ }
+ 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 will match and list not only the files
+ /// and directories it explicitly lists, but anything beneath those as well.
+ Glob(String pattern, {p.Context context, bool recursive: false})
+ : this._(
+ pattern,
+ context == null ? p.context : context,
+ recursive);
+
+ // Internal constructor used to fake local variables for [context] and [ast].
+ Glob._(String pattern, p.Context context, bool recursive)
+ : pattern = pattern,
+ context = context,
+ recursive = recursive,
+ _ast = new Parser(pattern + (recursive ? "{,/**}" : ""), context)
+ .parse();
+
+ /// Returns whether this glob matches [path].
+ bool matches(String path) => matchAsPrefix(path) != null;
+
+ 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(absolutePath))) {
+ return new GlobMatch(path, this);
+ }
+ }
+
+ if (_patternCanMatchRelative) {
+ var relativePath = context.relative(path);
+ if (_ast.matches(_toPosixPath(relativePath))) {
+ return new GlobMatch(path, this);
+ }
+ }
+
+ return null;
+ }
+
+ /// Returns [path] converted to the POSIX format that globs match against.
+ String _toPosixPath(String path) {
+ if (context.style == p.Style.windows) return path.replaceAll('\\', '/');
+ if (context.style == p.Style.url) return Uri.decodeFull(path);
+ return path;
+ }
+
+ Iterable<Match> allMatches(String path, [int start = 0]) {
+ var match = matchAsPrefix(path, start);
+ return match == null ? [] : [match];
+ }
+
+ String toString() => pattern;
+}
diff --git a/pkgs/glob/lib/src/ast.dart b/pkgs/glob/lib/src/ast.dart
new file mode 100644
index 0000000..5e1da1a
--- /dev/null
+++ b/pkgs/glob/lib/src/ast.dart
@@ -0,0 +1,204 @@
+// 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.
+
+library glob.ast;
+
+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 glob could match an absolute path.
+ ///
+ /// Either this or [canMatchRelative] or both will be true.
+ final bool canMatchAbsolute = false;
+
+ /// Whether this glob could match a relative path.
+ ///
+ /// Either this or [canMatchRelative] or both will be true.
+ final bool canMatchRelative = true;
+
+ /// Returns whether this glob matches [string].
+ bool matches(String string) {
+ if (_regExp == null) _regExp = new RegExp('^${_toRegExp()}\$');
+ return _regExp.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;
+
+ bool get canMatchAbsolute => nodes.first.canMatchAbsolute;
+ bool get canMatchRelative => nodes.first.canMatchRelative;
+
+ SequenceNode(Iterable<AstNode> nodes)
+ : nodes = nodes.toList();
+
+ String _toRegExp() => nodes.map((node) => node._toRegExp()).join();
+
+ String toString() => nodes.join();
+}
+
+/// A node matching zero or more non-separator characters.
+class StarNode extends AstNode {
+ StarNode();
+
+ String _toRegExp() => '[^/]*';
+
+ 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);
+
+ 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 = new 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();
+ }
+
+ String toString() => '**';
+}
+
+/// A node matching a single non-separator character.
+class AnyCharNode extends AstNode {
+ AnyCharNode();
+
+ String _toRegExp() => '[^/]';
+
+ 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, {this.negated})
+ : ranges = ranges.toSet();
+
+ String _toRegExp() {
+ var buffer = new 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 = new String.fromCharCodes([range.min]);
+ buffer.write(regExpQuote(start));
+ if (range.isSingleton) continue;
+ buffer.write('-');
+ buffer.write(regExpQuote(new String.fromCharCodes([range.max])));
+ }
+
+ buffer.write(']');
+ return buffer.toString();
+ }
+
+ String toString() {
+ var buffer = new 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;
+
+ bool get canMatchAbsolute => options.any((node) => node.canMatchAbsolute);
+ bool get canMatchRelative => options.any((node) => node.canMatchRelative);
+
+ OptionsNode(Iterable<SequenceNode> options)
+ : options = options.toList();
+
+ String _toRegExp() =>
+ '(?:${options.map((option) => option._toRegExp()).join("|")})';
+
+ 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;
+
+ bool get canMatchAbsolute {
+ var nativeText = _context.style == p.Style.windows ?
+ text.replaceAll('/', '\\') : text;
+ return _context.isAbsolute(nativeText);
+ }
+
+ bool get canMatchRelative => !canMatchAbsolute;
+
+ LiteralNode(this.text, this._context);
+
+ String _toRegExp() => regExpQuote(text);
+
+ String toString() => text;
+}
diff --git a/pkgs/glob/lib/src/parser.dart b/pkgs/glob/lib/src/parser.dart
new file mode 100644
index 0000000..5dac146
--- /dev/null
+++ b/pkgs/glob/lib/src/parser.dart
@@ -0,0 +1,173 @@
+// 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.
+
+library glob.single_component;
+
+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;
+
+ Parser(String component, this._context)
+ : _scanner = new StringScanner(component);
+
+ /// 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 = [];
+
+ 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 new SequenceNode(nodes);
+ }
+
+ /// 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('*') ? new DoubleStarNode(_context) : new StarNode();
+ }
+
+ /// Tries to parse an [AnyCharNode].
+ ///
+ /// Returns `null` if there's not one to parse.
+ AstNode _parseAnyChar() {
+ if (!_scanner.scan('?')) return null;
+ return new AnyCharNode();
+ }
+
+ /// 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('^');
+
+ 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 = [];
+ 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(new Range.singleton(char));
+ ranges.add(new 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(new Range(char, end));
+ } else {
+ ranges.add(new Range.singleton(char));
+ }
+ }
+
+ return new RangeNode(ranges, negated: negated);
+ }
+
+ /// 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 = [];
+ do {
+ options.add(_parseSequence(inOptions: true));
+ } while (_scanner.scan(','));
+
+ // Don't allow single-option blocks.
+ if (options.length == 1) _scanner.expect(',');
+ _scanner.expect('}');
+
+ return new OptionsNode(options);
+ }
+
+ /// 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 = new RegExp(
+ inOptions ? r'[^*{[?\\}\],()]*' : r'[^*{[?\\}\]()]*');
+
+ _scanner.scan(regExp);
+ var buffer = new 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 new LiteralNode(buffer.toString(), _context);
+ }
+}
diff --git a/pkgs/glob/lib/src/utils.dart b/pkgs/glob/lib/src/utils.dart
new file mode 100644
index 0000000..12952e8
--- /dev/null
+++ b/pkgs/glob/lib/src/utils.dart
@@ -0,0 +1,55 @@
+// 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.
+
+library glob.utils;
+
+/// 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;
+}
+
+/// An implementation of [Match] constructed by [Glob]s.
+class GlobMatch implements Match {
+ final String input;
+ final Pattern pattern;
+ final int start = 0;
+
+ int get end => input.length;
+ int get groupCount => 0;
+
+ GlobMatch(this.input, this.pattern);
+
+ String operator [](int group) => this.group(group);
+
+ String group(int group) {
+ if (group != 0) throw new RangeError.range(group, 0, 0);
+ return input;
+ }
+
+ List<String> groups(List<int> groupIndices) =>
+ groupIndices.map((index) => group(index)).toList();
+}
+
+final _quote = new RegExp(r"[+*?{}|[\]\\().^$-]");
+
+/// Returns [contents] with characters that are meaningful in regular
+/// expressions backslash-escaped.
+String regExpQuote(String contents) =>
+ contents.replaceAllMapped(_quote, (char) => "\\${char[0]}");
diff --git a/pkgs/glob/pubspec.yaml b/pkgs/glob/pubspec.yaml
new file mode 100644
index 0000000..2a5e9a8
--- /dev/null
+++ b/pkgs/glob/pubspec.yaml
@@ -0,0 +1,8 @@
+name: glob
+version: 1.0.0-dev
+description: Bash-style filename globbing.
+dependencies:
+ path: ">=1.0.0 <2.0.0"
+ string_scanner: ">=0.1.0 <0.2.0"
+dev_dependencies:
+ unittest: ">=0.11.0 <0.12.0"
diff --git a/pkgs/glob/test/glob_test.dart b/pkgs/glob/test/glob_test.dart
new file mode 100644
index 0000000..34dbb65
--- /dev/null
+++ b/pkgs/glob/test/glob_test.dart
@@ -0,0 +1,94 @@
+// 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:unittest/unittest.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 = new Glob("foo*");
+ expect(glob.matches("foobar"), isTrue);
+ expect(glob.matches("baz"), isFalse);
+ });
+
+ test("only matches the entire path", () {
+ var glob = new 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 = new Glob("foo*");
+ expect(glob.matchAsPrefix("foobar"), new isInstanceOf<Match>());
+ expect(glob.matchAsPrefix("baz"), isNull);
+ });
+
+ test("returns null for start > 0", () {
+ var glob = new Glob("*");
+ expect(glob.matchAsPrefix("foobar", 1), isNull);
+ });
+ });
+
+ group("Glob.allMatches()", () {
+ test("returns a single match if the path matches the glob", () {
+ var matches = new Glob("foo*").allMatches("foobar");
+ expect(matches, hasLength(1));
+ expect(matches.first, new isInstanceOf<Match>());
+ });
+
+ test("returns an empty list if the path doesn't match the glob", () {
+ expect(new Glob("foo*").allMatches("baz"), isEmpty);
+ });
+
+ test("returns no matches for start > 0", () {
+ var glob = new Glob("*");
+ expect(glob.allMatches("foobar", 1), isEmpty);
+ });
+ });
+
+ group("GlobMatch", () {
+ var glob = new 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);
+ });
+ });
+}
diff --git a/pkgs/glob/test/match_test.dart b/pkgs/glob/test/match_test.dart
new file mode 100644
index 0000000..eec050c
--- /dev/null
+++ b/pkgs/glob/test/match_test.dart
@@ -0,0 +1,286 @@
+// 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:io';
+
+import 'package:glob/glob.dart';
+import 'package:path/path.dart' as p;
+import 'package:unittest/unittest.dart';
+
+const ASCII_WITHOUT_SLASH = "\t\n\r !\"#\$%&'()*+`-.0123456789:;<=>?@ABCDEFGHIJ"
+ "KLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
+
+void main() {
+ test("literals match exactly", () {
+ expect("foo", contains(new Glob("foo")));
+ expect("foo/bar", contains(new Glob("foo/bar")));
+ expect("foo*", contains(new Glob(r"foo\*")));
+ });
+
+ group("star", () {
+ test("matches non-separator characters", () {
+ var glob = new Glob("*");
+ expect(ASCII_WITHOUT_SLASH, contains(glob));
+ });
+
+ test("matches the empty string", () {
+ expect("foo", contains(new Glob("foo*")));
+ expect("", contains(new Glob("*")));
+ });
+
+ test("doesn't match separators", () {
+ var glob = new Glob("*");
+ expect("foo/bar", isNot(contains(glob)));
+ });
+ });
+
+ group("double star", () {
+ test("matches non-separator characters", () {
+ var glob = new Glob("**");
+ expect(ASCII_WITHOUT_SLASH, contains(glob));
+ });
+
+ test("matches the empty string", () {
+ var glob = new Glob("foo**");
+ expect("foo", contains(glob));
+ });
+
+ test("matches any level of nesting", () {
+ var glob = new 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(new Glob("**"))));
+ });
+
+ test("matches entities containing dot dots", () {
+ expect("..foo/bar", contains(new Glob("**")));
+ expect("foo../bar", contains(new Glob("**")));
+ expect("foo/..bar", contains(new Glob("**")));
+ expect("foo/bar..", contains(new Glob("**")));
+ });
+ });
+
+ group("any char", () {
+ test("matches any non-separator character", () {
+ var glob = new Glob("foo?");
+ for (var char in ASCII_WITHOUT_SLASH.split('')) {
+ expect("foo$char", contains(glob));
+ }
+ });
+
+ test("doesn't match a separator", () {
+ expect("foo/bar", isNot(contains(new Glob("foo?bar"))));
+ });
+ });
+
+ group("range", () {
+ test("can match individual characters", () {
+ var glob = new 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 = new 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 = new 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 = new 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 = new 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(new Glob("foo[\t-~]bar"))));
+ expect("foo/bar", isNot(contains(new Glob("foo[^a]bar"))));
+ });
+
+ test("allows dangling -", () {
+ expect("-", contains(new Glob(r"[-]")));
+
+ var glob = new Glob(r"[a-]");
+ expect("-", contains(glob));
+ expect("a", contains(glob));
+
+ glob = new Glob(r"[-b]");
+ expect("-", contains(glob));
+ expect("b", contains(glob));
+ });
+
+ test("allows multiple -s", () {
+ expect("-", contains(new Glob(r"[--]")));
+ expect("-", contains(new Glob(r"[---]")));
+
+ var glob = new Glob(r"[--a]");
+ expect("-", contains(glob));
+ expect("a", contains(glob));
+ });
+
+ test("allows negated /", () {
+ expect("foo-bar", contains(new Glob("foo[^/]bar")));
+ });
+
+ test("doesn't choke on RegExp-active characters", () {
+ var glob = new Glob(r"foo[\]].*");
+ expect("foobar", isNot(contains(glob)));
+ expect("foo].*", contains(glob));
+ });
+ });
+
+ group("options", () {
+ test("match if any of the options match", () {
+ var glob = new 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 = new 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 = new 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(new Glob("foo/bar")));
+ expect("foo/", contains(new Glob("*")));
+ });
+
+ test("dot directories are ignored", () {
+ expect("foo/./bar", contains(new Glob("foo/bar")));
+ expect("foo/.", contains(new Glob("foo")));
+ });
+
+ test("dot dot directories are resolved", () {
+ expect("foo/../bar", contains(new Glob("bar")));
+ expect("../foo/bar", contains(new Glob("../foo/bar")));
+ expect("foo/../../bar", contains(new Glob("../bar")));
+ });
+
+ test("Windows separators are converted in a Windows context", () {
+ expect(r"foo\bar", contains(new Glob("foo/bar", context: p.windows)));
+ expect(r"foo\bar/baz",
+ contains(new 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(new Glob("foo/bar")));
+ });
+
+ test("a relative path can be matched by an absolute glob", () {
+ var pattern = p.absolute('foo/bar');
+ if (Platform.isWindows) pattern = pattern.replaceAll('\\', '/');
+ expect('foo/bar', contains(new Glob(pattern)));
+ });
+
+ group("with recursive: true", () {
+ var glob = new 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(new Glob("/foo/bar", context: p.posix)));
+ expect("/foo/bar", isNot(contains(new Glob("**", context: p.posix))));
+ expect("/foo/bar", contains(new Glob("/**", context: p.posix)));
+ });
+
+ test("absolute Windows paths", () {
+ expect(r"C:\foo\bar", contains(new Glob("C:/foo/bar", context: p.windows)));
+ expect(r"C:\foo\bar", isNot(contains(new Glob("**", context: p.windows))));
+ expect(r"C:\foo\bar", contains(new Glob("C:/**", context: p.windows)));
+
+ expect(r"\\foo\bar\baz",
+ contains(new Glob("//foo/bar/baz", context: p.windows)));
+ expect(r"\\foo\bar\baz",
+ isNot(contains(new Glob("**", context: p.windows))));
+ expect(r"\\foo\bar\baz", contains(new Glob("//**", context: p.windows)));
+ expect(r"\\foo\bar\baz",
+ contains(new Glob("//foo/**", context: p.windows)));
+ });
+
+ test("absolute URL paths", () {
+ expect(r"http://foo.com/bar",
+ contains(new Glob("http://foo.com/bar", context: p.url)));
+ expect(r"http://foo.com/bar",
+ isNot(contains(new Glob("**", context: p.url))));
+ expect(r"http://foo.com/bar",
+ contains(new Glob("http://**", context: p.url)));
+ expect(r"http://foo.com/bar",
+ contains(new Glob("http://foo.com/**", context: p.url)));
+
+ expect("/foo/bar", contains(new Glob("/foo/bar", context: p.url)));
+ expect("/foo/bar", isNot(contains(new Glob("**", context: p.url))));
+ expect("/foo/bar", contains(new Glob("/**", context: p.url)));
+ });
+}
diff --git a/pkgs/glob/test/parse_test.dart b/pkgs/glob/test/parse_test.dart
new file mode 100644
index 0000000..5ea3f05
--- /dev/null
+++ b/pkgs/glob/test/parse_test.dart
@@ -0,0 +1,92 @@
+// 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:unittest/unittest.dart';
+
+void main() {
+ test("supports backslash-escaped characters", () {
+ expect(r"\*[]{,}?()", contains(new Glob(r"\\\*\[\]\{\,\}\?\(\)")));
+ });
+
+ test("disallows an empty glob", () {
+ expect(() => new Glob(""), throwsFormatException);
+ });
+
+ group("range", () {
+ test("supports either ^ or ! for negated ranges", () {
+ var bang = new Glob("fo[!a-z]");
+ expect("foo", isNot(contains(bang)));
+ expect("fo2", contains(bang));
+
+ var caret = new Glob("fo[^a-z]");
+ expect("foo", isNot(contains(bang)));
+ expect("fo2", contains(bang));
+ });
+
+ test("supports backslash-escaped characters", () {
+ var glob = new Glob(r"fo[\*\--\]]");
+ expect("fo]", contains(glob));
+ expect("fo-", contains(glob));
+ expect("fo*", contains(glob));
+ });
+
+ test("disallows inverted ranges", () {
+ expect(() => new Glob(r"[z-a]"), throwsFormatException);
+ });
+
+ test("disallows empty ranges", () {
+ expect(() => new Glob(r"[]"), throwsFormatException);
+ });
+
+ test("disallows unclosed ranges", () {
+ expect(() => new Glob(r"[abc"), throwsFormatException);
+ expect(() => new Glob(r"[-"), throwsFormatException);
+ });
+
+ test("disallows dangling ]", () {
+ expect(() => new Glob(r"abc]"), throwsFormatException);
+ });
+
+ test("disallows explicit /", () {
+ expect(() => new Glob(r"[/]"), throwsFormatException);
+ expect(() => new Glob(r"[ -/]"), throwsFormatException);
+ expect(() => new Glob(r"[/-~]"), throwsFormatException);
+ });
+ });
+
+ group("options", () {
+ test("allows empty branches", () {
+ var glob = new Glob("foo{,bar}");
+ expect("foo", contains(glob));
+ expect("foobar", contains(glob));
+ });
+
+ test("disallows empty options", () {
+ expect(() => new Glob("{}"), throwsFormatException);
+ });
+
+ test("disallows single options", () {
+ expect(() => new Glob("{foo}"), throwsFormatException);
+ });
+
+ test("disallows unclosed options", () {
+ expect(() => new Glob("{foo,bar"), throwsFormatException);
+ expect(() => new Glob("{foo,"), throwsFormatException);
+ });
+
+ test("disallows dangling }", () {
+ expect(() => new Glob("foo}"), throwsFormatException);
+ });
+
+ test("disallows dangling ] in options", () {
+ expect(() => new Glob(r"{abc]}"), throwsFormatException);
+ });
+ });
+
+ test("disallows unescaped parens", () {
+ expect(() => new Glob("foo(bar"), throwsFormatException);
+ expect(() => new Glob("foo)bar"), throwsFormatException);
+ });
+}