| // Written in the D programming language. |
| |
| /** This module is used to manipulate _path strings. |
| |
| All functions, with the exception of $(LREF expandTilde) (and in some |
| cases $(LREF absolutePath) and $(LREF relativePath)), are pure |
| string manipulation functions; they don't depend on any state outside |
| the program, nor do they perform any actual file system actions. |
| This has the consequence that the module does not make any distinction |
| between a _path that points to a directory and a _path that points to a |
| file, and it does not know whether or not the object pointed to by the |
| _path actually exists in the file system. |
| To differentiate between these cases, use $(REF isDir, std,file) and |
| $(REF exists, std,file). |
| |
| Note that on Windows, both the backslash ($(D `\`)) and the slash ($(D `/`)) |
| are in principle valid directory separators. This module treats them |
| both on equal footing, but in cases where a $(I new) separator is |
| added, a backslash will be used. Furthermore, the $(LREF buildNormalizedPath) |
| function will replace all slashes with backslashes on that platform. |
| |
| In general, the functions in this module assume that the input paths |
| are well-formed. (That is, they should not contain invalid characters, |
| they should follow the file system's _path format, etc.) The result |
| of calling a function on an ill-formed _path is undefined. When there |
| is a chance that a _path or a file name is invalid (for instance, when it |
| has been input by the user), it may sometimes be desirable to use the |
| $(LREF isValidFilename) and $(LREF isValidPath) functions to check |
| this. |
| |
| Most functions do not perform any memory allocations, and if a string is |
| returned, it is usually a slice of an input string. If a function |
| allocates, this is explicitly mentioned in the documentation. |
| |
| $(SCRIPT inhibitQuickIndex = 1;) |
| $(DIVC quickindex, |
| $(BOOKTABLE, |
| $(TR $(TH Category) $(TH Functions)) |
| $(TR $(TD Normalization) $(TD |
| $(LREF absolutePath) |
| $(LREF asAbsolutePath) |
| $(LREF asNormalizedPath) |
| $(LREF asRelativePath) |
| $(LREF buildNormalizedPath) |
| $(LREF buildPath) |
| $(LREF chainPath) |
| $(LREF expandTilde) |
| )) |
| $(TR $(TD Partitioning) $(TD |
| $(LREF baseName) |
| $(LREF dirName) |
| $(LREF dirSeparator) |
| $(LREF driveName) |
| $(LREF pathSeparator) |
| $(LREF pathSplitter) |
| $(LREF relativePath) |
| $(LREF rootName) |
| $(LREF stripDrive) |
| )) |
| $(TR $(TD Validation) $(TD |
| $(LREF isAbsolute) |
| $(LREF isDirSeparator) |
| $(LREF isRooted) |
| $(LREF isValidFilename) |
| $(LREF isValidPath) |
| )) |
| $(TR $(TD Extension) $(TD |
| $(LREF defaultExtension) |
| $(LREF extension) |
| $(LREF setExtension) |
| $(LREF stripExtension) |
| $(LREF withDefaultExtension) |
| $(LREF withExtension) |
| )) |
| $(TR $(TD Other) $(TD |
| $(LREF filenameCharCmp) |
| $(LREF filenameCmp) |
| $(LREF globMatch) |
| $(LREF CaseSensitive) |
| )) |
| )) |
| |
| Authors: |
| Lars Tandle Kyllingstad, |
| $(HTTP digitalmars.com, Walter Bright), |
| Grzegorz Adam Hankiewicz, |
| Thomas K$(UUML)hne, |
| $(HTTP erdani.org, Andrei Alexandrescu) |
| Copyright: |
| Copyright (c) 2000-2014, the authors. All rights reserved. |
| License: |
| $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0) |
| Source: |
| $(PHOBOSSRC std/_path.d) |
| */ |
| module std.path; |
| |
| |
| // FIXME |
| import std.file; //: getcwd; |
| static import std.meta; |
| import std.range.primitives; |
| import std.traits; |
| |
| version (unittest) |
| { |
| private: |
| struct TestAliasedString |
| { |
| string get() @safe @nogc pure nothrow { return _s; } |
| alias get this; |
| @disable this(this); |
| string _s; |
| } |
| |
| bool testAliasedString(alias func, Args...)(string s, Args args) |
| { |
| return func(TestAliasedString(s), args) == func(s, args); |
| } |
| } |
| |
| /** String used to separate directory names in a path. Under |
| POSIX this is a slash, under Windows a backslash. |
| */ |
| version (Posix) enum string dirSeparator = "/"; |
| else version (Windows) enum string dirSeparator = "\\"; |
| else static assert(0, "unsupported platform"); |
| |
| |
| |
| |
| /** Path separator string. A colon under POSIX, a semicolon |
| under Windows. |
| */ |
| version (Posix) enum string pathSeparator = ":"; |
| else version (Windows) enum string pathSeparator = ";"; |
| else static assert(0, "unsupported platform"); |
| |
| |
| |
| |
| /** Determines whether the given character is a directory separator. |
| |
| On Windows, this includes both $(D `\`) and $(D `/`). |
| On POSIX, it's just $(D `/`). |
| */ |
| bool isDirSeparator(dchar c) @safe pure nothrow @nogc |
| { |
| if (c == '/') return true; |
| version (Windows) if (c == '\\') return true; |
| return false; |
| } |
| |
| |
| /* Determines whether the given character is a drive separator. |
| |
| On Windows, this is true if c is the ':' character that separates |
| the drive letter from the rest of the path. On POSIX, this always |
| returns false. |
| */ |
| private bool isDriveSeparator(dchar c) @safe pure nothrow @nogc |
| { |
| version (Windows) return c == ':'; |
| else return false; |
| } |
| |
| |
| /* Combines the isDirSeparator and isDriveSeparator tests. */ |
| version (Windows) private bool isSeparator(dchar c) @safe pure nothrow @nogc |
| { |
| return isDirSeparator(c) || isDriveSeparator(c); |
| } |
| version (Posix) private alias isSeparator = isDirSeparator; |
| |
| |
| /* Helper function that determines the position of the last |
| drive/directory separator in a string. Returns -1 if none |
| is found. |
| */ |
| private ptrdiff_t lastSeparator(R)(R path) |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| auto i = (cast(ptrdiff_t) path.length) - 1; |
| while (i >= 0 && !isSeparator(path[i])) --i; |
| return i; |
| } |
| |
| |
| version (Windows) |
| { |
| private bool isUNC(R)(R path) |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| return path.length >= 3 && isDirSeparator(path[0]) && isDirSeparator(path[1]) |
| && !isDirSeparator(path[2]); |
| } |
| |
| private ptrdiff_t uncRootLength(R)(R path) |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| in { assert(isUNC(path)); } |
| body |
| { |
| ptrdiff_t i = 3; |
| while (i < path.length && !isDirSeparator(path[i])) ++i; |
| if (i < path.length) |
| { |
| auto j = i; |
| do { ++j; } while (j < path.length && isDirSeparator(path[j])); |
| if (j < path.length) |
| { |
| do { ++j; } while (j < path.length && !isDirSeparator(path[j])); |
| i = j; |
| } |
| } |
| return i; |
| } |
| |
| private bool hasDrive(R)(R path) |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| return path.length >= 2 && isDriveSeparator(path[1]); |
| } |
| |
| private bool isDriveRoot(R)(R path) |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| return path.length >= 3 && isDriveSeparator(path[1]) |
| && isDirSeparator(path[2]); |
| } |
| } |
| |
| |
| /* Helper functions that strip leading/trailing slashes and backslashes |
| from a path. |
| */ |
| private auto ltrimDirSeparators(R)(R path) |
| if (isInputRange!R && !isInfinite!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| static if (isRandomAccessRange!R && hasSlicing!R || isNarrowString!R) |
| { |
| int i = 0; |
| while (i < path.length && isDirSeparator(path[i])) |
| ++i; |
| return path[i .. path.length]; |
| } |
| else |
| { |
| while (!path.empty && isDirSeparator(path.front)) |
| path.popFront(); |
| return path; |
| } |
| } |
| |
| @system unittest |
| { |
| import std.array; |
| import std.utf : byDchar; |
| |
| assert(ltrimDirSeparators("//abc//").array == "abc//"); |
| assert(ltrimDirSeparators("//abc//"d).array == "abc//"d); |
| assert(ltrimDirSeparators("//abc//".byDchar).array == "abc//"d); |
| } |
| |
| private auto rtrimDirSeparators(R)(R path) |
| if (isBidirectionalRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| static if (isRandomAccessRange!R && hasSlicing!R && hasLength!R || isNarrowString!R) |
| { |
| auto i = (cast(ptrdiff_t) path.length) - 1; |
| while (i >= 0 && isDirSeparator(path[i])) |
| --i; |
| return path[0 .. i+1]; |
| } |
| else |
| { |
| while (!path.empty && isDirSeparator(path.back)) |
| path.popBack(); |
| return path; |
| } |
| } |
| |
| @system unittest |
| { |
| import std.array; |
| import std.utf : byDchar; |
| |
| assert(rtrimDirSeparators("//abc//").array == "//abc"); |
| assert(rtrimDirSeparators("//abc//"d).array == "//abc"d); |
| |
| assert(rtrimDirSeparators(MockBiRange!char("//abc//")).array == "//abc"); |
| } |
| |
| private auto trimDirSeparators(R)(R path) |
| if (isBidirectionalRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| return ltrimDirSeparators(rtrimDirSeparators(path)); |
| } |
| |
| @system unittest |
| { |
| import std.array; |
| import std.utf : byDchar; |
| |
| assert(trimDirSeparators("//abc//").array == "abc"); |
| assert(trimDirSeparators("//abc//"d).array == "abc"d); |
| |
| assert(trimDirSeparators(MockBiRange!char("//abc//")).array == "abc"); |
| } |
| |
| |
| |
| |
| /** This $(D enum) is used as a template argument to functions which |
| compare file names, and determines whether the comparison is |
| case sensitive or not. |
| */ |
| enum CaseSensitive : bool |
| { |
| /// File names are case insensitive |
| no = false, |
| |
| /// File names are case sensitive |
| yes = true, |
| |
| /** The default (or most common) setting for the current platform. |
| That is, $(D no) on Windows and Mac OS X, and $(D yes) on all |
| POSIX systems except OS X (Linux, *BSD, etc.). |
| */ |
| osDefault = osDefaultCaseSensitivity |
| } |
| version (Windows) private enum osDefaultCaseSensitivity = false; |
| else version (OSX) private enum osDefaultCaseSensitivity = false; |
| else version (Posix) private enum osDefaultCaseSensitivity = true; |
| else static assert(0); |
| |
| |
| |
| |
| /** |
| Params: |
| cs = Whether or not suffix matching is case-sensitive. |
| path = A path name. It can be a string, or any random-access range of |
| characters. |
| suffix = An optional suffix to be removed from the file name. |
| Returns: The name of the file in the path name, without any leading |
| directory and with an optional suffix chopped off. |
| |
| If $(D suffix) is specified, it will be compared to $(D path) |
| using $(D filenameCmp!cs), |
| where $(D cs) is an optional template parameter determining whether |
| the comparison is case sensitive or not. See the |
| $(LREF filenameCmp) documentation for details. |
| |
| Example: |
| --- |
| assert(baseName("dir/file.ext") == "file.ext"); |
| assert(baseName("dir/file.ext", ".ext") == "file"); |
| assert(baseName("dir/file.ext", ".xyz") == "file.ext"); |
| assert(baseName("dir/filename", "name") == "file"); |
| assert(baseName("dir/subdir/") == "subdir"); |
| |
| version (Windows) |
| { |
| assert(baseName(`d:file.ext`) == "file.ext"); |
| assert(baseName(`d:\dir\file.ext`) == "file.ext"); |
| } |
| --- |
| |
| Note: |
| This function $(I only) strips away the specified suffix, which |
| doesn't necessarily have to represent an extension. |
| To remove the extension from a path, regardless of what the extension |
| is, use $(LREF stripExtension). |
| To obtain the filename without leading directories and without |
| an extension, combine the functions like this: |
| --- |
| assert(baseName(stripExtension("dir/file.ext")) == "file"); |
| --- |
| |
| Standards: |
| This function complies with |
| $(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html, |
| the POSIX requirements for the 'basename' shell utility) |
| (with suitable adaptations for Windows paths). |
| */ |
| auto baseName(R)(R path) |
| if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) && !isSomeString!R) |
| { |
| return _baseName(path); |
| } |
| |
| /// ditto |
| auto baseName(C)(C[] path) |
| if (isSomeChar!C) |
| { |
| return _baseName(path); |
| } |
| |
| private R _baseName(R)(R path) |
| if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || isNarrowString!R) |
| { |
| auto p1 = stripDrive(path); |
| if (p1.empty) |
| { |
| version (Windows) if (isUNC(path)) |
| return path[0 .. 1]; |
| static if (isSomeString!R) |
| return null; |
| else |
| return p1; // which is empty |
| } |
| |
| auto p2 = rtrimDirSeparators(p1); |
| if (p2.empty) return p1[0 .. 1]; |
| |
| return p2[lastSeparator(p2)+1 .. p2.length]; |
| } |
| |
| /// ditto |
| inout(C)[] baseName(CaseSensitive cs = CaseSensitive.osDefault, C, C1) |
| (inout(C)[] path, in C1[] suffix) |
| @safe pure //TODO: nothrow (because of filenameCmp()) |
| if (isSomeChar!C && isSomeChar!C1) |
| { |
| auto p = baseName(path); |
| if (p.length > suffix.length |
| && filenameCmp!cs(cast(const(C)[])p[$-suffix.length .. $], suffix) == 0) |
| { |
| return p[0 .. $-suffix.length]; |
| } |
| else return p; |
| } |
| |
| @safe unittest |
| { |
| assert(baseName("").empty); |
| assert(baseName("file.ext"w) == "file.ext"); |
| assert(baseName("file.ext"d, ".ext") == "file"); |
| assert(baseName("file", "file"w.dup) == "file"); |
| assert(baseName("dir/file.ext"d.dup) == "file.ext"); |
| assert(baseName("dir/file.ext", ".ext"d) == "file"); |
| assert(baseName("dir/file"w, "file"d) == "file"); |
| assert(baseName("dir///subdir////") == "subdir"); |
| assert(baseName("dir/subdir.ext/", ".ext") == "subdir"); |
| assert(baseName("dir/subdir/".dup, "subdir") == "subdir"); |
| assert(baseName("/"w.dup) == "/"); |
| assert(baseName("//"d.dup) == "/"); |
| assert(baseName("///") == "/"); |
| |
| assert(baseName!(CaseSensitive.yes)("file.ext", ".EXT") == "file.ext"); |
| assert(baseName!(CaseSensitive.no)("file.ext", ".EXT") == "file"); |
| |
| { |
| auto r = MockRange!(immutable(char))(`dir/file.ext`); |
| auto s = r.baseName(); |
| foreach (i, c; `file`) |
| assert(s[i] == c); |
| } |
| |
| version (Windows) |
| { |
| assert(baseName(`dir\file.ext`) == `file.ext`); |
| assert(baseName(`dir\file.ext`, `.ext`) == `file`); |
| assert(baseName(`dir\file`, `file`) == `file`); |
| assert(baseName(`d:file.ext`) == `file.ext`); |
| assert(baseName(`d:file.ext`, `.ext`) == `file`); |
| assert(baseName(`d:file`, `file`) == `file`); |
| assert(baseName(`dir\\subdir\\\`) == `subdir`); |
| assert(baseName(`dir\subdir.ext\`, `.ext`) == `subdir`); |
| assert(baseName(`dir\subdir\`, `subdir`) == `subdir`); |
| assert(baseName(`\`) == `\`); |
| assert(baseName(`\\`) == `\`); |
| assert(baseName(`\\\`) == `\`); |
| assert(baseName(`d:\`) == `\`); |
| assert(baseName(`d:`).empty); |
| assert(baseName(`\\server\share\file`) == `file`); |
| assert(baseName(`\\server\share\`) == `\`); |
| assert(baseName(`\\server\share`) == `\`); |
| |
| auto r = MockRange!(immutable(char))(`\\server\share`); |
| auto s = r.baseName(); |
| foreach (i, c; `\`) |
| assert(s[i] == c); |
| } |
| |
| assert(baseName(stripExtension("dir/file.ext")) == "file"); |
| |
| static assert(baseName("dir/file.ext") == "file.ext"); |
| static assert(baseName("dir/file.ext", ".ext") == "file"); |
| |
| static struct DirEntry { string s; alias s this; } |
| assert(baseName(DirEntry("dir/file.ext")) == "file.ext"); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!baseName("file")); |
| |
| enum S : string { a = "file/path/to/test" } |
| assert(S.a.baseName == "test"); |
| |
| char[S.a.length] sa = S.a[]; |
| assert(sa.baseName == "test"); |
| } |
| |
| /** Returns the directory part of a path. On Windows, this |
| includes the drive letter if present. |
| |
| Params: |
| path = A path name. |
| |
| Returns: |
| A slice of $(D path) or ".". |
| |
| Standards: |
| This function complies with |
| $(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/dirname.html, |
| the POSIX requirements for the 'dirname' shell utility) |
| (with suitable adaptations for Windows paths). |
| */ |
| auto dirName(R)(R path) |
| if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) && !isSomeString!R) |
| { |
| return _dirName(path); |
| } |
| |
| /// ditto |
| auto dirName(C)(C[] path) |
| if (isSomeChar!C) |
| { |
| return _dirName(path); |
| } |
| |
| private auto _dirName(R)(R path) |
| if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| static auto result(bool dot, typeof(path[0 .. 1]) p) |
| { |
| static if (isSomeString!R) |
| return dot ? "." : p; |
| else |
| { |
| import std.range : choose, only; |
| return choose(dot, only(cast(ElementEncodingType!R)'.'), p); |
| } |
| } |
| |
| if (path.empty) |
| return result(true, path[0 .. 0]); |
| |
| auto p = rtrimDirSeparators(path); |
| if (p.empty) |
| return result(false, path[0 .. 1]); |
| |
| version (Windows) |
| { |
| if (isUNC(p) && uncRootLength(p) == p.length) |
| return result(false, p); |
| |
| if (p.length == 2 && isDriveSeparator(p[1]) && path.length > 2) |
| return result(false, path[0 .. 3]); |
| } |
| |
| auto i = lastSeparator(p); |
| if (i == -1) |
| return result(true, p); |
| if (i == 0) |
| return result(false, p[0 .. 1]); |
| |
| version (Windows) |
| { |
| // If the directory part is either d: or d:\ |
| // do not chop off the last symbol. |
| if (isDriveSeparator(p[i]) || isDriveSeparator(p[i-1])) |
| return result(false, p[0 .. i+1]); |
| } |
| // Remove any remaining trailing (back)slashes. |
| return result(false, rtrimDirSeparators(p[0 .. i])); |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(dirName("") == "."); |
| assert(dirName("file"w) == "."); |
| assert(dirName("dir/"d) == "."); |
| assert(dirName("dir///") == "."); |
| assert(dirName("dir/file"w.dup) == "dir"); |
| assert(dirName("dir///file"d.dup) == "dir"); |
| assert(dirName("dir/subdir/") == "dir"); |
| assert(dirName("/dir/file"w) == "/dir"); |
| assert(dirName("/file"d) == "/"); |
| assert(dirName("/") == "/"); |
| assert(dirName("///") == "/"); |
| |
| version (Windows) |
| { |
| assert(dirName(`dir\`) == `.`); |
| assert(dirName(`dir\\\`) == `.`); |
| assert(dirName(`dir\file`) == `dir`); |
| assert(dirName(`dir\\\file`) == `dir`); |
| assert(dirName(`dir\subdir\`) == `dir`); |
| assert(dirName(`\dir\file`) == `\dir`); |
| assert(dirName(`\file`) == `\`); |
| assert(dirName(`\`) == `\`); |
| assert(dirName(`\\\`) == `\`); |
| assert(dirName(`d:`) == `d:`); |
| assert(dirName(`d:file`) == `d:`); |
| assert(dirName(`d:\`) == `d:\`); |
| assert(dirName(`d:\file`) == `d:\`); |
| assert(dirName(`d:\dir\file`) == `d:\dir`); |
| assert(dirName(`\\server\share\dir\file`) == `\\server\share\dir`); |
| assert(dirName(`\\server\share\file`) == `\\server\share`); |
| assert(dirName(`\\server\share\`) == `\\server\share`); |
| assert(dirName(`\\server\share`) == `\\server\share`); |
| } |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!dirName("file")); |
| |
| enum S : string { a = "file/path/to/test" } |
| assert(S.a.dirName == "file/path/to"); |
| |
| char[S.a.length] sa = S.a[]; |
| assert(sa.dirName == "file/path/to"); |
| } |
| |
| @system unittest |
| { |
| static assert(dirName("dir/file") == "dir"); |
| |
| import std.array; |
| import std.utf : byChar, byWchar, byDchar; |
| |
| assert(dirName("".byChar).array == "."); |
| assert(dirName("file"w.byWchar).array == "."w); |
| assert(dirName("dir/"d.byDchar).array == "."d); |
| assert(dirName("dir///".byChar).array == "."); |
| assert(dirName("dir/subdir/".byChar).array == "dir"); |
| assert(dirName("/dir/file"w.byWchar).array == "/dir"w); |
| assert(dirName("/file"d.byDchar).array == "/"d); |
| assert(dirName("/".byChar).array == "/"); |
| assert(dirName("///".byChar).array == "/"); |
| |
| version (Windows) |
| { |
| assert(dirName(`dir\`.byChar).array == `.`); |
| assert(dirName(`dir\\\`.byChar).array == `.`); |
| assert(dirName(`dir\file`.byChar).array == `dir`); |
| assert(dirName(`dir\\\file`.byChar).array == `dir`); |
| assert(dirName(`dir\subdir\`.byChar).array == `dir`); |
| assert(dirName(`\dir\file`.byChar).array == `\dir`); |
| assert(dirName(`\file`.byChar).array == `\`); |
| assert(dirName(`\`.byChar).array == `\`); |
| assert(dirName(`\\\`.byChar).array == `\`); |
| assert(dirName(`d:`.byChar).array == `d:`); |
| assert(dirName(`d:file`.byChar).array == `d:`); |
| assert(dirName(`d:\`.byChar).array == `d:\`); |
| assert(dirName(`d:\file`.byChar).array == `d:\`); |
| assert(dirName(`d:\dir\file`.byChar).array == `d:\dir`); |
| assert(dirName(`\\server\share\dir\file`.byChar).array == `\\server\share\dir`); |
| assert(dirName(`\\server\share\file`) == `\\server\share`); |
| assert(dirName(`\\server\share\`.byChar).array == `\\server\share`); |
| assert(dirName(`\\server\share`.byChar).array == `\\server\share`); |
| } |
| |
| //static assert(dirName("dir/file".byChar).array == "dir"); |
| } |
| |
| |
| |
| |
| /** Returns the root directory of the specified path, or $(D null) if the |
| path is not rooted. |
| |
| Params: |
| path = A path name. |
| |
| Returns: |
| A slice of $(D path). |
| */ |
| auto rootName(R)(R path) |
| if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) && |
| !isConvertibleToString!R) |
| { |
| if (path.empty) |
| goto Lnull; |
| |
| version (Posix) |
| { |
| if (isDirSeparator(path[0])) return path[0 .. 1]; |
| } |
| else version (Windows) |
| { |
| if (isDirSeparator(path[0])) |
| { |
| if (isUNC(path)) return path[0 .. uncRootLength(path)]; |
| else return path[0 .. 1]; |
| } |
| else if (path.length >= 3 && isDriveSeparator(path[1]) && |
| isDirSeparator(path[2])) |
| { |
| return path[0 .. 3]; |
| } |
| } |
| else static assert(0, "unsupported platform"); |
| |
| assert(!isRooted(path)); |
| Lnull: |
| static if (is(StringTypeOf!R)) |
| return null; // legacy code may rely on null return rather than slice |
| else |
| return path[0 .. 0]; |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(rootName("") is null); |
| assert(rootName("foo") is null); |
| assert(rootName("/") == "/"); |
| assert(rootName("/foo/bar") == "/"); |
| |
| version (Windows) |
| { |
| assert(rootName("d:foo") is null); |
| assert(rootName(`d:\foo`) == `d:\`); |
| assert(rootName(`\\server\share\foo`) == `\\server\share`); |
| assert(rootName(`\\server\share`) == `\\server\share`); |
| } |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!rootName("/foo/bar")); |
| } |
| |
| @safe unittest |
| { |
| import std.array; |
| import std.utf : byChar; |
| |
| assert(rootName("".byChar).array == ""); |
| assert(rootName("foo".byChar).array == ""); |
| assert(rootName("/".byChar).array == "/"); |
| assert(rootName("/foo/bar".byChar).array == "/"); |
| |
| version (Windows) |
| { |
| assert(rootName("d:foo".byChar).array == ""); |
| assert(rootName(`d:\foo`.byChar).array == `d:\`); |
| assert(rootName(`\\server\share\foo`.byChar).array == `\\server\share`); |
| assert(rootName(`\\server\share`.byChar).array == `\\server\share`); |
| } |
| } |
| |
| auto rootName(R)(R path) |
| if (isConvertibleToString!R) |
| { |
| return rootName!(StringTypeOf!R)(path); |
| } |
| |
| |
| /** |
| Get the drive portion of a path. |
| |
| Params: |
| path = string or range of characters |
| |
| Returns: |
| A slice of $(D _path) that is the drive, or an empty range if the drive |
| is not specified. In the case of UNC paths, the network share |
| is returned. |
| |
| Always returns an empty range on POSIX. |
| */ |
| auto driveName(R)(R path) |
| if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) && |
| !isConvertibleToString!R) |
| { |
| version (Windows) |
| { |
| if (hasDrive(path)) |
| return path[0 .. 2]; |
| else if (isUNC(path)) |
| return path[0 .. uncRootLength(path)]; |
| } |
| static if (isSomeString!R) |
| return cast(ElementEncodingType!R[]) null; // legacy code may rely on null return rather than slice |
| else |
| return path[0 .. 0]; |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.range : empty; |
| version (Posix) assert(driveName("c:/foo").empty); |
| version (Windows) |
| { |
| assert(driveName(`dir\file`).empty); |
| assert(driveName(`d:file`) == "d:"); |
| assert(driveName(`d:\file`) == "d:"); |
| assert(driveName("d:") == "d:"); |
| assert(driveName(`\\server\share\file`) == `\\server\share`); |
| assert(driveName(`\\server\share\`) == `\\server\share`); |
| assert(driveName(`\\server\share`) == `\\server\share`); |
| |
| static assert(driveName(`d:\file`) == "d:"); |
| } |
| } |
| |
| auto driveName(R)(auto ref R path) |
| if (isConvertibleToString!R) |
| { |
| return driveName!(StringTypeOf!R)(path); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!driveName(`d:\file`)); |
| } |
| |
| @safe unittest |
| { |
| import std.array; |
| import std.utf : byChar; |
| |
| version (Posix) assert(driveName("c:/foo".byChar).empty); |
| version (Windows) |
| { |
| assert(driveName(`dir\file`.byChar).empty); |
| assert(driveName(`d:file`.byChar).array == "d:"); |
| assert(driveName(`d:\file`.byChar).array == "d:"); |
| assert(driveName("d:".byChar).array == "d:"); |
| assert(driveName(`\\server\share\file`.byChar).array == `\\server\share`); |
| assert(driveName(`\\server\share\`.byChar).array == `\\server\share`); |
| assert(driveName(`\\server\share`.byChar).array == `\\server\share`); |
| |
| static assert(driveName(`d:\file`).array == "d:"); |
| } |
| } |
| |
| |
| /** Strips the drive from a Windows path. On POSIX, the path is returned |
| unaltered. |
| |
| Params: |
| path = A pathname |
| |
| Returns: A slice of path without the drive component. |
| */ |
| auto stripDrive(R)(R path) |
| if ((isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) && |
| !isConvertibleToString!R) |
| { |
| version (Windows) |
| { |
| if (hasDrive!(BaseOf!R)(path)) return path[2 .. path.length]; |
| else if (isUNC!(BaseOf!R)(path)) return path[uncRootLength!(BaseOf!R)(path) .. path.length]; |
| } |
| return path; |
| } |
| |
| /// |
| @safe unittest |
| { |
| version (Windows) |
| { |
| assert(stripDrive(`d:\dir\file`) == `\dir\file`); |
| assert(stripDrive(`\\server\share\dir\file`) == `\dir\file`); |
| } |
| } |
| |
| auto stripDrive(R)(auto ref R path) |
| if (isConvertibleToString!R) |
| { |
| return stripDrive!(StringTypeOf!R)(path); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!stripDrive(`d:\dir\file`)); |
| } |
| |
| @safe unittest |
| { |
| version (Windows) |
| { |
| assert(stripDrive(`d:\dir\file`) == `\dir\file`); |
| assert(stripDrive(`\\server\share\dir\file`) == `\dir\file`); |
| static assert(stripDrive(`d:\dir\file`) == `\dir\file`); |
| |
| auto r = MockRange!(immutable(char))(`d:\dir\file`); |
| auto s = r.stripDrive(); |
| foreach (i, c; `\dir\file`) |
| assert(s[i] == c); |
| } |
| version (Posix) |
| { |
| assert(stripDrive(`d:\dir\file`) == `d:\dir\file`); |
| |
| auto r = MockRange!(immutable(char))(`d:\dir\file`); |
| auto s = r.stripDrive(); |
| foreach (i, c; `d:\dir\file`) |
| assert(s[i] == c); |
| } |
| } |
| |
| |
| /* Helper function that returns the position of the filename/extension |
| separator dot in path. |
| |
| Params: |
| path = file spec as string or indexable range |
| Returns: |
| index of extension separator (the dot), or -1 if not found |
| */ |
| private ptrdiff_t extSeparatorPos(R)(const R path) |
| if (isRandomAccessRange!R && hasLength!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) |
| { |
| for (auto i = path.length; i-- > 0 && !isSeparator(path[i]); ) |
| { |
| if (path[i] == '.' && i > 0 && !isSeparator(path[i-1])) |
| return i; |
| } |
| return -1; |
| } |
| |
| @safe unittest |
| { |
| assert(extSeparatorPos("file") == -1); |
| assert(extSeparatorPos("file.ext"w) == 4); |
| assert(extSeparatorPos("file.ext1.ext2"d) == 9); |
| assert(extSeparatorPos(".foo".dup) == -1); |
| assert(extSeparatorPos(".foo.ext"w.dup) == 4); |
| } |
| |
| @safe unittest |
| { |
| assert(extSeparatorPos("dir/file"d.dup) == -1); |
| assert(extSeparatorPos("dir/file.ext") == 8); |
| assert(extSeparatorPos("dir/file.ext1.ext2"w) == 13); |
| assert(extSeparatorPos("dir/.foo"d) == -1); |
| assert(extSeparatorPos("dir/.foo.ext".dup) == 8); |
| |
| version (Windows) |
| { |
| assert(extSeparatorPos("dir\\file") == -1); |
| assert(extSeparatorPos("dir\\file.ext") == 8); |
| assert(extSeparatorPos("dir\\file.ext1.ext2") == 13); |
| assert(extSeparatorPos("dir\\.foo") == -1); |
| assert(extSeparatorPos("dir\\.foo.ext") == 8); |
| |
| assert(extSeparatorPos("d:file") == -1); |
| assert(extSeparatorPos("d:file.ext") == 6); |
| assert(extSeparatorPos("d:file.ext1.ext2") == 11); |
| assert(extSeparatorPos("d:.foo") == -1); |
| assert(extSeparatorPos("d:.foo.ext") == 6); |
| } |
| |
| static assert(extSeparatorPos("file") == -1); |
| static assert(extSeparatorPos("file.ext"w) == 4); |
| } |
| |
| |
| /** |
| Params: path = A path name. |
| Returns: The _extension part of a file name, including the dot. |
| |
| If there is no _extension, $(D null) is returned. |
| */ |
| auto extension(R)(R path) |
| if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || |
| is(StringTypeOf!R)) |
| { |
| auto i = extSeparatorPos!(BaseOf!R)(path); |
| if (i == -1) |
| { |
| static if (is(StringTypeOf!R)) |
| return StringTypeOf!R.init[]; // which is null |
| else |
| return path[0 .. 0]; |
| } |
| else return path[i .. path.length]; |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.range : empty; |
| assert(extension("file").empty); |
| assert(extension("file.") == "."); |
| assert(extension("file.ext"w) == ".ext"); |
| assert(extension("file.ext1.ext2"d) == ".ext2"); |
| assert(extension(".foo".dup).empty); |
| assert(extension(".foo.ext"w.dup) == ".ext"); |
| |
| static assert(extension("file").empty); |
| static assert(extension("file.ext") == ".ext"); |
| } |
| |
| @safe unittest |
| { |
| { |
| auto r = MockRange!(immutable(char))(`file.ext1.ext2`); |
| auto s = r.extension(); |
| foreach (i, c; `.ext2`) |
| assert(s[i] == c); |
| } |
| |
| static struct DirEntry { string s; alias s this; } |
| assert(extension(DirEntry("file")).empty); |
| } |
| |
| |
| /** Remove extension from path. |
| |
| Params: |
| path = string or range to be sliced |
| |
| Returns: |
| slice of path with the extension (if any) stripped off |
| */ |
| auto stripExtension(R)(R path) |
| if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) && |
| !isConvertibleToString!R) |
| { |
| auto i = extSeparatorPos(path); |
| return (i == -1) ? path : path[0 .. i]; |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(stripExtension("file") == "file"); |
| assert(stripExtension("file.ext") == "file"); |
| assert(stripExtension("file.ext1.ext2") == "file.ext1"); |
| assert(stripExtension("file.") == "file"); |
| assert(stripExtension(".file") == ".file"); |
| assert(stripExtension(".file.ext") == ".file"); |
| assert(stripExtension("dir/file.ext") == "dir/file"); |
| } |
| |
| auto stripExtension(R)(auto ref R path) |
| if (isConvertibleToString!R) |
| { |
| return stripExtension!(StringTypeOf!R)(path); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!stripExtension("file")); |
| } |
| |
| @safe unittest |
| { |
| assert(stripExtension("file.ext"w) == "file"); |
| assert(stripExtension("file.ext1.ext2"d) == "file.ext1"); |
| |
| import std.array; |
| import std.utf : byChar, byWchar, byDchar; |
| |
| assert(stripExtension("file".byChar).array == "file"); |
| assert(stripExtension("file.ext"w.byWchar).array == "file"); |
| assert(stripExtension("file.ext1.ext2"d.byDchar).array == "file.ext1"); |
| } |
| |
| |
| /** Sets or replaces an extension. |
| |
| If the filename already has an extension, it is replaced. If not, the |
| extension is simply appended to the filename. Including a leading dot |
| in $(D ext) is optional. |
| |
| If the extension is empty, this function is equivalent to |
| $(LREF stripExtension). |
| |
| This function normally allocates a new string (the possible exception |
| being the case when path is immutable and doesn't already have an |
| extension). |
| |
| Params: |
| path = A path name |
| ext = The new extension |
| |
| Returns: A string containing the _path given by $(D path), but where |
| the extension has been set to $(D ext). |
| |
| See_Also: |
| $(LREF withExtension) which does not allocate and returns a lazy range. |
| */ |
| immutable(Unqual!C1)[] setExtension(C1, C2)(in C1[] path, in C2[] ext) |
| if (isSomeChar!C1 && !is(C1 == immutable) && is(Unqual!C1 == Unqual!C2)) |
| { |
| try |
| { |
| import std.conv : to; |
| return withExtension(path, ext).to!(typeof(return)); |
| } |
| catch (Exception e) |
| { |
| assert(0); |
| } |
| } |
| |
| ///ditto |
| immutable(C1)[] setExtension(C1, C2)(immutable(C1)[] path, const(C2)[] ext) |
| if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2)) |
| { |
| if (ext.length == 0) |
| return stripExtension(path); |
| |
| try |
| { |
| import std.conv : to; |
| return withExtension(path, ext).to!(typeof(return)); |
| } |
| catch (Exception e) |
| { |
| assert(0); |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(setExtension("file", "ext") == "file.ext"); |
| assert(setExtension("file"w, ".ext"w) == "file.ext"); |
| assert(setExtension("file."d, "ext"d) == "file.ext"); |
| assert(setExtension("file.", ".ext") == "file.ext"); |
| assert(setExtension("file.old"w, "new"w) == "file.new"); |
| assert(setExtension("file.old"d, ".new"d) == "file.new"); |
| } |
| |
| @safe unittest |
| { |
| assert(setExtension("file"w.dup, "ext"w) == "file.ext"); |
| assert(setExtension("file"w.dup, ".ext"w) == "file.ext"); |
| assert(setExtension("file."w, "ext"w.dup) == "file.ext"); |
| assert(setExtension("file."w, ".ext"w.dup) == "file.ext"); |
| assert(setExtension("file.old"d.dup, "new"d) == "file.new"); |
| assert(setExtension("file.old"d.dup, ".new"d) == "file.new"); |
| |
| static assert(setExtension("file", "ext") == "file.ext"); |
| static assert(setExtension("file.old", "new") == "file.new"); |
| |
| static assert(setExtension("file"w.dup, "ext"w) == "file.ext"); |
| static assert(setExtension("file.old"d.dup, "new"d) == "file.new"); |
| |
| // Issue 10601 |
| assert(setExtension("file", "") == "file"); |
| assert(setExtension("file.ext", "") == "file"); |
| } |
| |
| /************ |
| * Replace existing extension on filespec with new one. |
| * |
| * Params: |
| * path = string or random access range representing a filespec |
| * ext = the new extension |
| * Returns: |
| * Range with $(D path)'s extension (if any) replaced with $(D ext). |
| * The element encoding type of the returned range will be the same as $(D path)'s. |
| * See_Also: |
| * $(LREF setExtension) |
| */ |
| auto withExtension(R, C)(R path, C[] ext) |
| if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) && |
| !isConvertibleToString!R && |
| isSomeChar!C) |
| { |
| import std.range : only, chain; |
| import std.utf : byUTF; |
| |
| alias CR = Unqual!(ElementEncodingType!R); |
| auto dot = only(CR('.')); |
| if (ext.length == 0 || ext[0] == '.') |
| dot.popFront(); // so dot is an empty range, too |
| return chain(stripExtension(path).byUTF!CR, dot, ext.byUTF!CR); |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.array; |
| assert(withExtension("file", "ext").array == "file.ext"); |
| assert(withExtension("file"w, ".ext"w).array == "file.ext"); |
| assert(withExtension("file.ext"w, ".").array == "file."); |
| |
| import std.utf : byChar, byWchar; |
| assert(withExtension("file".byChar, "ext").array == "file.ext"); |
| assert(withExtension("file"w.byWchar, ".ext"w).array == "file.ext"w); |
| assert(withExtension("file.ext"w.byWchar, ".").array == "file."w); |
| } |
| |
| auto withExtension(R, C)(auto ref R path, C[] ext) |
| if (isConvertibleToString!R) |
| { |
| return withExtension!(StringTypeOf!R)(path, ext); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!withExtension("file", "ext")); |
| } |
| |
| /** Params: |
| path = A path name. |
| ext = The default extension to use. |
| |
| Returns: The _path given by $(D path), with the extension given by $(D ext) |
| appended if the path doesn't already have one. |
| |
| Including the dot in the extension is optional. |
| |
| This function always allocates a new string, except in the case when |
| path is immutable and already has an extension. |
| */ |
| immutable(Unqual!C1)[] defaultExtension(C1, C2)(in C1[] path, in C2[] ext) |
| if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2)) |
| { |
| import std.conv : to; |
| return withDefaultExtension(path, ext).to!(typeof(return)); |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(defaultExtension("file", "ext") == "file.ext"); |
| assert(defaultExtension("file", ".ext") == "file.ext"); |
| assert(defaultExtension("file.", "ext") == "file."); |
| assert(defaultExtension("file.old", "new") == "file.old"); |
| assert(defaultExtension("file.old", ".new") == "file.old"); |
| } |
| |
| @safe unittest |
| { |
| assert(defaultExtension("file"w.dup, "ext"w) == "file.ext"); |
| assert(defaultExtension("file.old"d.dup, "new"d) == "file.old"); |
| |
| static assert(defaultExtension("file", "ext") == "file.ext"); |
| static assert(defaultExtension("file.old", "new") == "file.old"); |
| |
| static assert(defaultExtension("file"w.dup, "ext"w) == "file.ext"); |
| static assert(defaultExtension("file.old"d.dup, "new"d) == "file.old"); |
| } |
| |
| |
| /******************************** |
| * Set the extension of $(D path) to $(D ext) if $(D path) doesn't have one. |
| * |
| * Params: |
| * path = filespec as string or range |
| * ext = extension, may have leading '.' |
| * Returns: |
| * range with the result |
| */ |
| auto withDefaultExtension(R, C)(R path, C[] ext) |
| if ((isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) && |
| !isConvertibleToString!R && |
| isSomeChar!C) |
| { |
| import std.range : only, chain; |
| import std.utf : byUTF; |
| |
| alias CR = Unqual!(ElementEncodingType!R); |
| auto dot = only(CR('.')); |
| auto i = extSeparatorPos(path); |
| if (i == -1) |
| { |
| if (ext.length > 0 && ext[0] == '.') |
| ext = ext[1 .. $]; // remove any leading . from ext[] |
| } |
| else |
| { |
| // path already has an extension, so make these empty |
| ext = ext[0 .. 0]; |
| dot.popFront(); |
| } |
| return chain(path.byUTF!CR, dot, ext.byUTF!CR); |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.array; |
| assert(withDefaultExtension("file", "ext").array == "file.ext"); |
| assert(withDefaultExtension("file"w, ".ext").array == "file.ext"w); |
| assert(withDefaultExtension("file.", "ext").array == "file."); |
| assert(withDefaultExtension("file", "").array == "file."); |
| |
| import std.utf : byChar, byWchar; |
| assert(withDefaultExtension("file".byChar, "ext").array == "file.ext"); |
| assert(withDefaultExtension("file"w.byWchar, ".ext").array == "file.ext"w); |
| assert(withDefaultExtension("file.".byChar, "ext"d).array == "file."); |
| assert(withDefaultExtension("file".byChar, "").array == "file."); |
| } |
| |
| auto withDefaultExtension(R, C)(auto ref R path, C[] ext) |
| if (isConvertibleToString!R) |
| { |
| return withDefaultExtension!(StringTypeOf!R, C)(path, ext); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!withDefaultExtension("file", "ext")); |
| } |
| |
| /** Combines one or more path segments. |
| |
| This function takes a set of path segments, given as an input |
| range of string elements or as a set of string arguments, |
| and concatenates them with each other. Directory separators |
| are inserted between segments if necessary. If any of the |
| path segments are absolute (as defined by $(LREF isAbsolute)), the |
| preceding segments will be dropped. |
| |
| On Windows, if one of the path segments are rooted, but not absolute |
| (e.g. $(D `\foo`)), all preceding path segments down to the previous |
| root will be dropped. (See below for an example.) |
| |
| This function always allocates memory to hold the resulting path. |
| The variadic overload is guaranteed to only perform a single |
| allocation, as is the range version if $(D paths) is a forward |
| range. |
| |
| Params: |
| segments = An input range of segments to assemble the path from. |
| Returns: The assembled path. |
| */ |
| immutable(ElementEncodingType!(ElementType!Range))[] |
| buildPath(Range)(Range segments) |
| if (isInputRange!Range && !isInfinite!Range && isSomeString!(ElementType!Range)) |
| { |
| if (segments.empty) return null; |
| |
| // If this is a forward range, we can pre-calculate a maximum length. |
| static if (isForwardRange!Range) |
| { |
| auto segments2 = segments.save; |
| size_t precalc = 0; |
| foreach (segment; segments2) precalc += segment.length + 1; |
| } |
| // Otherwise, just venture a guess and resize later if necessary. |
| else size_t precalc = 255; |
| |
| auto buf = new Unqual!(ElementEncodingType!(ElementType!Range))[](precalc); |
| size_t pos = 0; |
| foreach (segment; segments) |
| { |
| if (segment.empty) continue; |
| static if (!isForwardRange!Range) |
| { |
| immutable neededLength = pos + segment.length + 1; |
| if (buf.length < neededLength) |
| buf.length = reserve(buf, neededLength + buf.length/2); |
| } |
| auto r = chainPath(buf[0 .. pos], segment); |
| size_t i; |
| foreach (c; r) |
| { |
| buf[i] = c; |
| ++i; |
| } |
| pos = i; |
| } |
| static U trustedCast(U, V)(V v) @trusted pure nothrow { return cast(U) v; } |
| return trustedCast!(typeof(return))(buf[0 .. pos]); |
| } |
| |
| /// ditto |
| immutable(C)[] buildPath(C)(const(C)[][] paths...) |
| @safe pure nothrow |
| if (isSomeChar!C) |
| { |
| return buildPath!(typeof(paths))(paths); |
| } |
| |
| /// |
| @safe unittest |
| { |
| version (Posix) |
| { |
| assert(buildPath("foo", "bar", "baz") == "foo/bar/baz"); |
| assert(buildPath("/foo/", "bar/baz") == "/foo/bar/baz"); |
| assert(buildPath("/foo", "/bar") == "/bar"); |
| } |
| |
| version (Windows) |
| { |
| assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`); |
| assert(buildPath(`c:\foo`, `bar\baz`) == `c:\foo\bar\baz`); |
| assert(buildPath("foo", `d:\bar`) == `d:\bar`); |
| assert(buildPath("foo", `\bar`) == `\bar`); |
| assert(buildPath(`c:\foo`, `\bar`) == `c:\bar`); |
| } |
| } |
| |
| @system unittest // non-documented |
| { |
| import std.range; |
| // ir() wraps an array in a plain (i.e. non-forward) input range, so that |
| // we can test both code paths |
| InputRange!(C[]) ir(C)(C[][] p...) { return inputRangeObject(p); } |
| version (Posix) |
| { |
| assert(buildPath("foo") == "foo"); |
| assert(buildPath("/foo/") == "/foo/"); |
| assert(buildPath("foo", "bar") == "foo/bar"); |
| assert(buildPath("foo", "bar", "baz") == "foo/bar/baz"); |
| assert(buildPath("foo/".dup, "bar") == "foo/bar"); |
| assert(buildPath("foo///", "bar".dup) == "foo///bar"); |
| assert(buildPath("/foo"w, "bar"w) == "/foo/bar"); |
| assert(buildPath("foo"w.dup, "/bar"w) == "/bar"); |
| assert(buildPath("foo"w, "bar/"w.dup) == "foo/bar/"); |
| assert(buildPath("/"d, "foo"d) == "/foo"); |
| assert(buildPath(""d.dup, "foo"d) == "foo"); |
| assert(buildPath("foo"d, ""d.dup) == "foo"); |
| assert(buildPath("foo", "bar".dup, "baz") == "foo/bar/baz"); |
| assert(buildPath("foo"w, "/bar"w, "baz"w.dup) == "/bar/baz"); |
| |
| static assert(buildPath("foo", "bar", "baz") == "foo/bar/baz"); |
| static assert(buildPath("foo", "/bar", "baz") == "/bar/baz"); |
| |
| // The following are mostly duplicates of the above, except that the |
| // range version does not accept mixed constness. |
| assert(buildPath(ir("foo")) == "foo"); |
| assert(buildPath(ir("/foo/")) == "/foo/"); |
| assert(buildPath(ir("foo", "bar")) == "foo/bar"); |
| assert(buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz"); |
| assert(buildPath(ir("foo/".dup, "bar".dup)) == "foo/bar"); |
| assert(buildPath(ir("foo///".dup, "bar".dup)) == "foo///bar"); |
| assert(buildPath(ir("/foo"w, "bar"w)) == "/foo/bar"); |
| assert(buildPath(ir("foo"w.dup, "/bar"w.dup)) == "/bar"); |
| assert(buildPath(ir("foo"w.dup, "bar/"w.dup)) == "foo/bar/"); |
| assert(buildPath(ir("/"d, "foo"d)) == "/foo"); |
| assert(buildPath(ir(""d.dup, "foo"d.dup)) == "foo"); |
| assert(buildPath(ir("foo"d, ""d)) == "foo"); |
| assert(buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz"); |
| assert(buildPath(ir("foo"w.dup, "/bar"w.dup, "baz"w.dup)) == "/bar/baz"); |
| } |
| version (Windows) |
| { |
| assert(buildPath("foo") == "foo"); |
| assert(buildPath(`\foo/`) == `\foo/`); |
| assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`); |
| assert(buildPath("foo", `\bar`) == `\bar`); |
| assert(buildPath(`c:\foo`, "bar") == `c:\foo\bar`); |
| assert(buildPath("foo"w, `d:\bar`w.dup) == `d:\bar`); |
| assert(buildPath(`c:\foo\bar`, `\baz`) == `c:\baz`); |
| assert(buildPath(`\\foo\bar\baz`d, `foo`d, `\bar`d) == `\\foo\bar\bar`d); |
| |
| static assert(buildPath("foo", "bar", "baz") == `foo\bar\baz`); |
| static assert(buildPath("foo", `c:\bar`, "baz") == `c:\bar\baz`); |
| |
| assert(buildPath(ir("foo")) == "foo"); |
| assert(buildPath(ir(`\foo/`)) == `\foo/`); |
| assert(buildPath(ir("foo", "bar", "baz")) == `foo\bar\baz`); |
| assert(buildPath(ir("foo", `\bar`)) == `\bar`); |
| assert(buildPath(ir(`c:\foo`, "bar")) == `c:\foo\bar`); |
| assert(buildPath(ir("foo"w.dup, `d:\bar`w.dup)) == `d:\bar`); |
| assert(buildPath(ir(`c:\foo\bar`, `\baz`)) == `c:\baz`); |
| assert(buildPath(ir(`\\foo\bar\baz`d, `foo`d, `\bar`d)) == `\\foo\bar\bar`d); |
| } |
| |
| // Test that allocation works as it should. |
| auto manyShort = "aaa".repeat(1000).array(); |
| auto manyShortCombined = join(manyShort, dirSeparator); |
| assert(buildPath(manyShort) == manyShortCombined); |
| assert(buildPath(ir(manyShort)) == manyShortCombined); |
| |
| auto fewLong = 'b'.repeat(500).array().repeat(10).array(); |
| auto fewLongCombined = join(fewLong, dirSeparator); |
| assert(buildPath(fewLong) == fewLongCombined); |
| assert(buildPath(ir(fewLong)) == fewLongCombined); |
| } |
| |
| @safe unittest |
| { |
| // Test for issue 7397 |
| string[] ary = ["a", "b"]; |
| version (Posix) |
| { |
| assert(buildPath(ary) == "a/b"); |
| } |
| else version (Windows) |
| { |
| assert(buildPath(ary) == `a\b`); |
| } |
| } |
| |
| |
| /** |
| * Concatenate path segments together to form one path. |
| * |
| * Params: |
| * r1 = first segment |
| * r2 = second segment |
| * ranges = 0 or more segments |
| * Returns: |
| * Lazy range which is the concatenation of r1, r2 and ranges with path separators. |
| * The resulting element type is that of r1. |
| * See_Also: |
| * $(LREF buildPath) |
| */ |
| auto chainPath(R1, R2, Ranges...)(R1 r1, R2 r2, Ranges ranges) |
| if ((isRandomAccessRange!R1 && hasSlicing!R1 && hasLength!R1 && isSomeChar!(ElementType!R1) || |
| isNarrowString!R1 && |
| !isConvertibleToString!R1) && |
| (isRandomAccessRange!R2 && hasSlicing!R2 && hasLength!R2 && isSomeChar!(ElementType!R2) || |
| isNarrowString!R2 && |
| !isConvertibleToString!R2) && |
| (Ranges.length == 0 || is(typeof(chainPath(r2, ranges)))) |
| ) |
| { |
| static if (Ranges.length) |
| { |
| return chainPath(chainPath(r1, r2), ranges); |
| } |
| else |
| { |
| import std.range : only, chain; |
| import std.utf : byUTF; |
| |
| alias CR = Unqual!(ElementEncodingType!R1); |
| auto sep = only(CR(dirSeparator[0])); |
| bool usesep = false; |
| |
| auto pos = r1.length; |
| |
| if (pos) |
| { |
| if (isRooted(r2)) |
| { |
| version (Posix) |
| { |
| pos = 0; |
| } |
| else version (Windows) |
| { |
| if (isAbsolute(r2)) |
| pos = 0; |
| else |
| { |
| pos = rootName(r1).length; |
| if (pos > 0 && isDirSeparator(r1[pos - 1])) |
| --pos; |
| } |
| } |
| else |
| static assert(0); |
| } |
| else if (!isDirSeparator(r1[pos - 1])) |
| usesep = true; |
| } |
| if (!usesep) |
| sep.popFront(); |
| // Return r1 ~ '/' ~ r2 |
| return chain(r1[0 .. pos].byUTF!CR, sep, r2.byUTF!CR); |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.array; |
| version (Posix) |
| { |
| assert(chainPath("foo", "bar", "baz").array == "foo/bar/baz"); |
| assert(chainPath("/foo/", "bar/baz").array == "/foo/bar/baz"); |
| assert(chainPath("/foo", "/bar").array == "/bar"); |
| } |
| |
| version (Windows) |
| { |
| assert(chainPath("foo", "bar", "baz").array == `foo\bar\baz`); |
| assert(chainPath(`c:\foo`, `bar\baz`).array == `c:\foo\bar\baz`); |
| assert(chainPath("foo", `d:\bar`).array == `d:\bar`); |
| assert(chainPath("foo", `\bar`).array == `\bar`); |
| assert(chainPath(`c:\foo`, `\bar`).array == `c:\bar`); |
| } |
| |
| import std.utf : byChar; |
| version (Posix) |
| { |
| assert(chainPath("foo", "bar", "baz").array == "foo/bar/baz"); |
| assert(chainPath("/foo/".byChar, "bar/baz").array == "/foo/bar/baz"); |
| assert(chainPath("/foo", "/bar".byChar).array == "/bar"); |
| } |
| |
| version (Windows) |
| { |
| assert(chainPath("foo", "bar", "baz").array == `foo\bar\baz`); |
| assert(chainPath(`c:\foo`.byChar, `bar\baz`).array == `c:\foo\bar\baz`); |
| assert(chainPath("foo", `d:\bar`).array == `d:\bar`); |
| assert(chainPath("foo", `\bar`.byChar).array == `\bar`); |
| assert(chainPath(`c:\foo`, `\bar`w).array == `c:\bar`); |
| } |
| } |
| |
| auto chainPath(Ranges...)(auto ref Ranges ranges) |
| if (Ranges.length >= 2 && |
| std.meta.anySatisfy!(isConvertibleToString, Ranges)) |
| { |
| import std.meta : staticMap; |
| alias Types = staticMap!(convertToString, Ranges); |
| return chainPath!Types(ranges); |
| } |
| |
| @safe unittest |
| { |
| assert(chainPath(TestAliasedString(null), TestAliasedString(null), TestAliasedString(null)).empty); |
| assert(chainPath(TestAliasedString(null), TestAliasedString(null), "").empty); |
| assert(chainPath(TestAliasedString(null), "", TestAliasedString(null)).empty); |
| static struct S { string s; } |
| static assert(!__traits(compiles, chainPath(TestAliasedString(null), S(""), TestAliasedString(null)))); |
| } |
| |
| /** Performs the same task as $(LREF buildPath), |
| while at the same time resolving current/parent directory |
| symbols ($(D ".") and $(D "..")) and removing superfluous |
| directory separators. |
| It will return "." if the path leads to the starting directory. |
| On Windows, slashes are replaced with backslashes. |
| |
| Using buildNormalizedPath on null paths will always return null. |
| |
| Note that this function does not resolve symbolic links. |
| |
| This function always allocates memory to hold the resulting path. |
| Use $(LREF asNormalizedPath) to not allocate memory. |
| |
| Params: |
| paths = An array of paths to assemble. |
| |
| Returns: The assembled path. |
| */ |
| immutable(C)[] buildNormalizedPath(C)(const(C[])[] paths...) |
| @trusted pure nothrow |
| if (isSomeChar!C) |
| { |
| import std.array : array; |
| |
| const(C)[] result; |
| foreach (path; paths) |
| { |
| if (result) |
| result = chainPath(result, path).array; |
| else |
| result = path; |
| } |
| result = asNormalizedPath(result).array; |
| return cast(typeof(return)) result; |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(buildNormalizedPath("foo", "..") == "."); |
| |
| version (Posix) |
| { |
| assert(buildNormalizedPath("/foo/./bar/..//baz/") == "/foo/baz"); |
| assert(buildNormalizedPath("../foo/.") == "../foo"); |
| assert(buildNormalizedPath("/foo", "bar/baz/") == "/foo/bar/baz"); |
| assert(buildNormalizedPath("/foo", "/bar/..", "baz") == "/baz"); |
| assert(buildNormalizedPath("foo/./bar", "../../", "../baz") == "../baz"); |
| assert(buildNormalizedPath("/foo/./bar", "../../baz") == "/baz"); |
| } |
| |
| version (Windows) |
| { |
| assert(buildNormalizedPath(`c:\foo\.\bar/..\\baz\`) == `c:\foo\baz`); |
| assert(buildNormalizedPath(`..\foo\.`) == `..\foo`); |
| assert(buildNormalizedPath(`c:\foo`, `bar\baz\`) == `c:\foo\bar\baz`); |
| assert(buildNormalizedPath(`c:\foo`, `bar/..`) == `c:\foo`); |
| assert(buildNormalizedPath(`\\server\share\foo`, `..\bar`) == |
| `\\server\share\bar`); |
| } |
| } |
| |
| @safe unittest |
| { |
| assert(buildNormalizedPath(".", ".") == "."); |
| assert(buildNormalizedPath("foo", "..") == "."); |
| assert(buildNormalizedPath("", "") is null); |
| assert(buildNormalizedPath("", ".") == "."); |
| assert(buildNormalizedPath(".", "") == "."); |
| assert(buildNormalizedPath(null, "foo") == "foo"); |
| assert(buildNormalizedPath("", "foo") == "foo"); |
| assert(buildNormalizedPath("", "") == ""); |
| assert(buildNormalizedPath("", null) == ""); |
| assert(buildNormalizedPath(null, "") == ""); |
| assert(buildNormalizedPath!(char)(null, null) == ""); |
| |
| version (Posix) |
| { |
| assert(buildNormalizedPath("/", "foo", "bar") == "/foo/bar"); |
| assert(buildNormalizedPath("foo", "bar", "baz") == "foo/bar/baz"); |
| assert(buildNormalizedPath("foo", "bar/baz") == "foo/bar/baz"); |
| assert(buildNormalizedPath("foo", "bar//baz///") == "foo/bar/baz"); |
| assert(buildNormalizedPath("/foo", "bar/baz") == "/foo/bar/baz"); |
| assert(buildNormalizedPath("/foo", "/bar/baz") == "/bar/baz"); |
| assert(buildNormalizedPath("/foo/..", "/bar/./baz") == "/bar/baz"); |
| assert(buildNormalizedPath("/foo/..", "bar/baz") == "/bar/baz"); |
| assert(buildNormalizedPath("/foo/../../", "bar/baz") == "/bar/baz"); |
| assert(buildNormalizedPath("/foo/bar", "../baz") == "/foo/baz"); |
| assert(buildNormalizedPath("/foo/bar", "../../baz") == "/baz"); |
| assert(buildNormalizedPath("/foo/bar", ".././/baz/..", "wee/") == "/foo/wee"); |
| assert(buildNormalizedPath("//foo/bar", "baz///wee") == "/foo/bar/baz/wee"); |
| static assert(buildNormalizedPath("/foo/..", "/bar/./baz") == "/bar/baz"); |
| } |
| else version (Windows) |
| { |
| assert(buildNormalizedPath(`\`, `foo`, `bar`) == `\foo\bar`); |
| assert(buildNormalizedPath(`foo`, `bar`, `baz`) == `foo\bar\baz`); |
| assert(buildNormalizedPath(`foo`, `bar\baz`) == `foo\bar\baz`); |
| assert(buildNormalizedPath(`foo`, `bar\\baz\\\`) == `foo\bar\baz`); |
| assert(buildNormalizedPath(`\foo`, `bar\baz`) == `\foo\bar\baz`); |
| assert(buildNormalizedPath(`\foo`, `\bar\baz`) == `\bar\baz`); |
| assert(buildNormalizedPath(`\foo\..`, `\bar\.\baz`) == `\bar\baz`); |
| assert(buildNormalizedPath(`\foo\..`, `bar\baz`) == `\bar\baz`); |
| assert(buildNormalizedPath(`\foo\..\..\`, `bar\baz`) == `\bar\baz`); |
| assert(buildNormalizedPath(`\foo\bar`, `..\baz`) == `\foo\baz`); |
| assert(buildNormalizedPath(`\foo\bar`, `../../baz`) == `\baz`); |
| assert(buildNormalizedPath(`\foo\bar`, `..\.\/baz\..`, `wee\`) == `\foo\wee`); |
| |
| assert(buildNormalizedPath(`c:\`, `foo`, `bar`) == `c:\foo\bar`); |
| assert(buildNormalizedPath(`c:foo`, `bar`, `baz`) == `c:foo\bar\baz`); |
| assert(buildNormalizedPath(`c:foo`, `bar\baz`) == `c:foo\bar\baz`); |
| assert(buildNormalizedPath(`c:foo`, `bar\\baz\\\`) == `c:foo\bar\baz`); |
| assert(buildNormalizedPath(`c:\foo`, `bar\baz`) == `c:\foo\bar\baz`); |
| assert(buildNormalizedPath(`c:\foo`, `\bar\baz`) == `c:\bar\baz`); |
| assert(buildNormalizedPath(`c:\foo\..`, `\bar\.\baz`) == `c:\bar\baz`); |
| assert(buildNormalizedPath(`c:\foo\..`, `bar\baz`) == `c:\bar\baz`); |
| assert(buildNormalizedPath(`c:\foo\..\..\`, `bar\baz`) == `c:\bar\baz`); |
| assert(buildNormalizedPath(`c:\foo\bar`, `..\baz`) == `c:\foo\baz`); |
| assert(buildNormalizedPath(`c:\foo\bar`, `..\..\baz`) == `c:\baz`); |
| assert(buildNormalizedPath(`c:\foo\bar`, `..\.\\baz\..`, `wee\`) == `c:\foo\wee`); |
| |
| assert(buildNormalizedPath(`\\server\share`, `foo`, `bar`) == `\\server\share\foo\bar`); |
| assert(buildNormalizedPath(`\\server\share\`, `foo`, `bar`) == `\\server\share\foo\bar`); |
| assert(buildNormalizedPath(`\\server\share\foo`, `bar\baz`) == `\\server\share\foo\bar\baz`); |
| assert(buildNormalizedPath(`\\server\share\foo`, `\bar\baz`) == `\\server\share\bar\baz`); |
| assert(buildNormalizedPath(`\\server\share\foo\..`, `\bar\.\baz`) == `\\server\share\bar\baz`); |
| assert(buildNormalizedPath(`\\server\share\foo\..`, `bar\baz`) == `\\server\share\bar\baz`); |
| assert(buildNormalizedPath(`\\server\share\foo\..\..\`, `bar\baz`) == `\\server\share\bar\baz`); |
| assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\baz`) == `\\server\share\foo\baz`); |
| assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\..\baz`) == `\\server\share\baz`); |
| assert(buildNormalizedPath(`\\server\share\foo\bar`, `..\.\\baz\..`, `wee\`) == `\\server\share\foo\wee`); |
| |
| static assert(buildNormalizedPath(`\foo\..\..\`, `bar\baz`) == `\bar\baz`); |
| } |
| else static assert(0); |
| } |
| |
| @safe unittest |
| { |
| // Test for issue 7397 |
| string[] ary = ["a", "b"]; |
| version (Posix) |
| { |
| assert(buildNormalizedPath(ary) == "a/b"); |
| } |
| else version (Windows) |
| { |
| assert(buildNormalizedPath(ary) == `a\b`); |
| } |
| } |
| |
| |
| /** Normalize a path by resolving current/parent directory |
| symbols ($(D ".") and $(D "..")) and removing superfluous |
| directory separators. |
| It will return "." if the path leads to the starting directory. |
| On Windows, slashes are replaced with backslashes. |
| |
| Using asNormalizedPath on empty paths will always return an empty path. |
| |
| Does not resolve symbolic links. |
| |
| This function always allocates memory to hold the resulting path. |
| Use $(LREF buildNormalizedPath) to allocate memory and return a string. |
| |
| Params: |
| path = string or random access range representing the _path to normalize |
| |
| Returns: |
| normalized path as a forward range |
| */ |
| |
| auto asNormalizedPath(R)(R path) |
| if (isSomeChar!(ElementEncodingType!R) && |
| (isRandomAccessRange!R && hasSlicing!R && hasLength!R || isNarrowString!R) && |
| !isConvertibleToString!R) |
| { |
| alias C = Unqual!(ElementEncodingType!R); |
| alias S = typeof(path[0 .. 0]); |
| |
| static struct Result |
| { |
| @property bool empty() |
| { |
| return c == c.init; |
| } |
| |
| @property C front() |
| { |
| return c; |
| } |
| |
| void popFront() |
| { |
| C lastc = c; |
| c = c.init; |
| if (!element.empty) |
| { |
| c = getElement0(); |
| return; |
| } |
| L1: |
| while (1) |
| { |
| if (elements.empty) |
| { |
| element = element[0 .. 0]; |
| return; |
| } |
| element = elements.front; |
| elements.popFront(); |
| if (isDot(element) || (rooted && isDotDot(element))) |
| continue; |
| |
| if (rooted || !isDotDot(element)) |
| { |
| int n = 1; |
| auto elements2 = elements.save; |
| while (!elements2.empty) |
| { |
| auto e = elements2.front; |
| elements2.popFront(); |
| if (isDot(e)) |
| continue; |
| if (isDotDot(e)) |
| { |
| --n; |
| if (n == 0) |
| { |
| elements = elements2; |
| element = element[0 .. 0]; |
| continue L1; |
| } |
| } |
| else |
| ++n; |
| } |
| } |
| break; |
| } |
| |
| static assert(dirSeparator.length == 1); |
| if (lastc == dirSeparator[0] || lastc == lastc.init) |
| c = getElement0(); |
| else |
| c = dirSeparator[0]; |
| } |
| |
| static if (isForwardRange!R) |
| { |
| @property auto save() |
| { |
| auto result = this; |
| result.element = element.save; |
| result.elements = elements.save; |
| return result; |
| } |
| } |
| |
| private: |
| this(R path) |
| { |
| element = rootName(path); |
| auto i = element.length; |
| while (i < path.length && isDirSeparator(path[i])) |
| ++i; |
| rooted = i > 0; |
| elements = pathSplitter(path[i .. $]); |
| popFront(); |
| if (c == c.init && path.length) |
| c = C('.'); |
| } |
| |
| C getElement0() |
| { |
| static if (isNarrowString!S) // avoid autodecode |
| { |
| C c = element[0]; |
| element = element[1 .. $]; |
| } |
| else |
| { |
| C c = element.front; |
| element.popFront(); |
| } |
| version (Windows) |
| { |
| if (c == '/') // can appear in root element |
| c = '\\'; // use native Windows directory separator |
| } |
| return c; |
| } |
| |
| // See if elem is "." |
| static bool isDot(S elem) |
| { |
| return elem.length == 1 && elem[0] == '.'; |
| } |
| |
| // See if elem is ".." |
| static bool isDotDot(S elem) |
| { |
| return elem.length == 2 && elem[0] == '.' && elem[1] == '.'; |
| } |
| |
| bool rooted; // the path starts with a root directory |
| C c; |
| S element; |
| typeof(pathSplitter(path[0 .. 0])) elements; |
| } |
| |
| return Result(path); |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.array; |
| assert(asNormalizedPath("foo/..").array == "."); |
| |
| version (Posix) |
| { |
| assert(asNormalizedPath("/foo/./bar/..//baz/").array == "/foo/baz"); |
| assert(asNormalizedPath("../foo/.").array == "../foo"); |
| assert(asNormalizedPath("/foo/bar/baz/").array == "/foo/bar/baz"); |
| assert(asNormalizedPath("/foo/./bar/../../baz").array == "/baz"); |
| } |
| |
| version (Windows) |
| { |
| assert(asNormalizedPath(`c:\foo\.\bar/..\\baz\`).array == `c:\foo\baz`); |
| assert(asNormalizedPath(`..\foo\.`).array == `..\foo`); |
| assert(asNormalizedPath(`c:\foo\bar\baz\`).array == `c:\foo\bar\baz`); |
| assert(asNormalizedPath(`c:\foo\bar/..`).array == `c:\foo`); |
| assert(asNormalizedPath(`\\server\share\foo\..\bar`).array == |
| `\\server\share\bar`); |
| } |
| } |
| |
| auto asNormalizedPath(R)(auto ref R path) |
| if (isConvertibleToString!R) |
| { |
| return asNormalizedPath!(StringTypeOf!R)(path); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!asNormalizedPath(null)); |
| } |
| |
| @safe unittest |
| { |
| import std.array; |
| import std.utf : byChar; |
| |
| assert(asNormalizedPath("").array is null); |
| assert(asNormalizedPath("foo").array == "foo"); |
| assert(asNormalizedPath(".").array == "."); |
| assert(asNormalizedPath("./.").array == "."); |
| assert(asNormalizedPath("foo/..").array == "."); |
| |
| auto save = asNormalizedPath("fob").save; |
| save.popFront(); |
| assert(save.front == 'o'); |
| |
| version (Posix) |
| { |
| assert(asNormalizedPath("/foo/bar").array == "/foo/bar"); |
| assert(asNormalizedPath("foo/bar/baz").array == "foo/bar/baz"); |
| assert(asNormalizedPath("foo/bar/baz").array == "foo/bar/baz"); |
| assert(asNormalizedPath("foo/bar//baz///").array == "foo/bar/baz"); |
| assert(asNormalizedPath("/foo/bar/baz").array == "/foo/bar/baz"); |
| assert(asNormalizedPath("/foo/../bar/baz").array == "/bar/baz"); |
| assert(asNormalizedPath("/foo/../..//bar/baz").array == "/bar/baz"); |
| assert(asNormalizedPath("/foo/bar/../baz").array == "/foo/baz"); |
| assert(asNormalizedPath("/foo/bar/../../baz").array == "/baz"); |
| assert(asNormalizedPath("/foo/bar/.././/baz/../wee/").array == "/foo/wee"); |
| assert(asNormalizedPath("//foo/bar/baz///wee").array == "/foo/bar/baz/wee"); |
| |
| assert(asNormalizedPath("foo//bar").array == "foo/bar"); |
| assert(asNormalizedPath("foo/bar").array == "foo/bar"); |
| |
| //Curent dir path |
| assert(asNormalizedPath("./").array == "."); |
| assert(asNormalizedPath("././").array == "."); |
| assert(asNormalizedPath("./foo/..").array == "."); |
| assert(asNormalizedPath("foo/..").array == "."); |
| } |
| else version (Windows) |
| { |
| assert(asNormalizedPath(`\foo\bar`).array == `\foo\bar`); |
| assert(asNormalizedPath(`foo\bar\baz`).array == `foo\bar\baz`); |
| assert(asNormalizedPath(`foo\bar\baz`).array == `foo\bar\baz`); |
| assert(asNormalizedPath(`foo\bar\\baz\\\`).array == `foo\bar\baz`); |
| assert(asNormalizedPath(`\foo\bar\baz`).array == `\foo\bar\baz`); |
| assert(asNormalizedPath(`\foo\..\\bar\.\baz`).array == `\bar\baz`); |
| assert(asNormalizedPath(`\foo\..\bar\baz`).array == `\bar\baz`); |
| assert(asNormalizedPath(`\foo\..\..\\bar\baz`).array == `\bar\baz`); |
| |
| assert(asNormalizedPath(`\foo\bar\..\baz`).array == `\foo\baz`); |
| assert(asNormalizedPath(`\foo\bar\../../baz`).array == `\baz`); |
| assert(asNormalizedPath(`\foo\bar\..\.\/baz\..\wee\`).array == `\foo\wee`); |
| |
| assert(asNormalizedPath(`c:\foo\bar`).array == `c:\foo\bar`); |
| assert(asNormalizedPath(`c:foo\bar\baz`).array == `c:foo\bar\baz`); |
| assert(asNormalizedPath(`c:foo\bar\baz`).array == `c:foo\bar\baz`); |
| assert(asNormalizedPath(`c:foo\bar\\baz\\\`).array == `c:foo\bar\baz`); |
| assert(asNormalizedPath(`c:\foo\bar\baz`).array == `c:\foo\bar\baz`); |
| |
| assert(asNormalizedPath(`c:\foo\..\\bar\.\baz`).array == `c:\bar\baz`); |
| assert(asNormalizedPath(`c:\foo\..\bar\baz`).array == `c:\bar\baz`); |
| assert(asNormalizedPath(`c:\foo\..\..\\bar\baz`).array == `c:\bar\baz`); |
| assert(asNormalizedPath(`c:\foo\bar\..\baz`).array == `c:\foo\baz`); |
| assert(asNormalizedPath(`c:\foo\bar\..\..\baz`).array == `c:\baz`); |
| assert(asNormalizedPath(`c:\foo\bar\..\.\\baz\..\wee\`).array == `c:\foo\wee`); |
| assert(asNormalizedPath(`\\server\share\foo\bar`).array == `\\server\share\foo\bar`); |
| assert(asNormalizedPath(`\\server\share\\foo\bar`).array == `\\server\share\foo\bar`); |
| assert(asNormalizedPath(`\\server\share\foo\bar\baz`).array == `\\server\share\foo\bar\baz`); |
| assert(asNormalizedPath(`\\server\share\foo\..\\bar\.\baz`).array == `\\server\share\bar\baz`); |
| assert(asNormalizedPath(`\\server\share\foo\..\bar\baz`).array == `\\server\share\bar\baz`); |
| assert(asNormalizedPath(`\\server\share\foo\..\..\\bar\baz`).array == `\\server\share\bar\baz`); |
| assert(asNormalizedPath(`\\server\share\foo\bar\..\baz`).array == `\\server\share\foo\baz`); |
| assert(asNormalizedPath(`\\server\share\foo\bar\..\..\baz`).array == `\\server\share\baz`); |
| assert(asNormalizedPath(`\\server\share\foo\bar\..\.\\baz\..\wee\`).array == `\\server\share\foo\wee`); |
| |
| static assert(asNormalizedPath(`\foo\..\..\\bar\baz`).array == `\bar\baz`); |
| |
| assert(asNormalizedPath("foo//bar").array == `foo\bar`); |
| |
| //Curent dir path |
| assert(asNormalizedPath(`.\`).array == "."); |
| assert(asNormalizedPath(`.\.\`).array == "."); |
| assert(asNormalizedPath(`.\foo\..`).array == "."); |
| assert(asNormalizedPath(`foo\..`).array == "."); |
| } |
| else static assert(0); |
| } |
| |
| @safe unittest |
| { |
| import std.array; |
| |
| version (Posix) |
| { |
| // Trivial |
| assert(asNormalizedPath("").empty); |
| assert(asNormalizedPath("foo/bar").array == "foo/bar"); |
| |
| // Correct handling of leading slashes |
| assert(asNormalizedPath("/").array == "/"); |
| assert(asNormalizedPath("///").array == "/"); |
| assert(asNormalizedPath("////").array == "/"); |
| assert(asNormalizedPath("/foo/bar").array == "/foo/bar"); |
| assert(asNormalizedPath("//foo/bar").array == "/foo/bar"); |
| assert(asNormalizedPath("///foo/bar").array == "/foo/bar"); |
| assert(asNormalizedPath("////foo/bar").array == "/foo/bar"); |
| |
| // Correct handling of single-dot symbol (current directory) |
| assert(asNormalizedPath("/./foo").array == "/foo"); |
| assert(asNormalizedPath("/foo/./bar").array == "/foo/bar"); |
| |
| assert(asNormalizedPath("./foo").array == "foo"); |
| assert(asNormalizedPath("././foo").array == "foo"); |
| assert(asNormalizedPath("foo/././bar").array == "foo/bar"); |
| |
| // Correct handling of double-dot symbol (previous directory) |
| assert(asNormalizedPath("/foo/../bar").array == "/bar"); |
| assert(asNormalizedPath("/foo/../../bar").array == "/bar"); |
| assert(asNormalizedPath("/../foo").array == "/foo"); |
| assert(asNormalizedPath("/../../foo").array == "/foo"); |
| assert(asNormalizedPath("/foo/..").array == "/"); |
| assert(asNormalizedPath("/foo/../..").array == "/"); |
| |
| assert(asNormalizedPath("foo/../bar").array == "bar"); |
| assert(asNormalizedPath("foo/../../bar").array == "../bar"); |
| assert(asNormalizedPath("../foo").array == "../foo"); |
| assert(asNormalizedPath("../../foo").array == "../../foo"); |
| assert(asNormalizedPath("../foo/../bar").array == "../bar"); |
| assert(asNormalizedPath(".././../foo").array == "../../foo"); |
| assert(asNormalizedPath("foo/bar/..").array == "foo"); |
| assert(asNormalizedPath("/foo/../..").array == "/"); |
| |
| // The ultimate path |
| assert(asNormalizedPath("/foo/../bar//./../...///baz//").array == "/.../baz"); |
| static assert(asNormalizedPath("/foo/../bar//./../...///baz//").array == "/.../baz"); |
| } |
| else version (Windows) |
| { |
| // Trivial |
| assert(asNormalizedPath("").empty); |
| assert(asNormalizedPath(`foo\bar`).array == `foo\bar`); |
| assert(asNormalizedPath("foo/bar").array == `foo\bar`); |
| |
| // Correct handling of absolute paths |
| assert(asNormalizedPath("/").array == `\`); |
| assert(asNormalizedPath(`\`).array == `\`); |
| assert(asNormalizedPath(`\\\`).array == `\`); |
| assert(asNormalizedPath(`\\\\`).array == `\`); |
| assert(asNormalizedPath(`\foo\bar`).array == `\foo\bar`); |
| assert(asNormalizedPath(`\\foo`).array == `\\foo`); |
| assert(asNormalizedPath(`\\foo\\`).array == `\\foo`); |
| assert(asNormalizedPath(`\\foo/bar`).array == `\\foo\bar`); |
| assert(asNormalizedPath(`\\\foo\bar`).array == `\foo\bar`); |
| assert(asNormalizedPath(`\\\\foo\bar`).array == `\foo\bar`); |
| assert(asNormalizedPath(`c:\`).array == `c:\`); |
| assert(asNormalizedPath(`c:\foo\bar`).array == `c:\foo\bar`); |
| assert(asNormalizedPath(`c:\\foo\bar`).array == `c:\foo\bar`); |
| |
| // Correct handling of single-dot symbol (current directory) |
| assert(asNormalizedPath(`\./foo`).array == `\foo`); |
| assert(asNormalizedPath(`\foo/.\bar`).array == `\foo\bar`); |
| |
| assert(asNormalizedPath(`.\foo`).array == `foo`); |
| assert(asNormalizedPath(`./.\foo`).array == `foo`); |
| assert(asNormalizedPath(`foo\.\./bar`).array == `foo\bar`); |
| |
| // Correct handling of double-dot symbol (previous directory) |
| assert(asNormalizedPath(`\foo\..\bar`).array == `\bar`); |
| assert(asNormalizedPath(`\foo\../..\bar`).array == `\bar`); |
| assert(asNormalizedPath(`\..\foo`).array == `\foo`); |
| assert(asNormalizedPath(`\..\..\foo`).array == `\foo`); |
| assert(asNormalizedPath(`\foo\..`).array == `\`); |
| assert(asNormalizedPath(`\foo\../..`).array == `\`); |
| |
| assert(asNormalizedPath(`foo\..\bar`).array == `bar`); |
| assert(asNormalizedPath(`foo\..\../bar`).array == `..\bar`); |
| |
| assert(asNormalizedPath(`..\foo`).array == `..\foo`); |
| assert(asNormalizedPath(`..\..\foo`).array == `..\..\foo`); |
| assert(asNormalizedPath(`..\foo\..\bar`).array == `..\bar`); |
| assert(asNormalizedPath(`..\.\..\foo`).array == `..\..\foo`); |
| assert(asNormalizedPath(`foo\bar\..`).array == `foo`); |
| assert(asNormalizedPath(`\foo\..\..`).array == `\`); |
| assert(asNormalizedPath(`c:\foo\..\..`).array == `c:\`); |
| |
| // Correct handling of non-root path with drive specifier |
| assert(asNormalizedPath(`c:foo`).array == `c:foo`); |
| assert(asNormalizedPath(`c:..\foo\.\..\bar`).array == `c:..\bar`); |
| |
| // The ultimate path |
| assert(asNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`).array == `c:\...\baz`); |
| static assert(asNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`).array == `c:\...\baz`); |
| } |
| else static assert(false); |
| } |
| |
| /** Slice up a path into its elements. |
| |
| Params: |
| path = string or slicable random access range |
| |
| Returns: |
| bidirectional range of slices of `path` |
| */ |
| auto pathSplitter(R)(R path) |
| if ((isRandomAccessRange!R && hasSlicing!R || |
| isNarrowString!R) && |
| !isConvertibleToString!R) |
| { |
| static struct PathSplitter |
| { |
| @property bool empty() const { return pe == 0; } |
| |
| @property R front() |
| { |
| assert(!empty); |
| return _path[fs .. fe]; |
| } |
| |
| void popFront() |
| { |
| assert(!empty); |
| if (ps == pe) |
| { |
| if (fs == bs && fe == be) |
| { |
| pe = 0; |
| } |
| else |
| { |
| fs = bs; |
| fe = be; |
| } |
| } |
| else |
| { |
| fs = ps; |
| fe = fs; |
| while (fe < pe && !isDirSeparator(_path[fe])) |
| ++fe; |
| ps = ltrim(fe, pe); |
| } |
| } |
| |
| @property R back() |
| { |
| assert(!empty); |
| return _path[bs .. be]; |
| } |
| |
| void popBack() |
| { |
| assert(!empty); |
| if (ps == pe) |
| { |
| if (fs == bs && fe == be) |
| { |
| pe = 0; |
| } |
| else |
| { |
| bs = fs; |
| be = fe; |
| } |
| } |
| else |
| { |
| bs = pe; |
| be = bs; |
| while (bs > ps && !isDirSeparator(_path[bs - 1])) |
| --bs; |
| pe = rtrim(ps, bs); |
| } |
| } |
| @property auto save() { return this; } |
| |
| |
| private: |
| R _path; |
| size_t ps, pe; |
| size_t fs, fe; |
| size_t bs, be; |
| |
| this(R p) |
| { |
| if (p.empty) |
| { |
| pe = 0; |
| return; |
| } |
| _path = p; |
| |
| ps = 0; |
| pe = _path.length; |
| |
| // If path is rooted, first element is special |
| version (Windows) |
| { |
| if (isUNC(_path)) |
| { |
| auto i = uncRootLength(_path); |
| fs = 0; |
| fe = i; |
| ps = ltrim(fe, pe); |
| } |
| else if (isDriveRoot(_path)) |
| { |
| fs = 0; |
| fe = 3; |
| ps = ltrim(fe, pe); |
| } |
| else if (_path.length >= 1 && isDirSeparator(_path[0])) |
| { |
| fs = 0; |
| fe = 1; |
| ps = ltrim(fe, pe); |
| } |
| else |
| { |
| assert(!isRooted(_path)); |
| popFront(); |
| } |
| } |
| else version (Posix) |
| { |
| if (_path.length >= 1 && isDirSeparator(_path[0])) |
| { |
| fs = 0; |
| fe = 1; |
| ps = ltrim(fe, pe); |
| } |
| else |
| { |
| popFront(); |
| } |
| } |
| else static assert(0); |
| |
| if (ps == pe) |
| { |
| bs = fs; |
| be = fe; |
| } |
| else |
| { |
| pe = rtrim(ps, pe); |
| popBack(); |
| } |
| } |
| |
| size_t ltrim(size_t s, size_t e) |
| { |
| while (s < e && isDirSeparator(_path[s])) |
| ++s; |
| return s; |
| } |
| |
| size_t rtrim(size_t s, size_t e) |
| { |
| while (s < e && isDirSeparator(_path[e - 1])) |
| --e; |
| return e; |
| } |
| } |
| |
| return PathSplitter(path); |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.algorithm.comparison : equal; |
| import std.conv : to; |
| |
| assert(equal(pathSplitter("/"), ["/"])); |
| assert(equal(pathSplitter("/foo/bar"), ["/", "foo", "bar"])); |
| assert(equal(pathSplitter("foo/../bar//./"), ["foo", "..", "bar", "."])); |
| |
| version (Posix) |
| { |
| assert(equal(pathSplitter("//foo/bar"), ["/", "foo", "bar"])); |
| } |
| |
| version (Windows) |
| { |
| assert(equal(pathSplitter(`foo\..\bar\/.\`), ["foo", "..", "bar", "."])); |
| assert(equal(pathSplitter("c:"), ["c:"])); |
| assert(equal(pathSplitter(`c:\foo\bar`), [`c:\`, "foo", "bar"])); |
| assert(equal(pathSplitter(`c:foo\bar`), ["c:foo", "bar"])); |
| } |
| } |
| |
| auto pathSplitter(R)(auto ref R path) |
| if (isConvertibleToString!R) |
| { |
| return pathSplitter!(StringTypeOf!R)(path); |
| } |
| |
| @safe unittest |
| { |
| import std.algorithm.comparison : equal; |
| assert(testAliasedString!pathSplitter("/")); |
| } |
| |
| @safe unittest |
| { |
| // equal2 verifies that the range is the same both ways, i.e. |
| // through front/popFront and back/popBack. |
| import std.algorithm; |
| import std.range; |
| bool equal2(R1, R2)(R1 r1, R2 r2) |
| { |
| static assert(isBidirectionalRange!R1); |
| return equal(r1, r2) && equal(retro(r1), retro(r2)); |
| } |
| |
| assert(pathSplitter("").empty); |
| |
| // Root directories |
| assert(equal2(pathSplitter("/"), ["/"])); |
| assert(equal2(pathSplitter("//"), ["/"])); |
| assert(equal2(pathSplitter("///"w), ["/"w])); |
| |
| // Absolute paths |
| assert(equal2(pathSplitter("/foo/bar".dup), ["/", "foo", "bar"])); |
| |
| // General |
| assert(equal2(pathSplitter("foo/bar"d.dup), ["foo"d, "bar"d])); |
| assert(equal2(pathSplitter("foo//bar"), ["foo", "bar"])); |
| assert(equal2(pathSplitter("foo/bar//"w), ["foo"w, "bar"w])); |
| assert(equal2(pathSplitter("foo/../bar//./"d), ["foo"d, ".."d, "bar"d, "."d])); |
| |
| // save() |
| auto ps1 = pathSplitter("foo/bar/baz"); |
| auto ps2 = ps1.save; |
| ps1.popFront(); |
| assert(equal2(ps1, ["bar", "baz"])); |
| assert(equal2(ps2, ["foo", "bar", "baz"])); |
| |
| // Platform specific |
| version (Posix) |
| { |
| assert(equal2(pathSplitter("//foo/bar"w.dup), ["/"w, "foo"w, "bar"w])); |
| } |
| version (Windows) |
| { |
| assert(equal2(pathSplitter(`\`), [`\`])); |
| assert(equal2(pathSplitter(`foo\..\bar\/.\`), ["foo", "..", "bar", "."])); |
| assert(equal2(pathSplitter("c:"), ["c:"])); |
| assert(equal2(pathSplitter(`c:\foo\bar`), [`c:\`, "foo", "bar"])); |
| assert(equal2(pathSplitter(`c:foo\bar`), ["c:foo", "bar"])); |
| assert(equal2(pathSplitter(`\\foo\bar`), [`\\foo\bar`])); |
| assert(equal2(pathSplitter(`\\foo\bar\\`), [`\\foo\bar`])); |
| assert(equal2(pathSplitter(`\\foo\bar\baz`), [`\\foo\bar`, "baz"])); |
| } |
| |
| import std.exception; |
| assertCTFEable!( |
| { |
| assert(equal(pathSplitter("/foo/bar".dup), ["/", "foo", "bar"])); |
| }); |
| |
| static assert(is(typeof(pathSplitter!(const(char)[])(null).front) == const(char)[])); |
| |
| import std.utf : byDchar; |
| assert(equal2(pathSplitter("foo/bar"d.byDchar), ["foo"d, "bar"d])); |
| } |
| |
| |
| |
| |
| /** Determines whether a path starts at a root directory. |
| |
| Params: path = A path name. |
| Returns: Whether a path starts at a root directory. |
| |
| On POSIX, this function returns true if and only if the path starts |
| with a slash (/). |
| --- |
| version (Posix) |
| { |
| assert(isRooted("/")); |
| assert(isRooted("/foo")); |
| assert(!isRooted("foo")); |
| assert(!isRooted("../foo")); |
| } |
| --- |
| |
| On Windows, this function returns true if the path starts at |
| the root directory of the current drive, of some other drive, |
| or of a network drive. |
| --- |
| version (Windows) |
| { |
| assert(isRooted(`\`)); |
| assert(isRooted(`\foo`)); |
| assert(isRooted(`d:\foo`)); |
| assert(isRooted(`\\foo\bar`)); |
| assert(!isRooted("foo")); |
| assert(!isRooted("d:foo")); |
| } |
| --- |
| */ |
| bool isRooted(R)(R path) |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| is(StringTypeOf!R)) |
| { |
| if (path.length >= 1 && isDirSeparator(path[0])) return true; |
| version (Posix) return false; |
| else version (Windows) return isAbsolute!(BaseOf!R)(path); |
| } |
| |
| |
| @safe unittest |
| { |
| assert(isRooted("/")); |
| assert(isRooted("/foo")); |
| assert(!isRooted("foo")); |
| assert(!isRooted("../foo")); |
| |
| version (Windows) |
| { |
| assert(isRooted(`\`)); |
| assert(isRooted(`\foo`)); |
| assert(isRooted(`d:\foo`)); |
| assert(isRooted(`\\foo\bar`)); |
| assert(!isRooted("foo")); |
| assert(!isRooted("d:foo")); |
| } |
| |
| static assert(isRooted("/foo")); |
| static assert(!isRooted("foo")); |
| |
| static struct DirEntry { string s; alias s this; } |
| assert(!isRooted(DirEntry("foo"))); |
| } |
| |
| |
| |
| |
| /** Determines whether a path is absolute or not. |
| |
| Params: path = A path name. |
| |
| Returns: Whether a path is absolute or not. |
| |
| Example: |
| On POSIX, an absolute path starts at the root directory. |
| (In fact, $(D _isAbsolute) is just an alias for $(LREF isRooted).) |
| --- |
| version (Posix) |
| { |
| assert(isAbsolute("/")); |
| assert(isAbsolute("/foo")); |
| assert(!isAbsolute("foo")); |
| assert(!isAbsolute("../foo")); |
| } |
| --- |
| |
| On Windows, an absolute path starts at the root directory of |
| a specific drive. Hence, it must start with $(D `d:\`) or $(D `d:/`), |
| where $(D d) is the drive letter. Alternatively, it may be a |
| network path, i.e. a path starting with a double (back)slash. |
| --- |
| version (Windows) |
| { |
| assert(isAbsolute(`d:\`)); |
| assert(isAbsolute(`d:\foo`)); |
| assert(isAbsolute(`\\foo\bar`)); |
| assert(!isAbsolute(`\`)); |
| assert(!isAbsolute(`\foo`)); |
| assert(!isAbsolute("d:foo")); |
| } |
| --- |
| */ |
| version (StdDdoc) |
| { |
| bool isAbsolute(R)(R path) pure nothrow @safe |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| is(StringTypeOf!R)); |
| } |
| else version (Windows) |
| { |
| bool isAbsolute(R)(R path) |
| if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| is(StringTypeOf!R)) |
| { |
| return isDriveRoot!(BaseOf!R)(path) || isUNC!(BaseOf!R)(path); |
| } |
| } |
| else version (Posix) |
| { |
| alias isAbsolute = isRooted; |
| } |
| |
| |
| @safe unittest |
| { |
| assert(!isAbsolute("foo")); |
| assert(!isAbsolute("../foo"w)); |
| static assert(!isAbsolute("foo")); |
| |
| version (Posix) |
| { |
| assert(isAbsolute("/"d)); |
| assert(isAbsolute("/foo".dup)); |
| static assert(isAbsolute("/foo")); |
| } |
| |
| version (Windows) |
| { |
| assert(isAbsolute("d:\\"w)); |
| assert(isAbsolute("d:\\foo"d)); |
| assert(isAbsolute("\\\\foo\\bar")); |
| assert(!isAbsolute("\\"w.dup)); |
| assert(!isAbsolute("\\foo"d.dup)); |
| assert(!isAbsolute("d:")); |
| assert(!isAbsolute("d:foo")); |
| static assert(isAbsolute(`d:\foo`)); |
| } |
| |
| { |
| auto r = MockRange!(immutable(char))(`../foo`); |
| assert(!r.isAbsolute()); |
| } |
| |
| static struct DirEntry { string s; alias s this; } |
| assert(!isAbsolute(DirEntry("foo"))); |
| } |
| |
| |
| |
| |
| /** Transforms $(D path) into an absolute _path. |
| |
| The following algorithm is used: |
| $(OL |
| $(LI If $(D path) is empty, return $(D null).) |
| $(LI If $(D path) is already absolute, return it.) |
| $(LI Otherwise, append $(D path) to $(D base) and return |
| the result. If $(D base) is not specified, the current |
| working directory is used.) |
| ) |
| The function allocates memory if and only if it gets to the third stage |
| of this algorithm. |
| |
| Params: |
| path = the relative path to transform |
| base = the base directory of the relative path |
| |
| Returns: |
| string of transformed path |
| |
| Throws: |
| $(D Exception) if the specified _base directory is not absolute. |
| |
| See_Also: |
| $(LREF asAbsolutePath) which does not allocate |
| */ |
| string absolutePath(string path, lazy string base = getcwd()) |
| @safe pure |
| { |
| import std.array : array; |
| if (path.empty) return null; |
| if (isAbsolute(path)) return path; |
| auto baseVar = base; |
| if (!isAbsolute(baseVar)) throw new Exception("Base directory must be absolute"); |
| return chainPath(baseVar, path).array; |
| } |
| |
| /// |
| @safe unittest |
| { |
| version (Posix) |
| { |
| assert(absolutePath("some/file", "/foo/bar") == "/foo/bar/some/file"); |
| assert(absolutePath("../file", "/foo/bar") == "/foo/bar/../file"); |
| assert(absolutePath("/some/file", "/foo/bar") == "/some/file"); |
| } |
| |
| version (Windows) |
| { |
| assert(absolutePath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`); |
| assert(absolutePath(`..\file`, `c:\foo\bar`) == `c:\foo\bar\..\file`); |
| assert(absolutePath(`c:\some\file`, `c:\foo\bar`) == `c:\some\file`); |
| assert(absolutePath(`\`, `c:\`) == `c:\`); |
| assert(absolutePath(`\some\file`, `c:\foo\bar`) == `c:\some\file`); |
| } |
| } |
| |
| @safe unittest |
| { |
| version (Posix) |
| { |
| static assert(absolutePath("some/file", "/foo/bar") == "/foo/bar/some/file"); |
| } |
| |
| version (Windows) |
| { |
| static assert(absolutePath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`); |
| } |
| |
| import std.exception; |
| assertThrown(absolutePath("bar", "foo")); |
| } |
| |
| /** Transforms $(D path) into an absolute _path. |
| |
| The following algorithm is used: |
| $(OL |
| $(LI If $(D path) is empty, return $(D null).) |
| $(LI If $(D path) is already absolute, return it.) |
| $(LI Otherwise, append $(D path) to the current working directory, |
| which allocates memory.) |
| ) |
| |
| Params: |
| path = the relative path to transform |
| |
| Returns: |
| the transformed path as a lazy range |
| |
| See_Also: |
| $(LREF absolutePath) which returns an allocated string |
| */ |
| auto asAbsolutePath(R)(R path) |
| if ((isRandomAccessRange!R && isSomeChar!(ElementType!R) || |
| isNarrowString!R) && |
| !isConvertibleToString!R) |
| { |
| import std.file : getcwd; |
| string base = null; |
| if (!path.empty && !isAbsolute(path)) |
| base = getcwd(); |
| return chainPath(base, path); |
| } |
| |
| /// |
| @system unittest |
| { |
| import std.array; |
| assert(asAbsolutePath(cast(string) null).array == ""); |
| version (Posix) |
| { |
| assert(asAbsolutePath("/foo").array == "/foo"); |
| } |
| version (Windows) |
| { |
| assert(asAbsolutePath("c:/foo").array == "c:/foo"); |
| } |
| asAbsolutePath("foo"); |
| } |
| |
| auto asAbsolutePath(R)(auto ref R path) |
| if (isConvertibleToString!R) |
| { |
| return asAbsolutePath!(StringTypeOf!R)(path); |
| } |
| |
| @system unittest |
| { |
| assert(testAliasedString!asAbsolutePath(null)); |
| } |
| |
| /** Translates $(D path) into a relative _path. |
| |
| The returned _path is relative to $(D base), which is by default |
| taken to be the current working directory. If specified, |
| $(D base) must be an absolute _path, and it is always assumed |
| to refer to a directory. If $(D path) and $(D base) refer to |
| the same directory, the function returns $(D `.`). |
| |
| The following algorithm is used: |
| $(OL |
| $(LI If $(D path) is a relative directory, return it unaltered.) |
| $(LI Find a common root between $(D path) and $(D base). |
| If there is no common root, return $(D path) unaltered.) |
| $(LI Prepare a string with as many $(D `../`) or $(D `..\`) as |
| necessary to reach the common root from base path.) |
| $(LI Append the remaining segments of $(D path) to the string |
| and return.) |
| ) |
| |
| In the second step, path components are compared using $(D filenameCmp!cs), |
| where $(D cs) is an optional template parameter determining whether |
| the comparison is case sensitive or not. See the |
| $(LREF filenameCmp) documentation for details. |
| |
| This function allocates memory. |
| |
| Params: |
| cs = Whether matching path name components against the base path should |
| be case-sensitive or not. |
| path = A path name. |
| base = The base path to construct the relative path from. |
| |
| Returns: The relative path. |
| |
| See_Also: |
| $(LREF asRelativePath) which does not allocate memory |
| |
| Throws: |
| $(D Exception) if the specified _base directory is not absolute. |
| */ |
| string relativePath(CaseSensitive cs = CaseSensitive.osDefault) |
| (string path, lazy string base = getcwd()) |
| { |
| if (!isAbsolute(path)) |
| return path; |
| auto baseVar = base; |
| if (!isAbsolute(baseVar)) |
| throw new Exception("Base directory must be absolute"); |
| |
| import std.conv : to; |
| return asRelativePath!cs(path, baseVar).to!string; |
| } |
| |
| /// |
| @system unittest |
| { |
| assert(relativePath("foo") == "foo"); |
| |
| version (Posix) |
| { |
| assert(relativePath("foo", "/bar") == "foo"); |
| assert(relativePath("/foo/bar", "/foo/bar") == "."); |
| assert(relativePath("/foo/bar", "/foo/baz") == "../bar"); |
| assert(relativePath("/foo/bar/baz", "/foo/woo/wee") == "../../bar/baz"); |
| assert(relativePath("/foo/bar/baz", "/foo/bar") == "baz"); |
| } |
| version (Windows) |
| { |
| assert(relativePath("foo", `c:\bar`) == "foo"); |
| assert(relativePath(`c:\foo\bar`, `c:\foo\bar`) == "."); |
| assert(relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`); |
| assert(relativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`) == `..\..\bar\baz`); |
| assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz"); |
| assert(relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`); |
| } |
| } |
| |
| @system unittest |
| { |
| import std.exception; |
| assert(relativePath("foo") == "foo"); |
| version (Posix) |
| { |
| relativePath("/foo"); |
| assert(relativePath("/foo/bar", "/foo/baz") == "../bar"); |
| assertThrown(relativePath("/foo", "bar")); |
| } |
| else version (Windows) |
| { |
| relativePath(`\foo`); |
| assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz"); |
| assertThrown(relativePath(`c:\foo`, "bar")); |
| } |
| else static assert(0); |
| } |
| |
| /** Transforms `path` into a _path relative to `base`. |
| |
| The returned _path is relative to `base`, which is usually |
| the current working directory. |
| `base` must be an absolute _path, and it is always assumed |
| to refer to a directory. If `path` and `base` refer to |
| the same directory, the function returns `'.'`. |
| |
| The following algorithm is used: |
| $(OL |
| $(LI If `path` is a relative directory, return it unaltered.) |
| $(LI Find a common root between `path` and `base`. |
| If there is no common root, return `path` unaltered.) |
| $(LI Prepare a string with as many `../` or `..\` as |
| necessary to reach the common root from base path.) |
| $(LI Append the remaining segments of `path` to the string |
| and return.) |
| ) |
| |
| In the second step, path components are compared using `filenameCmp!cs`, |
| where `cs` is an optional template parameter determining whether |
| the comparison is case sensitive or not. See the |
| $(LREF filenameCmp) documentation for details. |
| |
| Params: |
| path = _path to transform |
| base = absolute path |
| cs = whether filespec comparisons are sensitive or not; defaults to |
| `CaseSensitive.osDefault` |
| |
| Returns: |
| a random access range of the transformed _path |
| |
| See_Also: |
| $(LREF relativePath) |
| */ |
| auto asRelativePath(CaseSensitive cs = CaseSensitive.osDefault, R1, R2) |
| (R1 path, R2 base) |
| if ((isNarrowString!R1 || |
| (isRandomAccessRange!R1 && hasSlicing!R1 && isSomeChar!(ElementType!R1)) && |
| !isConvertibleToString!R1) && |
| (isNarrowString!R2 || |
| (isRandomAccessRange!R2 && hasSlicing!R2 && isSomeChar!(ElementType!R2)) && |
| !isConvertibleToString!R2)) |
| { |
| bool choosePath = !isAbsolute(path); |
| |
| // Find common root with current working directory |
| |
| auto basePS = pathSplitter(base); |
| auto pathPS = pathSplitter(path); |
| choosePath |= filenameCmp!cs(basePS.front, pathPS.front) != 0; |
| |
| basePS.popFront(); |
| pathPS.popFront(); |
| |
| import std.algorithm.comparison : mismatch; |
| import std.algorithm.iteration : joiner; |
| import std.array : array; |
| import std.range.primitives : walkLength; |
| import std.range : repeat, chain, choose; |
| import std.utf : byCodeUnit, byChar; |
| |
| // Remove matching prefix from basePS and pathPS |
| auto tup = mismatch!((a, b) => filenameCmp!cs(a, b) == 0)(basePS, pathPS); |
| basePS = tup[0]; |
| pathPS = tup[1]; |
| |
| string sep; |
| if (basePS.empty && pathPS.empty) |
| sep = "."; // if base == path, this is the return |
| else if (!basePS.empty && !pathPS.empty) |
| sep = dirSeparator; |
| |
| // Append as many "../" as necessary to reach common base from path |
| auto r1 = ".." |
| .byChar |
| .repeat(basePS.walkLength()) |
| .joiner(dirSeparator.byChar); |
| |
| auto r2 = pathPS |
| .joiner(dirSeparator.byChar) |
| .byChar; |
| |
| // Return (r1 ~ sep ~ r2) |
| return choose(choosePath, path.byCodeUnit, chain(r1, sep.byChar, r2)); |
| } |
| |
| /// |
| @system unittest |
| { |
| import std.array; |
| version (Posix) |
| { |
| assert(asRelativePath("foo", "/bar").array == "foo"); |
| assert(asRelativePath("/foo/bar", "/foo/bar").array == "."); |
| assert(asRelativePath("/foo/bar", "/foo/baz").array == "../bar"); |
| assert(asRelativePath("/foo/bar/baz", "/foo/woo/wee").array == "../../bar/baz"); |
| assert(asRelativePath("/foo/bar/baz", "/foo/bar").array == "baz"); |
| } |
| else version (Windows) |
| { |
| assert(asRelativePath("foo", `c:\bar`).array == "foo"); |
| assert(asRelativePath(`c:\foo\bar`, `c:\foo\bar`).array == "."); |
| assert(asRelativePath(`c:\foo\bar`, `c:\foo\baz`).array == `..\bar`); |
| assert(asRelativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`).array == `..\..\bar\baz`); |
| assert(asRelativePath(`c:/foo/bar/baz`, `c:\foo\woo\wee`).array == `..\..\bar\baz`); |
| assert(asRelativePath(`c:\foo\bar\baz`, `c:\foo\bar`).array == "baz"); |
| assert(asRelativePath(`c:\foo\bar`, `d:\foo`).array == `c:\foo\bar`); |
| assert(asRelativePath(`\\foo\bar`, `c:\foo`).array == `\\foo\bar`); |
| } |
| else |
| static assert(0); |
| } |
| |
| auto asRelativePath(CaseSensitive cs = CaseSensitive.osDefault, R1, R2) |
| (auto ref R1 path, auto ref R2 base) |
| if (isConvertibleToString!R1 || isConvertibleToString!R2) |
| { |
| import std.meta : staticMap; |
| alias Types = staticMap!(convertToString, R1, R2); |
| return asRelativePath!(cs, Types)(path, base); |
| } |
| |
| @system unittest |
| { |
| import std.array; |
| version (Posix) |
| assert(asRelativePath(TestAliasedString("foo"), TestAliasedString("/bar")).array == "foo"); |
| else version (Windows) |
| assert(asRelativePath(TestAliasedString("foo"), TestAliasedString(`c:\bar`)).array == "foo"); |
| assert(asRelativePath(TestAliasedString("foo"), "bar").array == "foo"); |
| assert(asRelativePath("foo", TestAliasedString("bar")).array == "foo"); |
| assert(asRelativePath(TestAliasedString("foo"), TestAliasedString("bar")).array == "foo"); |
| import std.utf : byDchar; |
| assert(asRelativePath("foo"d.byDchar, TestAliasedString("bar")).array == "foo"); |
| } |
| |
| @system unittest |
| { |
| import std.array, std.utf : bCU=byCodeUnit; |
| version (Posix) |
| { |
| assert(asRelativePath("/foo/bar/baz".bCU, "/foo/bar".bCU).array == "baz"); |
| assert(asRelativePath("/foo/bar/baz"w.bCU, "/foo/bar"w.bCU).array == "baz"w); |
| assert(asRelativePath("/foo/bar/baz"d.bCU, "/foo/bar"d.bCU).array == "baz"d); |
| } |
| else version (Windows) |
| { |
| assert(asRelativePath(`\\foo\bar`.bCU, `c:\foo`.bCU).array == `\\foo\bar`); |
| assert(asRelativePath(`\\foo\bar`w.bCU, `c:\foo`w.bCU).array == `\\foo\bar`w); |
| assert(asRelativePath(`\\foo\bar`d.bCU, `c:\foo`d.bCU).array == `\\foo\bar`d); |
| } |
| } |
| |
| /** Compares filename characters. |
| |
| This function can perform a case-sensitive or a case-insensitive |
| comparison. This is controlled through the $(D cs) template parameter |
| which, if not specified, is given by $(LREF CaseSensitive)$(D .osDefault). |
| |
| On Windows, the backslash and slash characters ($(D `\`) and $(D `/`)) |
| are considered equal. |
| |
| Params: |
| cs = Case-sensitivity of the comparison. |
| a = A filename character. |
| b = A filename character. |
| |
| Returns: |
| $(D < 0) if $(D a < b), |
| $(D 0) if $(D a == b), and |
| $(D > 0) if $(D a > b). |
| */ |
| int filenameCharCmp(CaseSensitive cs = CaseSensitive.osDefault)(dchar a, dchar b) |
| @safe pure nothrow |
| { |
| if (isDirSeparator(a) && isDirSeparator(b)) return 0; |
| static if (!cs) |
| { |
| import std.uni : toLower; |
| a = toLower(a); |
| b = toLower(b); |
| } |
| return cast(int)(a - b); |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(filenameCharCmp('a', 'a') == 0); |
| assert(filenameCharCmp('a', 'b') < 0); |
| assert(filenameCharCmp('b', 'a') > 0); |
| |
| version (linux) |
| { |
| // Same as calling filenameCharCmp!(CaseSensitive.yes)(a, b) |
| assert(filenameCharCmp('A', 'a') < 0); |
| assert(filenameCharCmp('a', 'A') > 0); |
| } |
| version (Windows) |
| { |
| // Same as calling filenameCharCmp!(CaseSensitive.no)(a, b) |
| assert(filenameCharCmp('a', 'A') == 0); |
| assert(filenameCharCmp('a', 'B') < 0); |
| assert(filenameCharCmp('A', 'b') < 0); |
| } |
| } |
| |
| @safe unittest |
| { |
| assert(filenameCharCmp!(CaseSensitive.yes)('A', 'a') < 0); |
| assert(filenameCharCmp!(CaseSensitive.yes)('a', 'A') > 0); |
| |
| assert(filenameCharCmp!(CaseSensitive.no)('a', 'a') == 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('a', 'b') < 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('b', 'a') > 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('A', 'a') == 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('a', 'A') == 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('a', 'B') < 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('B', 'a') > 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('A', 'b') < 0); |
| assert(filenameCharCmp!(CaseSensitive.no)('b', 'A') > 0); |
| |
| version (Posix) assert(filenameCharCmp('\\', '/') != 0); |
| version (Windows) assert(filenameCharCmp('\\', '/') == 0); |
| } |
| |
| |
| /** Compares file names and returns |
| |
| Individual characters are compared using $(D filenameCharCmp!cs), |
| where $(D cs) is an optional template parameter determining whether |
| the comparison is case sensitive or not. |
| |
| Treatment of invalid UTF encodings is implementation defined. |
| |
| Params: |
| cs = case sensitivity |
| filename1 = range for first file name |
| filename2 = range for second file name |
| |
| Returns: |
| $(D < 0) if $(D filename1 < filename2), |
| $(D 0) if $(D filename1 == filename2) and |
| $(D > 0) if $(D filename1 > filename2). |
| |
| See_Also: |
| $(LREF filenameCharCmp) |
| */ |
| int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault, Range1, Range2) |
| (Range1 filename1, Range2 filename2) |
| if (isInputRange!Range1 && !isInfinite!Range1 && |
| isSomeChar!(ElementEncodingType!Range1) && |
| !isConvertibleToString!Range1 && |
| isInputRange!Range2 && !isInfinite!Range2 && |
| isSomeChar!(ElementEncodingType!Range2) && |
| !isConvertibleToString!Range2) |
| { |
| alias C1 = Unqual!(ElementEncodingType!Range1); |
| alias C2 = Unqual!(ElementEncodingType!Range2); |
| |
| static if (!cs && (C1.sizeof < 4 || C2.sizeof < 4) || |
| C1.sizeof != C2.sizeof) |
| { |
| // Case insensitive - decode so case is checkable |
| // Different char sizes - decode to bring to common type |
| import std.utf : byDchar; |
| return filenameCmp!cs(filename1.byDchar, filename2.byDchar); |
| } |
| else static if (isSomeString!Range1 && C1.sizeof < 4 || |
| isSomeString!Range2 && C2.sizeof < 4) |
| { |
| // Avoid autodecoding |
| import std.utf : byCodeUnit; |
| return filenameCmp!cs(filename1.byCodeUnit, filename2.byCodeUnit); |
| } |
| else |
| { |
| for (;;) |
| { |
| if (filename1.empty) return -(cast(int) !filename2.empty); |
| if (filename2.empty) return 1; |
| const c = filenameCharCmp!cs(filename1.front, filename2.front); |
| if (c != 0) return c; |
| filename1.popFront(); |
| filename2.popFront(); |
| } |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(filenameCmp("abc", "abc") == 0); |
| assert(filenameCmp("abc", "abd") < 0); |
| assert(filenameCmp("abc", "abb") > 0); |
| assert(filenameCmp("abc", "abcd") < 0); |
| assert(filenameCmp("abcd", "abc") > 0); |
| |
| version (linux) |
| { |
| // Same as calling filenameCmp!(CaseSensitive.yes)(filename1, filename2) |
| assert(filenameCmp("Abc", "abc") < 0); |
| assert(filenameCmp("abc", "Abc") > 0); |
| } |
| version (Windows) |
| { |
| // Same as calling filenameCmp!(CaseSensitive.no)(filename1, filename2) |
| assert(filenameCmp("Abc", "abc") == 0); |
| assert(filenameCmp("abc", "Abc") == 0); |
| assert(filenameCmp("Abc", "abD") < 0); |
| assert(filenameCmp("abc", "AbB") > 0); |
| } |
| } |
| |
| int filenameCmp(CaseSensitive cs = CaseSensitive.osDefault, Range1, Range2) |
| (auto ref Range1 filename1, auto ref Range2 filename2) |
| if (isConvertibleToString!Range1 || isConvertibleToString!Range2) |
| { |
| import std.meta : staticMap; |
| alias Types = staticMap!(convertToString, Range1, Range2); |
| return filenameCmp!(cs, Types)(filename1, filename2); |
| } |
| |
| @safe unittest |
| { |
| assert(filenameCmp!(CaseSensitive.yes)(TestAliasedString("Abc"), "abc") < 0); |
| assert(filenameCmp!(CaseSensitive.yes)("Abc", TestAliasedString("abc")) < 0); |
| assert(filenameCmp!(CaseSensitive.yes)(TestAliasedString("Abc"), TestAliasedString("abc")) < 0); |
| } |
| |
| @safe unittest |
| { |
| assert(filenameCmp!(CaseSensitive.yes)("Abc", "abc") < 0); |
| assert(filenameCmp!(CaseSensitive.yes)("abc", "Abc") > 0); |
| |
| assert(filenameCmp!(CaseSensitive.no)("abc", "abc") == 0); |
| assert(filenameCmp!(CaseSensitive.no)("abc", "abd") < 0); |
| assert(filenameCmp!(CaseSensitive.no)("abc", "abb") > 0); |
| assert(filenameCmp!(CaseSensitive.no)("abc", "abcd") < 0); |
| assert(filenameCmp!(CaseSensitive.no)("abcd", "abc") > 0); |
| assert(filenameCmp!(CaseSensitive.no)("Abc", "abc") == 0); |
| assert(filenameCmp!(CaseSensitive.no)("abc", "Abc") == 0); |
| assert(filenameCmp!(CaseSensitive.no)("Abc", "abD") < 0); |
| assert(filenameCmp!(CaseSensitive.no)("abc", "AbB") > 0); |
| |
| version (Posix) assert(filenameCmp(`abc\def`, `abc/def`) != 0); |
| version (Windows) assert(filenameCmp(`abc\def`, `abc/def`) == 0); |
| } |
| |
| /** Matches a pattern against a path. |
| |
| Some characters of pattern have a special meaning (they are |
| $(I meta-characters)) and can't be escaped. These are: |
| |
| $(BOOKTABLE, |
| $(TR $(TD $(D *)) |
| $(TD Matches 0 or more instances of any character.)) |
| $(TR $(TD $(D ?)) |
| $(TD Matches exactly one instance of any character.)) |
| $(TR $(TD $(D [)$(I chars)$(D ])) |
| $(TD Matches one instance of any character that appears |
| between the brackets.)) |
| $(TR $(TD $(D [!)$(I chars)$(D ])) |
| $(TD Matches one instance of any character that does not |
| appear between the brackets after the exclamation mark.)) |
| $(TR $(TD $(D {)$(I string1)$(D ,)$(I string2)$(D ,)…$(D })) |
| $(TD Matches either of the specified strings.)) |
| ) |
| |
| Individual characters are compared using $(D filenameCharCmp!cs), |
| where $(D cs) is an optional template parameter determining whether |
| the comparison is case sensitive or not. See the |
| $(LREF filenameCharCmp) documentation for details. |
| |
| Note that directory |
| separators and dots don't stop a meta-character from matching |
| further portions of the path. |
| |
| Params: |
| cs = Whether the matching should be case-sensitive |
| path = The path to be matched against |
| pattern = The glob pattern |
| |
| Returns: |
| $(D true) if pattern matches path, $(D false) otherwise. |
| |
| See_also: |
| $(LINK2 http://en.wikipedia.org/wiki/Glob_%28programming%29,Wikipedia: _glob (programming)) |
| */ |
| bool globMatch(CaseSensitive cs = CaseSensitive.osDefault, C, Range) |
| (Range path, const(C)[] pattern) |
| @safe pure nothrow |
| if (isForwardRange!Range && !isInfinite!Range && |
| isSomeChar!(ElementEncodingType!Range) && !isConvertibleToString!Range && |
| isSomeChar!C && is(Unqual!C == Unqual!(ElementEncodingType!Range))) |
| in |
| { |
| // Verify that pattern[] is valid |
| import std.algorithm.searching : balancedParens; |
| assert(balancedParens(pattern, '[', ']', 0)); |
| assert(balancedParens(pattern, '{', '}', 0)); |
| } |
| body |
| { |
| alias RC = Unqual!(ElementEncodingType!Range); |
| |
| static if (RC.sizeof == 1 && isSomeString!Range) |
| { |
| import std.utf : byChar; |
| return globMatch!cs(path.byChar, pattern); |
| } |
| else static if (RC.sizeof == 2 && isSomeString!Range) |
| { |
| import std.utf : byWchar; |
| return globMatch!cs(path.byWchar, pattern); |
| } |
| else |
| { |
| C[] pattmp; |
| foreach (ref pi; 0 .. pattern.length) |
| { |
| const pc = pattern[pi]; |
| switch (pc) |
| { |
| case '*': |
| if (pi + 1 == pattern.length) |
| return true; |
| for (; !path.empty; path.popFront()) |
| { |
| auto p = path.save; |
| if (globMatch!(cs, C)(p, |
| pattern[pi + 1 .. pattern.length])) |
| return true; |
| } |
| return false; |
| |
| case '?': |
| if (path.empty) |
| return false; |
| path.popFront(); |
| break; |
| |
| case '[': |
| if (path.empty) |
| return false; |
| auto nc = path.front; |
| path.popFront(); |
| auto not = false; |
| ++pi; |
| if (pattern[pi] == '!') |
| { |
| not = true; |
| ++pi; |
| } |
| auto anymatch = false; |
| while (1) |
| { |
| const pc2 = pattern[pi]; |
| if (pc2 == ']') |
| break; |
| if (!anymatch && (filenameCharCmp!cs(nc, pc2) == 0)) |
| anymatch = true; |
| ++pi; |
| } |
| if (anymatch == not) |
| return false; |
| break; |
| |
| case '{': |
| // find end of {} section |
| auto piRemain = pi; |
| for (; piRemain < pattern.length |
| && pattern[piRemain] != '}'; ++piRemain) |
| { } |
| |
| if (piRemain < pattern.length) |
| ++piRemain; |
| ++pi; |
| |
| while (pi < pattern.length) |
| { |
| const pi0 = pi; |
| C pc3 = pattern[pi]; |
| // find end of current alternative |
| for (; pi < pattern.length && pc3 != '}' && pc3 != ','; ++pi) |
| { |
| pc3 = pattern[pi]; |
| } |
| |
| auto p = path.save; |
| if (pi0 == pi) |
| { |
| if (globMatch!(cs, C)(p, pattern[piRemain..$])) |
| { |
| return true; |
| } |
| ++pi; |
| } |
| else |
| { |
| /* Match for: |
| * pattern[pi0 .. pi-1] ~ pattern[piRemain..$] |
| */ |
| if (pattmp is null) |
| // Allocate this only once per function invocation. |
| // Should do it with malloc/free, but that would make it impure. |
| pattmp = new C[pattern.length]; |
| |
| const len1 = pi - 1 - pi0; |
| pattmp[0 .. len1] = pattern[pi0 .. pi - 1]; |
| |
| const len2 = pattern.length - piRemain; |
| pattmp[len1 .. len1 + len2] = pattern[piRemain .. $]; |
| |
| if (globMatch!(cs, C)(p, pattmp[0 .. len1 + len2])) |
| { |
| return true; |
| } |
| } |
| if (pc3 == '}') |
| { |
| break; |
| } |
| } |
| return false; |
| |
| default: |
| if (path.empty) |
| return false; |
| if (filenameCharCmp!cs(pc, path.front) != 0) |
| return false; |
| path.popFront(); |
| break; |
| } |
| } |
| return path.empty; |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| assert(globMatch("foo.bar", "*")); |
| assert(globMatch("foo.bar", "*.*")); |
| assert(globMatch(`foo/foo\bar`, "f*b*r")); |
| assert(globMatch("foo.bar", "f???bar")); |
| assert(globMatch("foo.bar", "[fg]???bar")); |
| assert(globMatch("foo.bar", "[!gh]*bar")); |
| assert(globMatch("bar.fooz", "bar.{foo,bif}z")); |
| assert(globMatch("bar.bifz", "bar.{foo,bif}z")); |
| |
| version (Windows) |
| { |
| // Same as calling globMatch!(CaseSensitive.no)(path, pattern) |
| assert(globMatch("foo", "Foo")); |
| assert(globMatch("Goo.bar", "[fg]???bar")); |
| } |
| version (linux) |
| { |
| // Same as calling globMatch!(CaseSensitive.yes)(path, pattern) |
| assert(!globMatch("foo", "Foo")); |
| assert(!globMatch("Goo.bar", "[fg]???bar")); |
| } |
| } |
| |
| bool globMatch(CaseSensitive cs = CaseSensitive.osDefault, C, Range) |
| (auto ref Range path, const(C)[] pattern) |
| @safe pure nothrow |
| if (isConvertibleToString!Range) |
| { |
| return globMatch!(cs, C, StringTypeOf!Range)(path, pattern); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!globMatch("foo.bar", "*")); |
| } |
| |
| @safe unittest |
| { |
| assert(globMatch!(CaseSensitive.no)("foo", "Foo")); |
| assert(!globMatch!(CaseSensitive.yes)("foo", "Foo")); |
| |
| assert(globMatch("foo", "*")); |
| assert(globMatch("foo.bar"w, "*"w)); |
| assert(globMatch("foo.bar"d, "*.*"d)); |
| assert(globMatch("foo.bar", "foo*")); |
| assert(globMatch("foo.bar"w, "f*bar"w)); |
| assert(globMatch("foo.bar"d, "f*b*r"d)); |
| assert(globMatch("foo.bar", "f???bar")); |
| assert(globMatch("foo.bar"w, "[fg]???bar"w)); |
| assert(globMatch("foo.bar"d, "[!gh]*bar"d)); |
| |
| assert(!globMatch("foo", "bar")); |
| assert(!globMatch("foo"w, "*.*"w)); |
| assert(!globMatch("foo.bar"d, "f*baz"d)); |
| assert(!globMatch("foo.bar", "f*b*x")); |
| assert(!globMatch("foo.bar", "[gh]???bar")); |
| assert(!globMatch("foo.bar"w, "[!fg]*bar"w)); |
| assert(!globMatch("foo.bar"d, "[fg]???baz"d)); |
| assert(!globMatch("foo.di", "*.d")); // test issue 6634: triggered bad assertion |
| |
| assert(globMatch("foo.bar", "{foo,bif}.bar")); |
| assert(globMatch("bif.bar"w, "{foo,bif}.bar"w)); |
| |
| assert(globMatch("bar.foo"d, "bar.{foo,bif}"d)); |
| assert(globMatch("bar.bif", "bar.{foo,bif}")); |
| |
| assert(globMatch("bar.fooz"w, "bar.{foo,bif}z"w)); |
| assert(globMatch("bar.bifz"d, "bar.{foo,bif}z"d)); |
| |
| assert(globMatch("bar.foo", "bar.{biz,,baz}foo")); |
| assert(globMatch("bar.foo"w, "bar.{biz,}foo"w)); |
| assert(globMatch("bar.foo"d, "bar.{,biz}foo"d)); |
| assert(globMatch("bar.foo", "bar.{}foo")); |
| |
| assert(globMatch("bar.foo"w, "bar.{ar,,fo}o"w)); |
| assert(globMatch("bar.foo"d, "bar.{,ar,fo}o"d)); |
| assert(globMatch("bar.o", "bar.{,ar,fo}o")); |
| |
| assert(!globMatch("foo", "foo?")); |
| assert(!globMatch("foo", "foo[]")); |
| assert(!globMatch("foo", "foob")); |
| assert(!globMatch("foo", "foo{b}")); |
| |
| |
| static assert(globMatch("foo.bar", "[!gh]*bar")); |
| } |
| |
| |
| |
| |
| /** Checks that the given file or directory name is valid. |
| |
| The maximum length of $(D filename) is given by the constant |
| $(D core.stdc.stdio.FILENAME_MAX). (On Windows, this number is |
| defined as the maximum number of UTF-16 code points, and the |
| test will therefore only yield strictly correct results when |
| $(D filename) is a string of $(D wchar)s.) |
| |
| On Windows, the following criteria must be satisfied |
| ($(LINK2 http://msdn.microsoft.com/en-us/library/aa365247(v=vs.85).aspx,source)): |
| $(UL |
| $(LI $(D filename) must not contain any characters whose integer |
| representation is in the range 0-31.) |
| $(LI $(D filename) must not contain any of the following $(I reserved |
| characters): <>:"/\|?*) |
| $(LI $(D filename) may not end with a space ($(D ' ')) or a period |
| ($(D '.')).) |
| ) |
| |
| On POSIX, $(D filename) may not contain a forward slash ($(D '/')) or |
| the null character ($(D '\0')). |
| |
| Params: |
| filename = string to check |
| |
| Returns: |
| $(D true) if and only if $(D filename) is not |
| empty, not too long, and does not contain invalid characters. |
| |
| */ |
| bool isValidFilename(Range)(Range filename) |
| if ((isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!Range) || |
| isNarrowString!Range) && |
| !isConvertibleToString!Range) |
| { |
| import core.stdc.stdio : FILENAME_MAX; |
| if (filename.length == 0 || filename.length >= FILENAME_MAX) return false; |
| foreach (c; filename) |
| { |
| version (Windows) |
| { |
| switch (c) |
| { |
| case 0: |
| .. |
| case 31: |
| case '<': |
| case '>': |
| case ':': |
| case '"': |
| case '/': |
| case '\\': |
| case '|': |
| case '?': |
| case '*': |
| return false; |
| |
| default: |
| break; |
| } |
| } |
| else version (Posix) |
| { |
| if (c == 0 || c == '/') return false; |
| } |
| else static assert(0); |
| } |
| version (Windows) |
| { |
| auto last = filename[filename.length - 1]; |
| if (last == '.' || last == ' ') return false; |
| } |
| |
| // All criteria passed |
| return true; |
| } |
| |
| /// |
| @safe pure @nogc nothrow |
| unittest |
| { |
| import std.utf : byCodeUnit; |
| |
| assert(isValidFilename("hello.exe".byCodeUnit)); |
| } |
| |
| bool isValidFilename(Range)(auto ref Range filename) |
| if (isConvertibleToString!Range) |
| { |
| return isValidFilename!(StringTypeOf!Range)(filename); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!isValidFilename("hello.exe")); |
| } |
| |
| @safe pure |
| unittest |
| { |
| import std.conv; |
| auto valid = ["foo"]; |
| auto invalid = ["", "foo\0bar", "foo/bar"]; |
| auto pfdep = [`foo\bar`, "*.txt"]; |
| version (Windows) invalid ~= pfdep; |
| else version (Posix) valid ~= pfdep; |
| else static assert(0); |
| |
| import std.meta : AliasSeq; |
| foreach (T; AliasSeq!(char[], const(char)[], string, wchar[], |
| const(wchar)[], wstring, dchar[], const(dchar)[], dstring)) |
| { |
| foreach (fn; valid) |
| assert(isValidFilename(to!T(fn))); |
| foreach (fn; invalid) |
| assert(!isValidFilename(to!T(fn))); |
| } |
| |
| { |
| auto r = MockRange!(immutable(char))(`dir/file.d`); |
| assert(!isValidFilename(r)); |
| } |
| |
| static struct DirEntry { string s; alias s this; } |
| assert(isValidFilename(DirEntry("file.ext"))); |
| |
| version (Windows) |
| { |
| immutable string cases = "<>:\"/\\|?*"; |
| foreach (i; 0 .. 31 + cases.length) |
| { |
| char[3] buf; |
| buf[0] = 'a'; |
| buf[1] = i <= 31 ? cast(char) i : cases[i - 32]; |
| buf[2] = 'b'; |
| assert(!isValidFilename(buf[])); |
| } |
| } |
| } |
| |
| |
| |
| /** Checks whether $(D path) is a valid _path. |
| |
| Generally, this function checks that $(D path) is not empty, and that |
| each component of the path either satisfies $(LREF isValidFilename) |
| or is equal to $(D ".") or $(D ".."). |
| |
| $(B It does $(I not) check whether the _path points to an existing file |
| or directory; use $(REF exists, std,file) for this purpose.) |
| |
| On Windows, some special rules apply: |
| $(UL |
| $(LI If the second character of $(D path) is a colon ($(D ':')), |
| the first character is interpreted as a drive letter, and |
| must be in the range A-Z (case insensitive).) |
| $(LI If $(D path) is on the form $(D `\\$(I server)\$(I share)\...`) |
| (UNC path), $(LREF isValidFilename) is applied to $(I server) |
| and $(I share) as well.) |
| $(LI If $(D path) starts with $(D `\\?\`) (long UNC path), the |
| only requirement for the rest of the string is that it does |
| not contain the null character.) |
| $(LI If $(D path) starts with $(D `\\.\`) (Win32 device namespace) |
| this function returns $(D false); such paths are beyond the scope |
| of this module.) |
| ) |
| |
| Params: |
| path = string or Range of characters to check |
| |
| Returns: |
| true if $(D path) is a valid _path. |
| */ |
| bool isValidPath(Range)(Range path) |
| if ((isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!Range) || |
| isNarrowString!Range) && |
| !isConvertibleToString!Range) |
| { |
| alias C = Unqual!(ElementEncodingType!Range); |
| |
| if (path.empty) return false; |
| |
| // Check whether component is "." or "..", or whether it satisfies |
| // isValidFilename. |
| bool isValidComponent(Range component) |
| { |
| assert(component.length > 0); |
| if (component[0] == '.') |
| { |
| if (component.length == 1) return true; |
| else if (component.length == 2 && component[1] == '.') return true; |
| } |
| return isValidFilename(component); |
| } |
| |
| if (path.length == 1) |
| return isDirSeparator(path[0]) || isValidComponent(path); |
| |
| Range remainder; |
| version (Windows) |
| { |
| if (isDirSeparator(path[0]) && isDirSeparator(path[1])) |
| { |
| // Some kind of UNC path |
| if (path.length < 5) |
| { |
| // All valid UNC paths must have at least 5 characters |
| return false; |
| } |
| else if (path[2] == '?') |
| { |
| // Long UNC path |
| if (!isDirSeparator(path[3])) return false; |
| foreach (c; path[4 .. $]) |
| { |
| if (c == '\0') return false; |
| } |
| return true; |
| } |
| else if (path[2] == '.') |
| { |
| // Win32 device namespace not supported |
| return false; |
| } |
| else |
| { |
| // Normal UNC path, i.e. \\server\share\... |
| size_t i = 2; |
| while (i < path.length && !isDirSeparator(path[i])) ++i; |
| if (i == path.length || !isValidFilename(path[2 .. i])) |
| return false; |
| ++i; // Skip a single dir separator |
| size_t j = i; |
| while (j < path.length && !isDirSeparator(path[j])) ++j; |
| if (!isValidFilename(path[i .. j])) return false; |
| remainder = path[j .. $]; |
| } |
| } |
| else if (isDriveSeparator(path[1])) |
| { |
| import std.ascii : isAlpha; |
| if (!isAlpha(path[0])) return false; |
| remainder = path[2 .. $]; |
| } |
| else |
| { |
| remainder = path; |
| } |
| } |
| else version (Posix) |
| { |
| remainder = path; |
| } |
| else static assert(0); |
| remainder = ltrimDirSeparators(remainder); |
| |
| // Check that each component satisfies isValidComponent. |
| while (!remainder.empty) |
| { |
| size_t i = 0; |
| while (i < remainder.length && !isDirSeparator(remainder[i])) ++i; |
| assert(i > 0); |
| if (!isValidComponent(remainder[0 .. i])) return false; |
| remainder = ltrimDirSeparators(remainder[i .. $]); |
| } |
| |
| // All criteria passed |
| return true; |
| } |
| |
| /// |
| @safe pure @nogc nothrow |
| unittest |
| { |
| assert(isValidPath("/foo/bar")); |
| assert(!isValidPath("/foo\0/bar")); |
| assert(isValidPath("/")); |
| assert(isValidPath("a")); |
| |
| version (Windows) |
| { |
| assert(isValidPath(`c:\`)); |
| assert(isValidPath(`c:\foo`)); |
| assert(isValidPath(`c:\foo\.\bar\\\..\`)); |
| assert(!isValidPath(`!:\foo`)); |
| assert(!isValidPath(`c::\foo`)); |
| assert(!isValidPath(`c:\foo?`)); |
| assert(!isValidPath(`c:\foo.`)); |
| |
| assert(isValidPath(`\\server\share`)); |
| assert(isValidPath(`\\server\share\foo`)); |
| assert(isValidPath(`\\server\share\\foo`)); |
| assert(!isValidPath(`\\\server\share\foo`)); |
| assert(!isValidPath(`\\server\\share\foo`)); |
| assert(!isValidPath(`\\ser*er\share\foo`)); |
| assert(!isValidPath(`\\server\sha?e\foo`)); |
| assert(!isValidPath(`\\server\share\|oo`)); |
| |
| assert(isValidPath(`\\?\<>:"?*|/\..\.`)); |
| assert(!isValidPath("\\\\?\\foo\0bar")); |
| |
| assert(!isValidPath(`\\.\PhysicalDisk1`)); |
| assert(!isValidPath(`\\`)); |
| } |
| |
| import std.utf : byCodeUnit; |
| assert(isValidPath("/foo/bar".byCodeUnit)); |
| } |
| |
| bool isValidPath(Range)(auto ref Range path) |
| if (isConvertibleToString!Range) |
| { |
| return isValidPath!(StringTypeOf!Range)(path); |
| } |
| |
| @safe unittest |
| { |
| assert(testAliasedString!isValidPath("/foo/bar")); |
| } |
| |
| /** Performs tilde expansion in paths on POSIX systems. |
| On Windows, this function does nothing. |
| |
| There are two ways of using tilde expansion in a path. One |
| involves using the tilde alone or followed by a path separator. In |
| this case, the tilde will be expanded with the value of the |
| environment variable $(D HOME). The second way is putting |
| a username after the tilde (i.e. $(D ~john/Mail)). Here, |
| the username will be searched for in the user database |
| (i.e. $(D /etc/passwd) on Unix systems) and will expand to |
| whatever path is stored there. The username is considered the |
| string after the tilde ending at the first instance of a path |
| separator. |
| |
| Note that using the $(D ~user) syntax may give different |
| values from just $(D ~) if the environment variable doesn't |
| match the value stored in the user database. |
| |
| When the environment variable version is used, the path won't |
| be modified if the environment variable doesn't exist or it |
| is empty. When the database version is used, the path won't be |
| modified if the user doesn't exist in the database or there is |
| not enough memory to perform the query. |
| |
| This function performs several memory allocations. |
| |
| Params: |
| inputPath = The path name to expand. |
| |
| Returns: |
| $(D inputPath) with the tilde expanded, or just $(D inputPath) |
| if it could not be expanded. |
| For Windows, $(D expandTilde) merely returns its argument $(D inputPath). |
| |
| Example: |
| ----- |
| void processFile(string path) |
| { |
| // Allow calling this function with paths such as ~/foo |
| auto fullPath = expandTilde(path); |
| ... |
| } |
| ----- |
| */ |
| string expandTilde(string inputPath) nothrow |
| { |
| version (Posix) |
| { |
| import core.exception : onOutOfMemoryError; |
| import core.stdc.errno : errno, ERANGE; |
| import core.stdc.stdlib : malloc, free, realloc; |
| |
| /* Joins a path from a C string to the remainder of path. |
| |
| The last path separator from c_path is discarded. The result |
| is joined to path[char_pos .. length] if char_pos is smaller |
| than length, otherwise path is not appended to c_path. |
| */ |
| static string combineCPathWithDPath(char* c_path, string path, size_t char_pos) nothrow |
| { |
| import core.stdc.string : strlen; |
| |
| assert(c_path != null); |
| assert(path.length > 0); |
| assert(char_pos >= 0); |
| |
| // Search end of C string |
| size_t end = strlen(c_path); |
| |
| // Remove trailing path separator, if any |
| if (end && isDirSeparator(c_path[end - 1])) |
| end--; |
| |
| // (this is the only GC allocation done in expandTilde()) |
| string cp; |
| if (char_pos < path.length) |
| // Append something from path |
| cp = cast(string)(c_path[0 .. end] ~ path[char_pos .. $]); |
| else |
| // Create our own copy, as lifetime of c_path is undocumented |
| cp = c_path[0 .. end].idup; |
| |
| return cp; |
| } |
| |
| // Replaces the tilde from path with the environment variable HOME. |
| static string expandFromEnvironment(string path) nothrow |
| { |
| import core.stdc.stdlib : getenv; |
| |
| assert(path.length >= 1); |
| assert(path[0] == '~'); |
| |
| // Get HOME and use that to replace the tilde. |
| auto home = getenv("HOME"); |
| if (home == null) |
| return path; |
| |
| return combineCPathWithDPath(home, path, 1); |
| } |
| |
| // Replaces the tilde from path with the path from the user database. |
| static string expandFromDatabase(string path) nothrow |
| { |
| // bionic doesn't really support this, as getpwnam_r |
| // isn't provided and getpwnam is basically just a stub |
| version (CRuntime_Bionic) |
| { |
| return path; |
| } |
| else |
| { |
| import core.sys.posix.pwd : passwd, getpwnam_r; |
| import std.string : indexOf; |
| |
| assert(path.length > 2 || (path.length == 2 && !isDirSeparator(path[1]))); |
| assert(path[0] == '~'); |
| |
| // Extract username, searching for path separator. |
| auto last_char = indexOf(path, dirSeparator[0]); |
| |
| size_t username_len = (last_char == -1) ? path.length : last_char; |
| char* username = cast(char*) malloc(username_len * char.sizeof); |
| if (!username) |
| onOutOfMemoryError(); |
| scope(exit) free(username); |
| |
| if (last_char == -1) |
| { |
| username[0 .. username_len - 1] = path[1 .. $]; |
| last_char = path.length + 1; |
| } |
| else |
| { |
| username[0 .. username_len - 1] = path[1 .. last_char]; |
| } |
| username[username_len - 1] = 0; |
| |
| assert(last_char > 1); |
| |
| // Reserve C memory for the getpwnam_r() function. |
| version (unittest) |
| uint extra_memory_size = 2; |
| else |
| uint extra_memory_size = 5 * 1024; |
| char* extra_memory; |
| scope(exit) free(extra_memory); |
| |
| passwd result; |
| while (1) |
| { |
| extra_memory = cast(char*) realloc(extra_memory, extra_memory_size * char.sizeof); |
| if (extra_memory == null) |
| onOutOfMemoryError(); |
| |
| // Obtain info from database. |
| passwd *verify; |
| errno = 0; |
| if (getpwnam_r(username, &result, extra_memory, extra_memory_size, |
| &verify) == 0) |
| { |
| // Succeeded if verify points at result |
| if (verify == &result) |
| // username is found |
| path = combineCPathWithDPath(result.pw_dir, path, last_char); |
| break; |
| } |
| |
| if (errno != ERANGE && |
| // On BSD and OSX, errno can be left at 0 instead of set to ERANGE |
| errno != 0) |
| onOutOfMemoryError(); |
| |
| // extra_memory isn't large enough |
| import core.checkedint : mulu; |
| bool overflow; |
| extra_memory_size = mulu(extra_memory_size, 2, overflow); |
| if (overflow) assert(0); |
| } |
| return path; |
| } |
| } |
| |
| // Return early if there is no tilde in path. |
| if (inputPath.length < 1 || inputPath[0] != '~') |
| return inputPath; |
| |
| if (inputPath.length == 1 || isDirSeparator(inputPath[1])) |
| return expandFromEnvironment(inputPath); |
| else |
| return expandFromDatabase(inputPath); |
| } |
| else version (Windows) |
| { |
| // Put here real windows implementation. |
| return inputPath; |
| } |
| else |
| { |
| static assert(0); // Guard. Implement on other platforms. |
| } |
| } |
| |
| |
| version (unittest) import std.process : environment; |
| @system unittest |
| { |
| version (Posix) |
| { |
| // Retrieve the current home variable. |
| auto oldHome = environment.get("HOME"); |
| |
| // Testing when there is no environment variable. |
| environment.remove("HOME"); |
| assert(expandTilde("~/") == "~/"); |
| assert(expandTilde("~") == "~"); |
| |
| // Testing when an environment variable is set. |
| environment["HOME"] = "dmd/test"; |
| assert(expandTilde("~/") == "dmd/test/"); |
| assert(expandTilde("~") == "dmd/test"); |
| |
| // The same, but with a variable ending in a slash. |
| environment["HOME"] = "dmd/test/"; |
| assert(expandTilde("~/") == "dmd/test/"); |
| assert(expandTilde("~") == "dmd/test"); |
| |
| // Recover original HOME variable before continuing. |
| if (oldHome !is null) environment["HOME"] = oldHome; |
| else environment.remove("HOME"); |
| |
| // Test user expansion for root, no /root on Android |
| version (OSX) |
| { |
| assert(expandTilde("~root") == "/var/root", expandTilde("~root")); |
| assert(expandTilde("~root/") == "/var/root/", expandTilde("~root/")); |
| } |
| else version (Android) |
| { |
| } |
| else |
| { |
| assert(expandTilde("~root") == "/root", expandTilde("~root")); |
| assert(expandTilde("~root/") == "/root/", expandTilde("~root/")); |
| } |
| assert(expandTilde("~Idontexist/hey") == "~Idontexist/hey"); |
| } |
| } |
| |
| version (unittest) |
| { |
| /* Define a mock RandomAccessRange to use for unittesting. |
| */ |
| |
| struct MockRange(C) |
| { |
| this(C[] array) { this.array = array; } |
| const |
| { |
| @property size_t length() { return array.length; } |
| @property bool empty() { return array.length == 0; } |
| @property C front() { return array[0]; } |
| @property C back() { return array[$ - 1]; } |
| @property size_t opDollar() { return length; } |
| C opIndex(size_t i) { return array[i]; } |
| } |
| void popFront() { array = array[1 .. $]; } |
| void popBack() { array = array[0 .. $-1]; } |
| MockRange!C opSlice( size_t lwr, size_t upr) const |
| { |
| return MockRange!C(array[lwr .. upr]); |
| } |
| @property MockRange save() { return this; } |
| private: |
| C[] array; |
| } |
| |
| static assert( isRandomAccessRange!(MockRange!(const(char))) ); |
| } |
| |
| version (unittest) |
| { |
| /* Define a mock BidirectionalRange to use for unittesting. |
| */ |
| |
| struct MockBiRange(C) |
| { |
| this(const(C)[] array) { this.array = array; } |
| const |
| { |
| @property bool empty() { return array.length == 0; } |
| @property C front() { return array[0]; } |
| @property C back() { return array[$ - 1]; } |
| @property size_t opDollar() { return array.length; } |
| } |
| void popFront() { array = array[1 .. $]; } |
| void popBack() { array = array[0 .. $-1]; } |
| @property MockBiRange save() { return this; } |
| private: |
| const(C)[] array; |
| } |
| |
| static assert( isBidirectionalRange!(MockBiRange!(const(char))) ); |
| } |
| |
| private template BaseOf(R) |
| { |
| static if (isRandomAccessRange!R && isSomeChar!(ElementType!R)) |
| alias BaseOf = R; |
| else |
| alias BaseOf = StringTypeOf!R; |
| } |