// 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. /// /// The path library was designed to be imported with a prefix, though you don't /// have to if you don't want to: /// /// import 'package:path/path.dart' as p; /// /// The most common way to use the library is through the top-level functions. /// These manipulate path strings based on your current working directory and /// the path style (POSIX, Windows, or URLs) of the host platform. For example: /// /// p.join('directory', 'file.txt'); /// /// This calls the top-level [join] function to join "directory" and "file.txt" /// using the current platform's directory separator. /// /// If you want to work with paths for a specific platform regardless of the /// underlying platform that the program is running on, you can create a /// [Context] and give it an explicit [Style]: /// /// var context = p.Context(style: Style.windows); /// context.join('directory', 'file.txt'); /// /// This will join "directory" and "file.txt" using the Windows path separator, /// even when the program is run on a POSIX machine. library; import 'src/context.dart'; import 'src/style.dart'; export 'src/context.dart' hide createInternal; export 'src/path_exception.dart'; export 'src/path_map.dart'; export 'src/path_set.dart'; export 'src/style.dart'; /// A default context for manipulating POSIX paths. final Context posix = Context(style: Style.posix); /// A default context for manipulating Windows paths. final Context windows = Context(style: Style.windows); /// A default context for manipulating URLs. /// /// URL path equality is undefined for paths that differ only in their /// percent-encoding or only in the case of their host segment. final Context url = Context(style: Style.url); /// The system path context. /// /// This differs from a context created with [Context.new] in that its /// [Context.current] is always the current working directory, rather than being /// set once when the context is created. final Context context = createInternal(); /// Returns the [Style] of the current context. /// /// This is the style that all top-level path functions will use. Style get style => context.style; /// Gets the path to the current working directory. /// /// In the browser, this means the current URL, without the last file segment. String get current { // If the current working directory gets deleted out from under the program, // accessing it will throw an IO exception. In order to avoid transient // errors, if we already have a cached working directory, catch the error and // use that. Uri uri; try { uri = Uri.base; } on Exception { if (_current != null) return _current!; rethrow; } // Converting the base URI to a file path is pretty slow, and the base URI // rarely changes in practice, so we cache the result here. if (uri == _currentUriBase) return _current!; _currentUriBase = uri; if (Style.platform == Style.url) { _current = uri.resolve('.').toString(); } else { final path = uri.toFilePath(); // Remove trailing '/' or '\' unless it is the only thing left // (for instance the root on Linux). final lastIndex = path.length - 1; assert(path[lastIndex] == '/' || path[lastIndex] == '\\'); _current = lastIndex == 0 ? path : path.substring(0, lastIndex); } return _current!; } /// The last value returned by [Uri.base]. /// /// This is used to cache the current working directory. Uri? _currentUriBase; /// The last known value of the current working directory. /// /// This is cached because [current] is called frequently but rarely actually /// changes. String? _current; /// Gets the path separator for the current platform. This is `\` on Windows /// and `/` on other platforms (including the browser). String get separator => context.separator; /// Returns a new path with the given path parts appended to [current]. /// /// Equivalent to [join()] with [current] as the first argument. Example: /// /// p.absolute('path', 'to/foo'); // -> '/your/current/dir/path/to/foo' /// /// Does not [normalize] or [canonicalize] paths. String absolute(String part1, [String? part2, String? part3, String? part4, String? part5, String? part6, String? part7, String? part8, String? part9, String? part10, String? part11, String? part12, String? part13, String? part14, String? part15]) => context.absolute(part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15); /// Gets the part of [path] after the last separator. /// /// p.basename('path/to/foo.dart'); // -> 'foo.dart' /// p.basename('path/to'); // -> 'to' /// /// Trailing separators are ignored. /// /// p.basename('path/to/'); // -> 'to' String basename(String path) => context.basename(path); /// Gets the part of [path] after the last separator, and without any trailing /// file extension. /// /// p.basenameWithoutExtension('path/to/foo.dart'); // -> 'foo' /// /// Trailing separators are ignored. /// /// p.basenameWithoutExtension('path/to/foo.dart/'); // -> 'foo' String basenameWithoutExtension(String path) => context.basenameWithoutExtension(path); /// Gets the part of [path] before the last separator. /// /// p.dirname('path/to/foo.dart'); // -> 'path/to' /// p.dirname('path/to'); // -> 'path' /// /// Trailing separators are ignored. /// /// p.dirname('path/to/'); // -> 'path' /// /// If an absolute path contains no directories, only a root, then the root /// is returned. /// /// p.dirname('/'); // -> '/' (posix) /// p.dirname('c:\'); // -> 'c:\' (windows) /// /// If a relative path has no directories, then '.' is returned. /// /// p.dirname('foo'); // -> '.' /// p.dirname(''); // -> '.' String dirname(String path) => context.dirname(path); /// Gets the file extension of [path]: the portion of [basename] from the last /// `.` to the end (including the `.` itself). /// /// p.extension('path/to/foo.dart'); // -> '.dart' /// p.extension('path/to/foo'); // -> '' /// p.extension('path.to/foo'); // -> '' /// p.extension('path/to/foo.dart.js'); // -> '.js' /// /// If the file name starts with a `.`, then that is not considered the /// extension: /// /// p.extension('~/.bashrc'); // -> '' /// p.extension('~/.notes.txt'); // -> '.txt' /// /// Takes an optional parameter `level` which makes possible to return /// multiple extensions having `level` number of dots. If `level` exceeds the /// number of dots, the full extension is returned. The value of `level` must /// be greater than 0, else `RangeError` is thrown. /// /// p.extension('foo.bar.dart.js', 2); // -> '.dart.js /// p.extension('foo.bar.dart.js', 3); // -> '.bar.dart.js' /// p.extension('foo.bar.dart.js', 10); // -> '.bar.dart.js' /// p.extension('path/to/foo.bar.dart.js', 2); // -> '.dart.js' String extension(String path, [int level = 1]) => context.extension(path, level); /// Returns the root of [path], if it's absolute, or the empty string if it's /// relative. /// /// // Unix /// p.rootPrefix('path/to/foo'); // -> '' /// p.rootPrefix('/path/to/foo'); // -> '/' /// /// // Windows /// p.rootPrefix(r'path\to\foo'); // -> '' /// p.rootPrefix(r'C:\path\to\foo'); // -> r'C:\' /// p.rootPrefix(r'\\server\share\a\b'); // -> r'\\server\share' /// /// // URL /// p.rootPrefix('path/to/foo'); // -> '' /// p.rootPrefix('https://dart.dev/path/to/foo'); /// // -> 'https://dart.dev' String rootPrefix(String path) => context.rootPrefix(path); /// Returns `true` if [path] is an absolute path and `false` if it is a /// relative path. /// /// On POSIX systems, absolute paths start with a `/` (forward slash). On /// Windows, an absolute path starts with `\\`, or a drive letter followed by /// `:/` or `:\`. For URLs, absolute paths either start with a protocol and /// optional hostname (e.g. `https://dart.dev`, `file://`) or with a `/`. /// /// URLs that start with `/` are known as "root-relative", since they're /// relative to the root of the current URL. Since root-relative paths are still /// absolute in every other sense, [isAbsolute] will return true for them. They /// can be detected using [isRootRelative]. bool isAbsolute(String path) => context.isAbsolute(path); /// Returns `true` if [path] is a relative path and `false` if it is absolute. /// On POSIX systems, absolute paths start with a `/` (forward slash). On /// Windows, an absolute path starts with `\\`, or a drive letter followed by /// `:/` or `:\`. bool isRelative(String path) => context.isRelative(path); /// Returns `true` if [path] is a root-relative path and `false` if it's not. /// /// URLs that start with `/` are known as "root-relative", since they're /// relative to the root of the current URL. Since root-relative paths are still /// absolute in every other sense, [isAbsolute] will return true for them. They /// can be detected using [isRootRelative]. /// /// No POSIX and Windows paths are root-relative. bool isRootRelative(String path) => context.isRootRelative(path); /// Joins the given path parts into a single path using the current platform's /// [separator]. Example: /// /// p.join('path', 'to', 'foo'); // -> 'path/to/foo' /// /// If any part ends in a path separator, then a redundant separator will not /// be added: /// /// p.join('path/', 'to', 'foo'); // -> 'path/to/foo' /// /// If a part is an absolute path, then anything before that will be ignored: /// /// p.join('path', '/to', 'foo'); // -> '/to/foo' String join(String part1, [String? part2, String? part3, String? part4, String? part5, String? part6, String? part7, String? part8, String? part9, String? part10, String? part11, String? part12, String? part13, String? part14, String? part15, String? part16]) => context.join(part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15, part16); /// Joins the given path parts into a single path using the current platform's /// [separator]. Example: /// /// p.joinAll(['path', 'to', 'foo']); // -> 'path/to/foo' /// /// If any part ends in a path separator, then a redundant separator will not /// be added: /// /// p.joinAll(['path/', 'to', 'foo']); // -> 'path/to/foo' /// /// If a part is an absolute path, then anything before that will be ignored: /// /// p.joinAll(['path', '/to', 'foo']); // -> '/to/foo' /// /// For a fixed number of parts, [join] is usually terser. String joinAll(Iterable parts) => context.joinAll(parts); /// Splits [path] into its components using the current platform's [separator]. /// /// p.split('path/to/foo'); // -> ['path', 'to', 'foo'] /// /// The path will *not* be normalized before splitting. /// /// p.split('path/../foo'); // -> ['path', '..', 'foo'] /// /// If [path] is absolute, the root directory will be the first element in the /// array. Example: /// /// // Unix /// p.split('/path/to/foo'); // -> ['/', 'path', 'to', 'foo'] /// /// // Windows /// p.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo'] /// p.split(r'\\server\share\path\to\foo'); /// // -> [r'\\server\share', 'foo', 'bar', 'baz'] /// /// // Browser /// p.split('https://dart.dev/path/to/foo'); /// // -> ['https://dart.dev', 'path', 'to', 'foo'] List split(String path) => context.split(path); /// Canonicalizes [path]. /// /// This is guaranteed to return the same path for two different input paths /// if and only if both input paths point to the same location. Unlike /// [normalize], it returns absolute paths when possible and canonicalizes /// ASCII case on Windows. /// /// Note that this does not resolve symlinks. /// /// If you want a map that uses path keys, it's probably more efficient to use a /// Map with [equals] and [hash] specified as the callbacks to use for keys than /// it is to canonicalize every key. String canonicalize(String path) => context.canonicalize(path); /// Normalizes [path], simplifying it by handling `..`, and `.`, and /// removing redundant path separators whenever possible. /// /// Note that this is *not* guaranteed to return the same result for two /// equivalent input paths. For that, see [canonicalize]. Or, if you're using /// paths as map keys use [equals] and [hash] as the key callbacks. /// /// p.normalize('path/./to/..//file.text'); // -> 'path/file.txt' String normalize(String path) => context.normalize(path); /// Attempts to convert [path] to an equivalent relative path from the current /// directory. /// /// // Given current directory is /root/path: /// p.relative('/root/path/a/b.dart'); // -> 'a/b.dart' /// p.relative('/root/other.dart'); // -> '../other.dart' /// /// If the [from] argument is passed, [path] is made relative to that instead. /// /// p.relative('/root/path/a/b.dart', from: '/root/path'); // -> 'a/b.dart' /// p.relative('/root/other.dart', from: '/root/path'); /// // -> '../other.dart' /// /// If [path] and/or [from] are relative paths, they are assumed to be relative /// to the current directory. /// /// Since there is no relative path from one drive letter to another on Windows, /// or from one hostname to another for URLs, this will return an absolute path /// in those cases. /// /// // Windows /// p.relative(r'D:\other', from: r'C:\home'); // -> 'D:\other' /// /// // URL /// p.relative('https://dart.dev', from: 'https://pub.dev'); /// // -> 'https://dart.dev' String relative(String path, {String? from}) => context.relative(path, from: from); /// Returns `true` if [child] is a path beneath `parent`, and `false` otherwise. /// /// p.isWithin('/root/path', '/root/path/a'); // -> true /// p.isWithin('/root/path', '/root/other'); // -> false /// p.isWithin('/root/path', '/root/path') // -> false bool isWithin(String parent, String child) => context.isWithin(parent, child); /// Returns `true` if [path1] points to the same location as [path2], and /// `false` otherwise. /// /// The [hash] function returns a hash code that matches these equality /// semantics. bool equals(String path1, String path2) => context.equals(path1, path2); /// Returns a hash code for [path] such that, if [equals] returns `true` for two /// paths, their hash codes are the same. /// /// Note that the same path may have different hash codes on different platforms /// or with different [current] directories. int hash(String path) => context.hash(path); /// Removes a trailing extension from the last part of [path]. /// /// p.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo' String withoutExtension(String path) => context.withoutExtension(path); /// Returns [path] with the trailing extension set to [extension]. /// /// If [path] doesn't have a trailing extension, this just adds [extension] to /// the end. /// /// p.setExtension('path/to/foo.dart', '.js') // -> 'path/to/foo.js' /// p.setExtension('path/to/foo.dart.js', '.map') /// // -> 'path/to/foo.dart.map' /// p.setExtension('path/to/foo', '.js') // -> 'path/to/foo.js' String setExtension(String path, String extension) => context.setExtension(path, extension); /// Returns the path represented by [uri], which may be a [String] or a [Uri]. /// /// For POSIX and Windows styles, [uri] must be a `file:` URI. For the URL /// style, this will just convert [uri] to a string. /// /// // POSIX /// p.fromUri('file:///path/to/foo') // -> '/path/to/foo' /// /// // Windows /// p.fromUri('file:///C:/path/to/foo') // -> r'C:\path\to\foo' /// /// // URL /// p.fromUri('https://dart.dev/path/to/foo') /// // -> 'https://dart.dev/path/to/foo' /// /// If [uri] is relative, a relative path will be returned. /// /// p.fromUri('path/to/foo'); // -> 'path/to/foo' String fromUri(Object? uri) => context.fromUri(uri!); /// Returns the URI that represents [path]. /// /// For POSIX and Windows styles, this will return a `file:` URI. For the URL /// style, this will just convert [path] to a [Uri]. /// /// // POSIX /// p.toUri('/path/to/foo') /// // -> Uri.parse('file:///path/to/foo') /// /// // Windows /// p.toUri(r'C:\path\to\foo') /// // -> Uri.parse('file:///C:/path/to/foo') /// /// // URL /// p.toUri('https://dart.dev/path/to/foo') /// // -> Uri.parse('https://dart.dev/path/to/foo') /// /// If [path] is relative, a relative URI will be returned. /// /// p.toUri('path/to/foo') // -> Uri.parse('path/to/foo') Uri toUri(String path) => context.toUri(path); /// Returns a terse, human-readable representation of [uri]. /// /// [uri] can be a [String] or a [Uri]. If it can be made relative to the /// current working directory, that's done. Otherwise, it's returned as-is. This /// gracefully handles non-`file:` URIs for [Style.posix] and [Style.windows]. /// /// The returned value is meant for human consumption, and may be either URI- /// or path-formatted. /// /// // POSIX at "/root/path" /// p.prettyUri('file:///root/path/a/b.dart'); // -> 'a/b.dart' /// p.prettyUri('https://dart.dev/'); // -> 'https://dart.dev' /// /// // Windows at "C:\root\path" /// p.prettyUri('file:///C:/root/path/a/b.dart'); // -> r'a\b.dart' /// p.prettyUri('https://dart.dev/'); // -> 'https://dart.dev' /// /// // URL at "https://dart.dev/root/path" /// p.prettyUri('https://dart.dev/root/path/a/b.dart'); // -> r'a/b.dart' /// p.prettyUri('file:///root/path'); // -> 'file:///root/path' String prettyUri(Object? uri) => context.prettyUri(uri!);