Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(935)

Unified Diff: utils/pub/path.dart

Issue 11475046: Add Bob's path library to pub. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 8 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: utils/pub/path.dart
diff --git a/utils/pub/path.dart b/utils/pub/path.dart
new file mode 100644
index 0000000000000000000000000000000000000000..3b889d380654bd50e2d75c97e3e8f3e5964bfc15
--- /dev/null
+++ b/utils/pub/path.dart
@@ -0,0 +1,514 @@
+// Copyright (c) 2012, 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.
+
+/// A comprehensive, cross-platform path manipulation library.
+library path;
+
+import 'dart:io' as io;
+
+/// An internal builder for the current OS so we can provide a straight
+/// functional interface and not require users to create one.
+final _builder = new Builder();
+
+/// Gets the path to the current working directory.
+String get current => new io.Directory.current().path;
+
+/// Gets the path separator for the current platform. On Mac and Linux, this
+/// is `/`. On Windows, it's `\`.
+String get separator => _builder.separator;
+
+/// Converts [path] to an absolute path by resolving it relative to the current
+/// working directory. If [path] is already an absolute path, just returns it.
+///
+/// path.absolute('foo/bar.txt'); // -> /your/current/dir/foo/bar.txt
+String absolute(String path) => join(current, path);
+
+/// Gets the file extension of [path]; the portion after the last `.` in the
nweiz 2012/12/08 01:46:31 nit: ";" -> ":". Also this should probably be "af
Bob Nystrom 2012/12/08 05:12:33 Done.
+/// [basename] of the path.
+///
+/// path.extension('path/to/foo.dart'); // -> '.dart'
+/// path.extension('path/to/foo'); // -> ''
+/// path.extension('path.to/foo'); // -> ''
+/// path.extension('path/to/foo.dart.js'); // -> '.js'
+///
+/// If the file name starts with a `.`, then it is not considered an extension:
+///
+/// path.extension('~/.bashrc'); // -> ''
nweiz 2012/12/08 01:46:31 Maybe give an example of a dotfile with an extensi
Bob Nystrom 2012/12/08 05:12:33 Done.
+String extension(String path) => _builder.extension(path);
+
+/// Gets the part of [path] after the last separator on the current platform.
nweiz 2012/12/08 01:46:31 "on the current platform" is implicit. Also below.
Bob Nystrom 2012/12/08 05:12:33 Done.
+///
+/// path.filename('path/to/foo.dart'); // -> 'foo.dart'
+/// path.filename('path/to'); // -> 'to'
+String filename(String path) => _builder.filename(path);
nweiz 2012/12/08 01:46:31 This should be called "basename", for consistency
Bob Nystrom 2012/12/08 05:12:33 Done.
+
+/// Gets the part of [path] after the last separator on the current platform,
+/// and without any trailing file extension.
+///
+/// path.filenameWithoutExtension('path/to/foo.dart'); // -> 'foo'
+String filenameWithoutExtension(String path) =>
+ _builder.filenameWithoutExtension(path);
+
+/// Returns `true` if [path] is an absolute path and `false` if it is a
+/// relative path. On Mac and Unix systems, relative paths start with a `/`
nweiz 2012/12/08 01:46:31 "relative paths" -> "absolute paths". Also, OS X i
Bob Nystrom 2012/12/08 05:12:33 Done. Changed to "POSIX".
+/// (forward slash). On Windows, an absolute path starts with `\\`, or a drive
+/// letter followed by `:/` or `:\`.
+bool isAbsolute(String path) => _builder.isAbsolute(path);
+
+/// Returns `true` if [path] is a relative path and `false` if it is absolute.
+/// On Mac and Unix systems, relative paths start with a `/` (forward slash).
+/// On Windows, an absolute path starts with `\\`, or a drive letter followed
+/// by `:/` or `:\`.
+bool isRelative(String path) => _builder.isRelative(path);
+
+/// Joins the given path parts into a single path using the current platform's
+/// [separator]. Example:
+///
+/// path.join('path', 'to', 'foo'); // -> 'path/to/foo'
+///
+/// If any part ends in a path separator, then a redundant separator will not
+/// be added:
+///
+/// path.join('path/', 'to', 'foo'); // -> 'path/to/foo
+///
+/// If a part is an absolute path, then anything before that will be ignored:
+///
+/// path.join('path', '/to', 'foo'); // -> '/to/foo'
+///
nweiz 2012/12/08 01:46:31 Nit: extra line
Bob Nystrom 2012/12/08 05:12:33 Done.
+String join(String part1, [String part2, String part3, String part4,
+ String part5, String part6, String part7, String part8]) {
+ if (!?part2) return _builder.join(part1);
+ if (!?part3) return _builder.join(part1, part2);
+ if (!?part4) return _builder.join(part1, part2, part3);
+ if (!?part5) return _builder.join(part1, part2, part3, part4);
+ if (!?part6) return _builder.join(part1, part2, part3, part4, part5);
+ if (!?part7) return _builder.join(part1, part2, part3, part4, part5, part6);
+ if (!?part8) return _builder.join(part1, part2, part3, part4, part5, part6,
+ part7);
+ return _builder.join(part1, part2, part3, part4, part5, part6, part7, part8);
+}
+
+/// Normalizes [path], simplifying it by handling `..`, and `.`, and
+/// removing redundant path separators whenever possible.
+///
+/// path.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
+String normalize(String path) => _builder.normalize(path);
+
+/// Converts [path] to an equivalent relative path from the current directory.
nweiz 2012/12/08 01:46:31 This doesn't always return a relative path. On Win
Bob Nystrom 2012/12/08 05:12:33 Done.
+///
+/// // Given current directory is /root/path:
+/// path.relative('/root/path/a/b.dart'); // -> 'a/b.dart'
+/// path.relative('/root/other.dart'); // -> '../other.dart'
+String relative(String path) => _builder.relative(path);
+
+/// Removes a trailing extension from the last part of [path].
+///
+/// withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
+String withoutExtension(String path) => _builder.withoutExtension(path);
+
+/// An instantiable class for manipulating paths. Unlike the top-level
+/// functions, this lets you explicitly select what platform the paths will use.
+class Builder {
nweiz 2012/12/08 01:46:31 Any comments above obviously apply to the parallel
Bob Nystrom 2012/12/08 05:12:33 Done.
+ /// Creates a new path builder for the given style and root directory.
nweiz 2012/12/08 01:46:31 It may be worth emphasizing more that root is alwa
Bob Nystrom 2012/12/08 05:12:33 I think this will be OK? I'm not sure how to empha
+ ///
+ /// If [style] is omitted, it uses the host operating system's path style. If
+ /// [root] is omitted, it defaults to the current working directory.
nweiz 2012/12/08 01:46:31 Mention that if `root` is relative, it's considere
Bob Nystrom 2012/12/08 05:12:33 Done.
+ factory Builder({Style style, String root}) {
+ if (style == null) {
+ if (io.Platform.operatingSystem == 'windows') {
+ style = Style.windows;
+ } else {
+ style = Style.posix;
+ }
+ }
+
+ if (root == null) root = new io.Directory.current().path;
nweiz 2012/12/08 01:46:31 Nit: it'd be a little cleaner to use `current` her
Bob Nystrom 2012/12/08 05:12:33 Done.
+
+ return new Builder._(style, root);
+ }
+
+ Builder._(this.style, this.root);
+
+ /// The style of path that this builder works with.
+ final Style style;
+
+ /// The root directory that relative paths will be relative to.
+ final String root;
+
+ /// Gets the path separator for the builder's [style]. On Mac and Linux,
+ /// this is `/`. On Windows, it's `\`.
+ String get separator => style.separator;
+
+ /// Gets the file extension of [path]; the portion after the last `.` in the
+ /// [basename] of the path.
+ ///
+ /// builder.extension('path/to/foo.dart'); // -> '.dart'
+ /// builder.extension('path/to/foo'); // -> ''
+ /// builder.extension('path.to/foo'); // -> ''
+ /// builder.extension('path/to/foo.dart.js'); // -> '.js'
+ ///
+ /// If the file name starts with a `.`, then it is not considered an
+ /// extension:
+ ///
+ /// builder.extension('~/.bashrc'); // -> ''
+ String extension(String path) => _parse(path).extension;
+
+ /// Gets the part of [path] after the last separator on the builder's
+ /// platform.
+ ///
+ /// builder.filename('path/to/foo.dart'); // -> 'foo.dart'
+ /// builder.filename('path/to'); // -> 'to'
+ String filename(String path) => _parse(path).filename;
+
+ /// Gets the part of [path] after the last separator on the builder's
+ /// platform, and without any trailing file extension.
+ ///
+ /// builder.filenameWithoutExtension('path/to/foo.dart'); // -> 'foo'
+ String filenameWithoutExtension(String path) =>
+ _parse(path).filenameWithoutExtension;
+
+ /// Returns `true` if [path] is an absolute path and `false` if it is a
+ /// relative path. On Mac and Unix systems, relative paths start with a `/`
+ /// (forward slash). On Windows, an absolute path starts with `\\`, or a drive
+ /// letter followed by `:/` or `:\`.
+ bool isAbsolute(String path) => _parse(path).isAbsolute;
+
+ /// Returns `true` if [path] is a relative path and `false` if it is absolute.
+ /// On Mac and Unix systems, relative paths start with a `/` (forward slash).
+ /// On Windows, an absolute path starts with `\\`, or a drive letter followed
+ /// by `:/` or `:\`.
+ bool isRelative(String path) => !isAbsolute(path);
+
+ /// Joins the given path parts into a single path. Example:
+ ///
+ /// builder.join('path', 'to', 'foo'); // -> 'path/to/foo'
+ ///
+ /// If any part ends in a path separator, then a redundant separator will not
+ /// be added:
+ ///
+ /// builder.join('path/', 'to', 'foo'); // -> 'path/to/foo
+ ///
+ /// If a part is an absolute path, then anything before that will be ignored:
+ ///
+ /// builder.join('path', '/to', 'foo'); // -> '/to/foo'
+ ///
+ String join(String part1, [String part2, String part3, String part4,
+ String part5, String part6, String part7, String part8]) {
+ var buffer = new StringBuffer();
+ var needsSeparator = false;
+
+ addPart(condition, part) {
+ if (!condition) return;
+
+ if (this.isAbsolute(part)) {
+ // An absolute path discards everything before it.
+ buffer.clear();
+ buffer.add(part);
+ } else {
+ if (part.length > 0 && style.separatorPattern.hasMatch(part[0])) {
+ // The part starts with a separator, so we don't need to add one.
+ } else if (needsSeparator) {
+ buffer.add(separator);
+ }
+
+ buffer.add(part);
+ }
+
+ // Unless this part ends with a separator, we'll need to add one before
+ // the next part.
+ needsSeparator = part.length > 0 &&
+ !style.separatorPattern.hasMatch(part[part.length - 1]);
+ }
+
+ addPart(true, part1);
+ addPart(?part2, part2);
+ addPart(?part3, part3);
+ addPart(?part4, part4);
+ addPart(?part5, part5);
+ addPart(?part6, part6);
+ addPart(?part7, part7);
+ addPart(?part8, part8);
+
+ return buffer.toString();
+ }
+
+ /// Normalizes [path], simplifying it by handling `..`, and `.`, and
+ /// removing redundant path separators whenever possible.
+ ///
+ /// builder.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
+ String normalize(String path) {
+ if (path == '') return path;
nweiz 2012/12/08 01:46:31 An empty string is never a valid path. Why is ther
Bob Nystrom 2012/12/08 05:12:33 It's following python. It feels a bit weird, but I
nweiz 2012/12/10 21:30:59 Even if it works, it's a little weird that you're
+
+ var parsed = _parse(path);
+ parsed.normalize();
+ return parsed.toString();
+ }
+
+ /// Creates a new path by appending the given path parts to the [root].
+ /// Equivalent to [join()] with [root] as the first argument. Example:
+ ///
+ /// var builder = new Builder(root: 'root');
+ /// builder.join('path', 'to', 'foo'); // -> 'root/path/to/foo'
nweiz 2012/12/08 01:46:31 ".join" -> ".resolve"
Bob Nystrom 2012/12/08 05:12:33 Done.
+ String resolve(String part1, [String part2, String part3, String part4,
+ String part5, String part6, String part7]) {
+ if (!?part2) return join(root, part1);
+ if (!?part3) return join(root, part1, part2);
+ if (!?part4) return join(root, part1, part2, part3);
+ if (!?part5) return join(root, part1, part2, part3, part4);
+ if (!?part6) return join(root, part1, part2, part3, part4, part5);
+ if (!?part7) return join(root, part1, part2, part3, part4, part5, part6);
+ return join(root, part1, part2, part3, part4, part5, part6, part7);
+ }
+
+ /// Converts [path] to an equivalent relative path starting at [root].
nweiz 2012/12/08 01:46:31 "an equivalent path relative to [root]"
Bob Nystrom 2012/12/08 05:12:33 Done.
+ ///
+ /// var builder = new Builder(root: '/root/path');
+ /// builder.relative('/root/path/a/b.dart'); // -> 'a/b.dart'
+ /// builder.relative('/root/other.dart'); // -> '../other.dart'
+ String relative(String path) {
+ // If the base path is relative, resolve it relative to the current
+ // directory.
+ var base = root;
+ if (this.isRelative(base)) base = absolute(base);
+
+ // If the given path is relative, resolve it relative to the base.
nweiz 2012/12/08 01:46:31 This seems unnecessary. If it's already relative t
Bob Nystrom 2012/12/08 05:12:33 Done.
+ path = this.join(base, path);
+
+ var baseParsed = _parse(base)..normalize();
+ var pathParsed = _parse(path)..normalize();
+
+ // If the root prefixes don't match (for example, different drive letters
+ // on Windows), then there is no relative path, so just return the absolute
+ // one.
+ if (baseParsed.root != pathParsed.root) return pathParsed.toString();
+
+ // Strip off their common prefix.
+ while (baseParsed.parts.length > 0 && pathParsed.parts.length > 0) {
+ if (baseParsed.parts[0] != pathParsed.parts[0]) break;
nweiz 2012/12/08 01:46:31 while (baseParsed.parts.length > 0 && pathParsed.p
Bob Nystrom 2012/12/08 05:12:33 Felt a bit gratuitous to me, but done.
+ baseParsed.parts.removeAt(0);
+ baseParsed.separators.removeAt(0);
+ pathParsed.parts.removeAt(0);
+ pathParsed.separators.removeAt(0);
+ }
+
+ // If there are any directories left in the root path, we need to walk up
+ // out of them.
+ pathParsed.parts.insertRange(0, baseParsed.parts.length, '..');
+ pathParsed.separators.insertRange(0, baseParsed.parts.length,
+ style.separator);
+
+ // Corner case: the paths completely collapsed.
+ if (pathParsed.parts.length == 0) return '.';
+
+ // Make it relative.
+ pathParsed.root = '';
+ pathParsed.removeTrailingSeparator();
+
+ return pathParsed.toString();
+ }
+
+ /// Removes a trailing extension from the last part of [path].
+ ///
+ /// builder.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
+ String withoutExtension(String path) {
nweiz 2012/12/08 01:46:31 Why isn't this using _parse?
Bob Nystrom 2012/12/08 05:12:33 Done.
+ var lastSeparator = path.lastIndexOf(separator);
nweiz 2012/12/08 01:46:31 Isn't this dangerous with windows paths that use `
Bob Nystrom 2012/12/08 05:12:33 Good catch. Added a test and made this use _parse.
+ var lastDot = path.lastIndexOf('.');
+
+ // Ignore '.' in anything but the last component.
+ if (lastSeparator != -1 && lastDot <= lastSeparator + 1) lastDot = -1;
+
+ if (lastDot <= 0) return path;
+ return path.substring(0, lastDot);
+ }
+
+ _ParsedPath _parse(String path) {
+ var before = path;
+
+ // Remove the root prefix, if any.
+ var root = style.getRoot(path);
+ if (root != null) path = path.substring(root.length);
+
+ // Split the parts on path separators.
+ var parts = [];
+ var separators = [];
+ var start = 0;
+ for (var match in style.separatorPattern.allMatches(path)) {
+ parts.add(path.substring(start, match.start));
+ separators.add(match[0]);
nweiz 2012/12/08 01:46:31 Why is it useful to keep track of separators? It j
Bob Nystrom 2012/12/08 05:12:33 As much as possible, path retains the information
nweiz 2012/12/10 21:30:59 I think this will end up causing more pain than it
+ start = match.end;
+ }
+
+ // Add the final part, if any.
+ if (start < path.length) {
+ parts.add(path.substring(start));
+ separators.add('');
+ }
+
+ // Separate out the file extension.
+ var extension = '';
+ if (parts.length > 0) {
+ var file = parts.last;
+ if (file != '..') {
+ var lastDot = file.lastIndexOf('.');
+
+ // If there is a dot (and it's not the first character, like '.bashrc').
+ if (lastDot > 0) {
+ parts[parts.length - 1] = file.substring(0, lastDot);
+ extension = file.substring(lastDot);
+ }
+ }
+ }
+
+ return new _ParsedPath(style, root, parts, separators, extension);
+ }
+}
+
+/// An enum type describing a "flavor" of path.
+class Style {
+ /// POSIX-style paths use "/" (forward slash) as separators. Absolute paths
+ /// start with "/". Used by UNIX, Linux, Mac OS X, and others.
+ static final posix = new Style._('posix', '/', '/', '/');
+
+ /// Windows paths use "\" (backslash) as separators. Absolute paths start with
+ /// a drive letter followed by a colon (example, "C:") or two backslashes
+ /// ("\\") for UNC paths.
nweiz 2012/12/08 01:46:31 UNC prefixes should encompass the server name as w
Bob Nystrom 2012/12/08 05:12:33 Added a TODO for now.
+ static final windows = new Style._('windows', '\\', r'[/\\]',
+ r'\\\\|[a-zA-Z]:[/\\]');
+
+ Style._(this.name, this.separator, String separatorPattern, String rootPattern)
nweiz 2012/12/08 01:46:31 Long line
Bob Nystrom 2012/12/08 05:12:33 Done.
+ : separatorPattern = new RegExp(separatorPattern),
+ _rootPattern = new RegExp('^$rootPattern');
nweiz 2012/12/08 01:46:31 This RegExp should be '^($rootPattern)'.
Bob Nystrom 2012/12/08 05:12:33 How come?
nweiz 2012/12/10 21:30:59 Otherwise on Windows it ends up as r'^\\\\|[a-zA-Z
+
+ /// The name of this path style. Will be "posix" or "windows".
+ final String name;
+
+ /// The path separator for this style. On POSIX, this is `/`. On Windows,
+ /// it's `\`.
+ final String separator;
+
+ /// The [Pattern] that can be used to match a separator for a path in this
+ /// style. Windows allows both "/" and "\" as path separators even though
+ /// "\" is the canonical one.
+ final Pattern separatorPattern;
+
+ /// The [Pattern] that can be used to match the root prefix of an absolute
+ /// path in this style.
+ final Pattern _rootPattern;
+
+ /// Gets the root prefix of [path] if path is absolute. If [path] is relative,
+ /// returns `null`.
+ String getRoot(String path) {
+ var match = _rootPattern.firstMatch(path);
+ if (match == null) return null;
+ return match[0];
+ }
+
+ String toString() => name;
+}
+
+// TODO(rnystrom): Make this public?
+class _ParsedPath {
+ /// The [Style] that was used to parse this path.
+ Style style;
+
+ /// The absolute root portion of the path, or `null` if the path is relative.
+ /// On POSIX systems, this will be `null` or "/". On Windows, it can be
+ /// `null`, "//" for a UNC path, or something like "C:\" for paths with drive
+ /// letters.
+ String root;
+
+ /// The path-separated parts of the path. All but the last will be
+ /// directories. The last could be a directory, or could be the file name
+ /// without its extension.
+ List<String> parts;
+
+ /// The path separators following each part. The last one will be an empty
+ /// string unless the path ends with a trailing separator.
+ List<String> separators;
+
+ /// The file's extension, or "" if it doesn't have one.
+ String extension;
+
+ /// `true` if the path ends with a trailing separator.
+ bool get hasTrailingSeparator {
+ if (separators.length == 0) return false;
+ return separators[separators.length - 1] != '';
+ }
+
+ /// `true` if this is an absolute path.
+ bool get isAbsolute => root != null;
+
+ _ParsedPath(this.style, this.root, this.parts, this.separators,
+ this.extension);
+
+ String get filename {
+ if (parts.length == 0) return extension;
+ if (hasTrailingSeparator) return '';
+ return '${parts.last}$extension';
+ }
+
+ String get filenameWithoutExtension {
+ if (parts.length == 0) return '';
+ if (hasTrailingSeparator) return '';
+ return parts.last;
+ }
+
+ void removeTrailingSeparator() {
+ if (separators.length > 0) {
+ separators[separators.length - 1] = '';
+ }
+ }
+
+ void normalize() {
+ // Handle '.', '..', and empty parts.
+ var leadingDoubles = 0;
+ var newParts = [];
+ for (var part in parts) {
+ if (part == '.' || part == '') {
+ // Do nothing. Ignore it.
+ } else if (part == '..') {
+ // Pop the last part off.
+ if (newParts.length > 0) {
+ newParts.removeLast();
+ } else {
+ // Backed out past the beginning, so preserve the "..".
+ leadingDoubles++;
+ }
+ } else {
+ newParts.add(part);
+ }
+ }
+
+ // A relative path can back out from the start directory.
+ if (!isAbsolute) {
+ newParts.insertRange(0, leadingDoubles, '..');
+ }
+
+ // If we collapsed down to nothing, do ".".
+ if (newParts.length == 0 && !isAbsolute) {
+ newParts.add('.');
+ }
+
+ // Canonicalize separators.
+ var newSeparators = [];
+ newSeparators.insertRange(0, newParts.length, style.separator);
+
+ parts = newParts;
+ separators = newSeparators;
+
+ removeTrailingSeparator();
+ }
+
+ String toString() {
+ var builder = new StringBuffer();
+ if (root != null) builder.add(root);
+ for (var i = 0; i < parts.length; i++) {
+ builder.add(parts[i]);
+ if (extension != null && i == parts.length - 1) builder.add(extension);
+ builder.add(separators[i]);
+ }
+
+ return builder.toString();
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698