| // 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; | |
| import std.file : getcwd; | |
| static import std.meta; | |
| import std.range; | |
| import std.traits; | |
| version (OSX) | |
| version = Darwin; | |
| else version (iOS) | |
| version = Darwin; | |
| else version (TVOS) | |
| version = Darwin; | |
| else version (WatchOS) | |
| version = Darwin; | |
| version (StdUnittest) | |
| { | |
| private: | |
| struct TestAliasedString | |
| { | |
| string get() @safe @nogc pure nothrow return scope { return _s; } | |
| alias get this; | |
| @disable this(this); | |
| string _s; | |
| } | |
| bool testAliasedString(alias func, Args...)(scope string s, scope 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; | |
| } | |
| /// | |
| @safe pure nothrow @nogc unittest | |
| { | |
| version (Windows) | |
| { | |
| assert( '/'.isDirSeparator); | |
| assert( '\\'.isDirSeparator); | |
| } | |
| else | |
| { | |
| assert( '/'.isDirSeparator); | |
| assert(!'\\'.isDirSeparator); | |
| } | |
| } | |
| /* 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)); } | |
| do | |
| { | |
| 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 (isSomeFiniteCharInputRange!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; | |
| } | |
| } | |
| @safe 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; | |
| } | |
| } | |
| @safe unittest | |
| { | |
| import std.array; | |
| 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)); | |
| } | |
| @safe unittest | |
| { | |
| import std.array; | |
| assert(trimDirSeparators("//abc//").array == "abc"); | |
| assert(trimDirSeparators("//abc//"d).array == "abc"d); | |
| assert(trimDirSeparators(MockBiRange!char("//abc//")).array == "abc"); | |
| } | |
| /** This `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, `no` on Windows and Mac OS X, and `yes` on all | |
| POSIX systems except Darwin (Linux, *BSD, etc.). | |
| */ | |
| osDefault = osDefaultCaseSensitivity | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| assert(baseName!(CaseSensitive.no)("dir/file.EXT", ".ext") == "file"); | |
| assert(baseName!(CaseSensitive.yes)("dir/file.EXT", ".ext") != "file"); | |
| version (Posix) | |
| assert(relativePath!(CaseSensitive.no)("/FOO/bar", "/foo/baz") == "../bar"); | |
| else | |
| assert(relativePath!(CaseSensitive.no)(`c:\FOO\bar`, `c:\foo\baz`) == `..\bar`); | |
| } | |
| version (Windows) private enum osDefaultCaseSensitivity = false; | |
| else version (Darwin) 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 `suffix` is specified, it will be compared to `path` | |
| 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. | |
| 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)(return scope R path) | |
| if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) && !isSomeString!R) | |
| { | |
| return _baseName(path); | |
| } | |
| /// ditto | |
| auto baseName(C)(return scope C[] path) | |
| if (isSomeChar!C) | |
| { | |
| return _baseName(path); | |
| } | |
| /// ditto | |
| inout(C)[] baseName(CaseSensitive cs = CaseSensitive.osDefault, C, C1) | |
| (return scope 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("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"); | |
| } | |
| } | |
| @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"); | |
| } | |
| private R _baseName(R)(return scope 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]; | |
| } | |
| /** Returns the parent directory of `path`. On Windows, this | |
| includes the drive letter if present. If `path` is a relative path and | |
| the parent directory is the current working directory, returns `"."`. | |
| Params: | |
| path = A path name. | |
| Returns: | |
| A slice of `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)(return scope R path) | |
| if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) && !isSomeString!R) | |
| { | |
| return _dirName(path); | |
| } | |
| /// ditto | |
| auto dirName(C)(return scope C[] path) | |
| if (isSomeChar!C) | |
| { | |
| return _dirName(path); | |
| } | |
| /// | |
| @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"); | |
| } | |
| @safe 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"); | |
| } | |
| private auto _dirName(R)(return scope R path) | |
| { | |
| 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])); | |
| } | |
| /** Returns the root directory of the specified path, or `null` if the | |
| path is not rooted. | |
| Params: | |
| path = A path name. | |
| Returns: | |
| A slice of `path`. | |
| */ | |
| auto rootName(R)(R path) | |
| if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) && !isSomeString!R) | |
| { | |
| return _rootName(path); | |
| } | |
| /// ditto | |
| auto rootName(C)(C[] path) | |
| if (isSomeChar!C) | |
| { | |
| return _rootName(path); | |
| } | |
| /// | |
| @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")); | |
| enum S : string { a = "/foo/bar" } | |
| assert(S.a.rootName == "/"); | |
| char[S.a.length] sa = S.a[]; | |
| assert(sa.rootName == "/"); | |
| } | |
| @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`); | |
| } | |
| } | |
| private auto _rootName(R)(R path) | |
| { | |
| 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]; | |
| } | |
| /** | |
| Get the drive portion of a path. | |
| Params: | |
| path = string or range of characters | |
| Returns: | |
| A slice of `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) && !isSomeString!R) | |
| { | |
| return _driveName(path); | |
| } | |
| /// ditto | |
| auto driveName(C)(C[] path) | |
| if (isSomeChar!C) | |
| { | |
| return _driveName(path); | |
| } | |
| /// | |
| @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:"); | |
| } | |
| } | |
| @safe unittest | |
| { | |
| assert(testAliasedString!driveName("d:/file")); | |
| version (Posix) | |
| immutable result = ""; | |
| else version (Windows) | |
| immutable result = "d:"; | |
| enum S : string { a = "d:/file" } | |
| assert(S.a.driveName == result); | |
| char[S.a.length] sa = S.a[]; | |
| assert(sa.driveName == result); | |
| } | |
| @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:"); | |
| } | |
| } | |
| private auto _driveName(R)(R path) | |
| { | |
| 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]; | |
| } | |
| /** 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) && !isSomeString!R) | |
| { | |
| return _stripDrive(path); | |
| } | |
| /// ditto | |
| auto stripDrive(C)(C[] path) | |
| if (isSomeChar!C) | |
| { | |
| return _stripDrive(path); | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| version (Windows) | |
| { | |
| assert(stripDrive(`d:\dir\file`) == `\dir\file`); | |
| assert(stripDrive(`\\server\share\dir\file`) == `\dir\file`); | |
| } | |
| } | |
| @safe unittest | |
| { | |
| assert(testAliasedString!stripDrive("d:/dir/file")); | |
| version (Posix) | |
| immutable result = "d:/dir/file"; | |
| else version (Windows) | |
| immutable result = "/dir/file"; | |
| enum S : string { a = "d:/dir/file" } | |
| assert(S.a.stripDrive == result); | |
| char[S.a.length] sa = S.a[]; | |
| assert(sa.stripDrive == result); | |
| } | |
| @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); | |
| } | |
| } | |
| private auto _stripDrive(R)(R path) | |
| { | |
| 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; | |
| } | |
| /* 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, `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) && !isSomeString!R) | |
| { | |
| return _stripExtension(path); | |
| } | |
| /// Ditto | |
| auto stripExtension(C)(C[] path) | |
| if (isSomeChar!C) | |
| { | |
| return _stripExtension(path); | |
| } | |
| /// | |
| @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"); | |
| } | |
| @safe unittest | |
| { | |
| assert(testAliasedString!stripExtension("file")); | |
| enum S : string { a = "foo.bar" } | |
| assert(S.a.stripExtension == "foo"); | |
| char[S.a.length] sa = S.a[]; | |
| assert(sa.stripExtension == "foo"); | |
| } | |
| @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"); | |
| } | |
| private auto _stripExtension(R)(R path) | |
| { | |
| immutable i = extSeparatorPos(path); | |
| return i == -1 ? path : path[0 .. i]; | |
| } | |
| /** 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 `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 `path`, but where | |
| the extension has been set to `ext`. | |
| See_Also: | |
| $(LREF withExtension) which does not allocate and returns a lazy range. | |
| */ | |
| immutable(C1)[] setExtension(C1, C2)(in C1[] path, in C2[] ext) | |
| if (isSomeChar!C1 && !is(C1 == immutable) && is(immutable C1 == immutable 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(immutable C1 == immutable 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"); | |
| // https://issues.dlang.org/show_bug.cgi?id=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 `path`'s extension (if any) replaced with `ext`. | |
| * The element encoding type of the returned range will be the same as `path`'s. | |
| * See_Also: | |
| * $(LREF setExtension) | |
| */ | |
| auto withExtension(R, C)(R path, C[] ext) | |
| if (isRandomAccessRange!R && hasSlicing!R && hasLength!R && isSomeChar!(ElementType!R) && | |
| !isSomeString!R && isSomeChar!C) | |
| { | |
| return _withExtension(path, ext); | |
| } | |
| /// Ditto | |
| auto withExtension(C1, C2)(C1[] path, C2[] ext) | |
| if (isSomeChar!C1 && isSomeChar!C2) | |
| { | |
| return _withExtension(path, ext); | |
| } | |
| /// | |
| @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); | |
| } | |
| @safe unittest | |
| { | |
| import std.algorithm.comparison : equal; | |
| assert(testAliasedString!withExtension("file", "ext")); | |
| enum S : string { a = "foo.bar" } | |
| assert(equal(S.a.withExtension(".txt"), "foo.txt")); | |
| char[S.a.length] sa = S.a[]; | |
| assert(equal(sa.withExtension(".txt"), "foo.txt")); | |
| } | |
| private auto _withExtension(R, C)(R path, C[] ext) | |
| { | |
| 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); | |
| } | |
| /** Params: | |
| path = A path name. | |
| ext = The default extension to use. | |
| Returns: The path given by `path`, with the extension given by `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(C1)[] defaultExtension(C1, C2)(in C1[] path, in C2[] ext) | |
| if (isSomeChar!C1 && is(immutable C1 == immutable 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 `path` to `ext` if `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) && | |
| !isSomeString!R && isSomeChar!C) | |
| { | |
| return _withDefaultExtension(path, ext); | |
| } | |
| /// Ditto | |
| auto withDefaultExtension(C1, C2)(C1[] path, C2[] ext) | |
| if (isSomeChar!C1 && isSomeChar!C2) | |
| { | |
| return _withDefaultExtension(path, ext); | |
| } | |
| /// | |
| @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."); | |
| } | |
| @safe unittest | |
| { | |
| import std.algorithm.comparison : equal; | |
| assert(testAliasedString!withDefaultExtension("file", "ext")); | |
| enum S : string { a = "foo" } | |
| assert(equal(S.a.withDefaultExtension(".txt"), "foo.txt")); | |
| char[S.a.length] sa = S.a[]; | |
| assert(equal(sa.withDefaultExtension(".txt"), "foo.txt")); | |
| } | |
| private auto _withDefaultExtension(R, C)(R path, C[] ext) | |
| { | |
| import std.range : only, chain; | |
| import std.utf : byUTF; | |
| alias CR = Unqual!(ElementEncodingType!R); | |
| auto dot = only(CR('.')); | |
| immutable 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); | |
| } | |
| /** 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 `paths` is a forward | |
| range. | |
| Params: | |
| segments = An $(REF_ALTTEXT input range, isInputRange, std,range,primitives) | |
| of segments to assemble the path from. | |
| Returns: The assembled path. | |
| */ | |
| immutable(ElementEncodingType!(ElementType!Range))[] buildPath(Range)(scope 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.dup); } | |
| 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 https://issues.dlang.org/show_bug.cgi?id=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 (`"."` and `".."`) 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...) | |
| @safe pure nothrow | |
| if (isSomeChar!C) | |
| { | |
| import std.array : array; | |
| const(C)[] chained; | |
| foreach (path; paths) | |
| { | |
| if (chained) | |
| chained = chainPath(chained, path).array; | |
| else | |
| chained = path; | |
| } | |
| auto result = asNormalizedPath(chained); | |
| // .array returns a copy, so it is unique | |
| return result.array; | |
| } | |
| /// | |
| @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 https://issues.dlang.org/show_bug.cgi?id=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 (`"."` and `".."`) 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)(return scope 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)(return scope 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 (/). | |
| 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. | |
| */ | |
| 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 | |
| { | |
| version (Posix) | |
| { | |
| 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")); | |
| } | |
| } | |
| @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, `_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` 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 `path` into an absolute path. | |
| The following algorithm is used: | |
| $(OL | |
| $(LI If `path` is empty, return `null`.) | |
| $(LI If `path` is already absolute, return it.) | |
| $(LI Otherwise, append `path` to `base` and return | |
| the result. If `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. | |
| Note that `absolutePath` will not normalize `..` segments. | |
| Use `buildNormalizedPath(absolutePath(path))` if that is desired. | |
| Params: | |
| path = the relative path to transform | |
| base = the base directory of the relative path | |
| Returns: | |
| string of transformed path | |
| Throws: | |
| `Exception` if the specified _base directory is not absolute. | |
| See_Also: | |
| $(LREF asAbsolutePath) which does not allocate | |
| */ | |
| string absolutePath(return scope const 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")); | |
| } | |
| // Ensure that we can call absolute path with scope paramaters | |
| @safe unittest | |
| { | |
| string testAbsPath(scope const string path, scope const string base) { | |
| return absolutePath(path, base); | |
| } | |
| version (Posix) | |
| assert(testAbsPath("some/file", "/foo/bar") == "/foo/bar/some/file"); | |
| version (Windows) | |
| assert(testAbsPath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`); | |
| } | |
| /** Transforms `path` into an absolute path. | |
| The following algorithm is used: | |
| $(OL | |
| $(LI If `path` is empty, return `null`.) | |
| $(LI If `path` is already absolute, return it.) | |
| $(LI Otherwise, append `path` to the current working directory, | |
| which allocates memory.) | |
| ) | |
| Note that `asAbsolutePath` will not normalize `..` segments. | |
| Use `asNormalizedPath(asAbsolutePath(path))` if that is desired. | |
| 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 `path` into a relative path. | |
| The returned path is relative to `base`, which is by default | |
| taken to be the current working directory. If specified, | |
| `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 $(D `.`). | |
| 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 $(D `../`) or $(D `..\`) 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. | |
| 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: | |
| `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; | |
| } | |
| /// | |
| @safe 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`); | |
| } | |
| } | |
| @safe 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)); | |
| } | |
| /// | |
| @safe 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); | |
| } | |
| @safe unittest | |
| { | |
| version (Posix) | |
| { | |
| assert(isBidirectionalRange!(typeof(asRelativePath("foo/bar/baz", "/foo/woo/wee")))); | |
| } | |
| version (Windows) | |
| { | |
| assert(isBidirectionalRange!(typeof(asRelativePath(`c:\foo\bar`, `c:\foo\baz`)))); | |
| } | |
| } | |
| 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); | |
| } | |
| @safe 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"); | |
| } | |
| @safe 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 `cs` template parameter | |
| which, if not specified, is given by $(LREF CaseSensitive)`.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), | |
| `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 `filenameCharCmp!cs`, | |
| where `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), | |
| `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 (isSomeFiniteCharInputRange!Range1 && !isConvertibleToString!Range1 && | |
| isSomeFiniteCharInputRange!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 `*`) | |
| $(TD Matches 0 or more instances of any character.)) | |
| $(TR $(TD `?`) | |
| $(TD Matches exactly one instance of any character.)) | |
| $(TR $(TD `[`$(I chars)`]`) | |
| $(TD Matches one instance of any character that appears | |
| between the brackets.)) | |
| $(TR $(TD `[!`$(I chars)`]`) | |
| $(TD Matches one instance of any character that does not | |
| appear between the brackets after the exclamation mark.)) | |
| $(TR $(TD `{`$(I string1)`,`$(I string2)`,`…`}`) | |
| $(TD Matches either of the specified strings.)) | |
| ) | |
| Individual characters are compared using `filenameCharCmp!cs`, | |
| where `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: | |
| `true` if pattern matches path, `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(immutable C == immutable ElementEncodingType!Range)) | |
| in | |
| { | |
| // Verify that pattern[] is valid | |
| import std.algorithm.searching : balancedParens; | |
| import std.utf : byUTF; | |
| assert(balancedParens(pattern.byUTF!C, '[', ']', 0)); | |
| assert(balancedParens(pattern.byUTF!C, '{', '}', 0)); | |
| } | |
| do | |
| { | |
| 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 | |
| { | |
| import core.memory : pureMalloc, pureFree; | |
| C[] pattmp; | |
| scope(exit) if (pattmp !is null) (() @trusted => pureFree(pattmp.ptr))(); | |
| for (size_t pi = 0; pi < pattern.length; pi++) | |
| { | |
| 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. | |
| pattmp = (() @trusted => | |
| (cast(C*) pureMalloc(C.sizeof * pattern.length))[0 .. 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 @nogc 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)); | |
| // https://issues.dlang.org/show_bug.cgi?id=6634 | |
| assert(!globMatch("foo.di", "*.d")); // 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 `filename` is given by the constant | |
| `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 | |
| `filename` is a string of `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 `filename` must not contain any characters whose integer | |
| representation is in the range 0-31.) | |
| $(LI `filename` must not contain any of the following $(I reserved | |
| characters): `<>:"/\|?*`) | |
| $(LI `filename` may not end with a space ($(D ' ')) or a period | |
| (`'.'`).) | |
| ) | |
| On POSIX, `filename` may not contain a forward slash (`'/'`) or | |
| the null character (`'\0'`). | |
| Params: | |
| filename = string to check | |
| Returns: | |
| `true` if and only if `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; | |
| static 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 `path` is a valid path. | |
| Generally, this function checks that `path` is not empty, and that | |
| each component of the path either satisfies $(LREF isValidFilename) | |
| or is equal to `"."` or `".."`. | |
| $(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 `path` is a colon (`':'`), | |
| the first character is interpreted as a drive letter, and | |
| must be in the range A-Z (case insensitive).) | |
| $(LI If `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 `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 `path` starts with $(D `\\.\`) (Win32 device namespace) | |
| this function returns `false`; such paths are beyond the scope | |
| of this module.) | |
| ) | |
| Params: | |
| path = string or Range of characters to check | |
| Returns: | |
| true if `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 `HOME`. The second way is putting | |
| a username after the tilde (i.e. `~john/Mail`). Here, | |
| the username will be searched for in the user database | |
| (i.e. `/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 `~user` syntax may give different | |
| values from just `~` 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: | |
| `inputPath` with the tilde expanded, or just `inputPath` | |
| if it could not be expanded. | |
| For Windows, `expandTilde` merely returns its argument `inputPath`. | |
| Example: | |
| ----- | |
| void processFile(string path) | |
| { | |
| // Allow calling this function with paths such as ~/foo | |
| auto fullPath = expandTilde(path); | |
| ... | |
| } | |
| ----- | |
| */ | |
| string expandTilde(return scope const string inputPath) @safe nothrow | |
| { | |
| version (Posix) | |
| { | |
| import core.exception : onOutOfMemoryError; | |
| import core.stdc.errno : errno, EBADF, ENOENT, EPERM, ERANGE, ESRCH; | |
| 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) @trusted nothrow | |
| { | |
| import core.stdc.string : strlen; | |
| import std.exception : assumeUnique; | |
| assert(c_path != null); | |
| assert(path.length > 0); | |
| assert(char_pos >= 0); | |
| // Search end of C string | |
| size_t end = strlen(c_path); | |
| const cPathEndsWithDirSep = end && isDirSeparator(c_path[end - 1]); | |
| string cp; | |
| if (char_pos < path.length) | |
| { | |
| // Remove trailing path separator, if any (with special care for root /) | |
| if (cPathEndsWithDirSep && (end > 1 || isDirSeparator(path[char_pos]))) | |
| end--; | |
| // Append something from path | |
| cp = assumeUnique(c_path[0 .. end] ~ path[char_pos .. $]); | |
| } | |
| else | |
| { | |
| // Remove trailing path separator, if any (except for root /) | |
| if (cPathEndsWithDirSep && end > 1) | |
| end--; | |
| // 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) @safe nothrow | |
| { | |
| import core.stdc.stdlib : getenv; | |
| assert(path.length >= 1); | |
| assert(path[0] == '~'); | |
| // Get HOME and use that to replace the tilde. | |
| auto home = () @trusted { return 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) @safe 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 = new char[username_len * char.sizeof]; | |
| 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 (StdUnittest) | |
| uint extra_memory_size = 2; | |
| else | |
| uint extra_memory_size = 5 * 1024; | |
| char[] extra_memory; | |
| passwd result; | |
| loop: while (1) | |
| { | |
| extra_memory.length += extra_memory_size; | |
| // Obtain info from database. | |
| passwd *verify; | |
| errno = 0; | |
| auto passResult = () @trusted { return getpwnam_r( | |
| &username[0], | |
| &result, | |
| &extra_memory[0], | |
| extra_memory.length, | |
| &verify | |
| ); } (); | |
| if (passResult == 0) | |
| { | |
| // Succeeded if verify points at result | |
| if (verify == () @trusted { return &result; } ()) | |
| // username is found | |
| path = combineCPathWithDPath(result.pw_dir, path, last_char); | |
| break; | |
| } | |
| switch (errno) | |
| { | |
| case ERANGE: | |
| // On BSD and OSX, errno can be left at 0 instead of set to ERANGE | |
| case 0: | |
| break; | |
| case ENOENT: | |
| case ESRCH: | |
| case EBADF: | |
| case EPERM: | |
| // The given name or uid was not found. | |
| break loop; | |
| default: | |
| 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. | |
| } | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| version (Posix) | |
| { | |
| import std.process : environment; | |
| auto oldHome = environment["HOME"]; | |
| scope(exit) environment["HOME"] = oldHome; | |
| environment["HOME"] = "dmd/test"; | |
| assert(expandTilde("~/") == "dmd/test/"); | |
| assert(expandTilde("~") == "dmd/test"); | |
| } | |
| } | |
| @safe unittest | |
| { | |
| version (Posix) | |
| { | |
| static if (__traits(compiles, { import std.process : executeShell; })) | |
| import std.process : executeShell; | |
| import std.process : environment; | |
| import std.string : strip; | |
| // 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"); | |
| // The same, but with a variable set to root. | |
| environment["HOME"] = "/"; | |
| assert(expandTilde("~/") == "/"); | |
| assert(expandTilde("~") == "/"); | |
| // Recover original HOME variable before continuing. | |
| if (oldHome !is null) environment["HOME"] = oldHome; | |
| else environment.remove("HOME"); | |
| static if (is(typeof(executeShell))) | |
| { | |
| immutable tildeUser = "~" ~ environment.get("USER"); | |
| immutable path = executeShell("echo " ~ tildeUser).output.strip(); | |
| immutable expTildeUser = expandTilde(tildeUser); | |
| assert(expTildeUser == path, expTildeUser); | |
| immutable expTildeUserSlash = expandTilde(tildeUser ~ "/"); | |
| immutable pathSlash = path[$-1] == '/' ? path : path ~ "/"; | |
| assert(expTildeUserSlash == pathSlash, expTildeUserSlash); | |
| } | |
| assert(expandTilde("~Idontexist/hey") == "~Idontexist/hey"); | |
| } | |
| } | |
| @safe unittest | |
| { | |
| version (Posix) | |
| { | |
| import std.process : environment; | |
| string testPath(scope const string source_path) { | |
| return source_path.expandTilde; | |
| } | |
| auto oldHome = environment["HOME"]; | |
| scope(exit) environment["HOME"] = oldHome; | |
| environment["HOME"] = "dmd/test"; | |
| assert(testPath("~/") == "dmd/test/"); | |
| assert(testPath("~") == "dmd/test"); | |
| } | |
| } | |
| version (StdUnittest) | |
| { | |
| private: | |
| /* 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]; } | |
| alias opDollar = 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; | |
| } | |
| /* 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; | |
| } | |
| } | |
| @safe unittest | |
| { | |
| static assert( isRandomAccessRange!(MockRange!(const(char))) ); | |
| 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; | |
| } |