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);
+  });
+}