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