// 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 $(XREF file,isDir) and $(XREF file,exists). 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. Upgrading: $(WEB digitalmars.com/d/1.0/phobos/std_path.html#fnmatch) can be replaced with $(D globMatch). Authors: Lars Tandle Kyllingstad, $(WEB digitalmars.com, Walter Bright), Grzegorz Adam Hankiewicz, Thomas K$(UUML)hne, $(WEB erdani.org, Andrei Alexandrescu) Copyright: Copyright (c) 2000-2014, the authors. All rights reserved. License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0) Source: $(PHOBOSSRC std/_path.d) Macros: WIKI = Phobos/StdPath */ module std.path; // FIXME import std.file; //: getcwd; import std.range.primitives; import std.traits; /** String used to separate directory names in a path. Under POSIX this is a slash, under Windows a backslash. */ version(Posix) enum string dirSeparator = "/"; else version(Windows) enum string dirSeparator = "\\"; else static assert (0, "unsupported platform"); /** Path separator string. A colon under POSIX, a semicolon under Windows. */ version(Posix) enum string pathSeparator = ":"; else version(Windows) enum string pathSeparator = ";"; else static assert (0, "unsupported platform"); /** Determines whether the given character is a directory separator. On Windows, this includes both $(D `\`) and $(D `/`). On POSIX, it's just $(D `/`). */ bool isDirSeparator(dchar c) @safe pure nothrow @nogc { if (c == '/') return true; version(Windows) if (c == '\\') return true; return false; } /* Determines whether the given character is a drive separator. On Windows, this is true if c is the ':' character that separates the drive letter from the rest of the path. On POSIX, this always returns false. */ private bool isDriveSeparator(dchar c) @safe pure nothrow @nogc { version(Windows) return c == ':'; else return false; } /* Combines the isDirSeparator and isDriveSeparator tests. */ version(Windows) private bool isSeparator(dchar c) @safe pure nothrow @nogc { return isDirSeparator(c) || isDriveSeparator(c); } version(Posix) private alias isSeparator = isDirSeparator; /* Helper function that determines the position of the last drive/directory separator in a string. Returns -1 if none is found. */ private ptrdiff_t lastSeparator(R)(const 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)(const 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)(const R path) if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || isNarrowString!R) in { assert (isUNC(path)); } body { ptrdiff_t i = 3; while (i < path.length && !isDirSeparator(path[i])) ++i; if (i < path.length) { auto j = i; do { ++j; } while (j < path.length && isDirSeparator(path[j])); if (j < path.length) { do { ++j; } while (j < path.length && !isDirSeparator(path[j])); i = j; } } return i; } private bool hasDrive(R)(const R path) if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || isNarrowString!R) { return path.length >= 2 && isDriveSeparator(path[1]); } private bool isDriveRoot(R)(const 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 (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || isNarrowString!R) { int i = 0; while (i < path.length && isDirSeparator(path[i])) ++i; return path[i .. path.length]; } private auto rtrimDirSeparators(R)(R path) if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || isNarrowString!R) { auto i = (cast(ptrdiff_t) path.length) - 1; while (i >= 0 && isDirSeparator(path[i])) --i; return path[0 .. i+1]; } private auto trimDirSeparators(R)(R path) if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || isNarrowString!R) { return ltrimDirSeparators(rtrimDirSeparators(path)); } /** This $(D enum) is used as a template argument to functions which compare file names, and determines whether the comparison is case sensitive or not. */ enum CaseSensitive : bool { /// File names are case insensitive no = false, /// File names are case sensitive yes = true, /** The default (or most common) setting for the current platform. That is, $(D no) on Windows and Mac OS X, and $(D yes) on all POSIX systems except OS X (Linux, *BSD, etc.). */ osDefault = osDefaultCaseSensitivity } version (Windows) private enum osDefaultCaseSensitivity = false; else version (OSX) private enum osDefaultCaseSensitivity = false; else version (Posix) private enum osDefaultCaseSensitivity = true; else static assert (0); /** Returns the name of a file, without any leading directory and with an optional suffix chopped off. If $(D suffix) is specified, it will be compared to $(D path) using $(D filenameCmp!cs), where $(D cs) is an optional template parameter determining whether the comparison is case sensitive or not. See the $(LREF filenameCmp) documentation for details. Examples: --- assert (baseName("dir/file.ext") == "file.ext"); assert (baseName("dir/file.ext", ".ext") == "file"); assert (baseName("dir/file.ext", ".xyz") == "file.ext"); assert (baseName("dir/filename", "name") == "file"); assert (baseName("dir/subdir/") == "subdir"); version (Windows) { assert (baseName(`d:file.ext`) == "file.ext"); assert (baseName(`d:\dir\file.ext`) == "file.ext"); } --- Note: This function $(I only) strips away the specified suffix, which doesn't necessarily have to represent an extension. To remove the extension from a path, regardless of what the extension is, use $(LREF stripExtension). To obtain the filename without leading directories and without an extension, combine the functions like this: --- assert (baseName(stripExtension("dir/file.ext")) == "file"); --- Standards: This function complies with $(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html, the POSIX requirements for the 'basename' shell utility) (with suitable adaptations for Windows paths). */ auto baseName(R)(R path) if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || is(StringTypeOf!R)) { auto p1 = stripDrive!(BaseOf!R)(path); if (p1.empty) { version (Windows) if (isUNC!(BaseOf!R)(path)) { return path[0..1]; } static if (is(StringTypeOf!R)) return StringTypeOf!R.init[]; // which is null else return p1; // which is empty } auto p2 = rtrimDirSeparators(p1); if (p2.empty) return p1[0 .. 1]; return p2[lastSeparator(p2)+1 .. p2.length]; } /// ditto inout(C)[] baseName(CaseSensitive cs = CaseSensitive.osDefault, C, C1) (inout(C)[] path, in C1[] suffix) @safe pure //TODO: nothrow (because of filenameCmp()) if (isSomeChar!C && isSomeChar!C1) { auto p = baseName(path); if (p.length > suffix.length && filenameCmp!cs(p[$-suffix.length .. $], suffix) == 0) { return p[0 .. $-suffix.length]; } else return p; } 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"); } /** Returns the directory part of a path. On Windows, this includes the drive letter if present. This function performs a memory allocation if and only if $(D path) does not have a directory (in which case a new string is needed to hold the returned current-directory symbol, $(D ".")). Examples: --- assert (dirName("file") == "."); assert (dirName("dir/file") == "dir"); assert (dirName("/file") == "/"); assert (dirName("dir/subdir/") == "dir"); version (Windows) { assert (dirName("d:file") == "d:"); assert (dirName(`d:\dir\file`) == `d:\dir`); assert (dirName(`d:\file`) == `d:\`); assert (dirName(`dir\subdir\`) == `dir`); } --- 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). */ C[] dirName(C)(C[] path) //TODO: @safe (BUG 6169) pure nothrow (because of to()) if (isSomeChar!C) { import std.conv : to; if (path.empty) return to!(typeof(return))("."); auto p = rtrimDirSeparators(path); if (p.empty) return path[0 .. 1]; version (Windows) { if (isUNC(p) && uncRootLength(p) == p.length) return p; if (p.length == 2 && isDriveSeparator(p[1]) && path.length > 2) return path[0 .. 3]; } auto i = lastSeparator(p); if (i == -1) return to!(typeof(return))("."); if (i == 0) return p[0 .. 1]; version (Windows) { // If the directory part is either d: or d:\, don't // chop off the last symbol. if (isDriveSeparator(p[i]) || isDriveSeparator(p[i-1])) return p[0 .. i+1]; } // Remove any remaining trailing (back)slashes. return rtrimDirSeparators(p[0 .. i]); } 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`); } static assert (dirName("dir/file") == "dir"); } /** Returns the root directory of the specified path, or $(D null) if the path is not rooted. Examples: --- assert (rootName("foo") is null); assert (rootName("/foo") == "/"); version (Windows) { assert (rootName(`\foo`) == `\`); assert (rootName(`c:\foo`) == `c:\`); assert (rootName(`\\server\share\foo`) == `\\server\share`); } --- */ inout(C)[] rootName(C)(inout(C)[] path) @safe pure nothrow @nogc if (isSomeChar!C) { if (path.empty) return null; 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)); return null; } 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`); } } /** Returns the drive of a path, or $(D null) if the drive is not specified. In the case of UNC paths, the network share is returned. Always returns $(D null) on POSIX. Examples: --- version (Windows) { assert (driveName(`d:\file`) == "d:"); assert (driveName(`\\server\share\file`) == `\\server\share`); assert (driveName(`dir\file`).empty); } --- */ inout(C)[] driveName(C)(inout(C)[] path) @safe pure nothrow @nogc if (isSomeChar!C) { version (Windows) { if (hasDrive(path)) return path[0 .. 2]; else if (isUNC(path)) return path[0 .. uncRootLength(path)]; } return null; } unittest { 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:"); } } /** Strips the drive from a Windows path. On POSIX, the path is returned unaltered. Example: --- version (Windows) { assert (stripDrive(`d:\dir\file`) == `\dir\file`); assert (stripDrive(`\\server\share\dir\file`) == `\dir\file`); } --- */ auto stripDrive(R)(inout R path) if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || is(StringTypeOf!R)) { version(Windows) { if (hasDrive!(BaseOf!R)(path)) return path[2 .. path.length]; else if (isUNC!(BaseOf!R)(path)) return path[uncRootLength!(BaseOf!R)(path) .. path.length]; } return path; } unittest { version(Windows) { assert (stripDrive(`d:\dir\file`) == `\dir\file`); assert (stripDrive(`\\server\share\dir\file`) == `\dir\file`); static assert (stripDrive(`d:\dir\file`) == `\dir\file`); auto r = MockRange!(immutable(char))(`d:\dir\file`); auto s = r.stripDrive(); foreach (i, c; `\dir\file`) assert(s[i] == c); } version(Posix) { assert (stripDrive(`d:\dir\file`) == `d:\dir\file`); auto r = MockRange!(immutable(char))(`d:\dir\file`); auto s = r.stripDrive(); foreach (i, c; `d:\dir\file`) assert(s[i] == c); } } /* Helper function that returns the position of the filename/extension separator dot in path. If not found, returns -1. */ private ptrdiff_t extSeparatorPos(R)(const R path) if (isRandomAccessRange!R && hasSlicing!R && isSomeChar!(ElementType!R) || isNarrowString!R) { auto i = (cast(ptrdiff_t) path.length) - 1; while (i >= 0 && !isSeparator(path[i])) { if (path[i] == '.' && i > 0 && !isSeparator(path[i-1])) return i; --i; } return -1; } /** Returns the _extension part of a file name, including the dot. If there is no _extension, $(D null) is returned. Examples: --- assert (extension("file").empty); assert (extension("file.ext") == ".ext"); assert (extension("file.ext1.ext2") == ".ext2"); assert (extension("file.") == "."); assert (extension(".file").empty); assert (extension(".file.ext") == ".ext"); --- */ 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]; } unittest { 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"); assert (extension("dir/file"d.dup).empty); assert (extension("dir/file.") == "."); assert (extension("dir/file.ext") == ".ext"); assert (extension("dir/file.ext1.ext2"w) == ".ext2"); assert (extension("dir/.foo"d).empty); assert (extension("dir/.foo.ext".dup) == ".ext"); version(Windows) { assert (extension(`dir\file`).empty); assert (extension(`dir\file.`) == "."); assert (extension(`dir\file.ext`) == `.ext`); assert (extension(`dir\file.ext1.ext2`) == `.ext2`); assert (extension(`dir\.foo`).empty); assert (extension(`dir\.foo.ext`) == `.ext`); assert (extension(`d:file`).empty); assert (extension(`d:file.`) == "."); assert (extension(`d:file.ext`) == `.ext`); assert (extension(`d:file.ext1.ext2`) == `.ext2`); assert (extension(`d:.foo`).empty); assert (extension(`d:.foo.ext`) == `.ext`); } static assert (extension("file").empty); static assert (extension("file.ext") == ".ext"); { 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); } /** Returns slice of path[] with the extension stripped off. Examples: --- 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"); --- */ inout(C)[] stripExtension(C)(inout(C)[] path) @safe pure nothrow @nogc if (isSomeChar!C) { auto i = extSeparatorPos(path); if (i == -1) return path; else return path[0 .. i]; } unittest { assert (stripExtension("file") == "file"); assert (stripExtension("file.ext"w) == "file"); assert (stripExtension("file.ext1.ext2"d) == "file.ext1"); assert (stripExtension(".foo".dup) == ".foo"); assert (stripExtension(".foo.ext"w.dup) == ".foo"); assert (stripExtension("dir/file"d.dup) == "dir/file"); assert (stripExtension("dir/file.ext") == "dir/file"); assert (stripExtension("dir/file.ext1.ext2"w) == "dir/file.ext1"); assert (stripExtension("dir/.foo"d) == "dir/.foo"); assert (stripExtension("dir/.foo.ext".dup) == "dir/.foo"); version(Windows) { assert (stripExtension("dir\\file") == "dir\\file"); assert (stripExtension("dir\\file.ext") == "dir\\file"); assert (stripExtension("dir\\file.ext1.ext2") == "dir\\file.ext1"); assert (stripExtension("dir\\.foo") == "dir\\.foo"); assert (stripExtension("dir\\.foo.ext") == "dir\\.foo"); assert (stripExtension("d:file") == "d:file"); assert (stripExtension("d:file.ext") == "d:file"); assert (stripExtension("d:file.ext1.ext2") == "d:file.ext1"); assert (stripExtension("d:.foo") == "d:.foo"); assert (stripExtension("d:.foo.ext") == "d:.foo"); } static assert (stripExtension("file") == "file"); static assert (stripExtension("file.ext"w) == "file"); } /** Returns a string containing the _path given by $(D path), but where the extension has been set to $(D ext). If the filename already has an extension, it is replaced. If not, the extension is simply appended to the filename. Including a leading dot in $(D ext) is optional. If the extension is empty, this function is equivalent to $(LREF stripExtension). This function normally allocates a new string (the possible exception being the case when path is immutable and doesn't already have an extension). Examples: --- assert (setExtension("file", "ext") == "file.ext"); assert (setExtension("file", ".ext") == "file.ext"); assert (setExtension("file.old", "") == "file"); assert (setExtension("file.old", "new") == "file.new"); assert (setExtension("file.old", ".new") == "file.new"); --- */ immutable(Unqual!C1)[] setExtension(C1, C2)(in C1[] path, in C2[] ext) @trusted pure nothrow if (isSomeChar!C1 && !is(C1 == immutable) && is(Unqual!C1 == Unqual!C2)) { if (ext.length > 0 && ext[0] != '.') return cast(typeof(return))(stripExtension(path)~'.'~ext); else return cast(typeof(return))(stripExtension(path)~ext); } ///ditto immutable(C1)[] setExtension(C1, C2)(immutable(C1)[] path, const(C2)[] ext) @trusted pure nothrow if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2)) { if (ext.length == 0) return stripExtension(path); // Optimised for the case where path is immutable and has no extension if (ext.length > 0 && ext[0] == '.') ext = ext[1 .. $]; auto i = extSeparatorPos(path); if (i == -1) { path ~= '.'; path ~= ext; return path; } else if (i == path.length - 1) { path ~= ext; return path; } else { return cast(typeof(return))(path[0 .. i+1] ~ ext); } } 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"); assert (setExtension("file"w.dup, "ext"w) == "file.ext"); assert (setExtension("file"w.dup, ".ext"w) == "file.ext"); assert (setExtension("file."w, "ext"w.dup) == "file.ext"); assert (setExtension("file."w, ".ext"w.dup) == "file.ext"); assert (setExtension("file.old"d.dup, "new"d) == "file.new"); assert (setExtension("file.old"d.dup, ".new"d) == "file.new"); static assert (setExtension("file", "ext") == "file.ext"); static assert (setExtension("file.old", "new") == "file.new"); static assert (setExtension("file"w.dup, "ext"w) == "file.ext"); static assert (setExtension("file.old"d.dup, "new"d) == "file.new"); // Issue 10601 assert (setExtension("file", "") == "file"); assert (setExtension("file.ext", "") == "file"); } /** Returns the _path given by $(D path), with the extension given by $(D ext) appended if the path doesn't already have one. Including the dot in the extension is optional. This function always allocates a new string, except in the case when path is immutable and already has an extension. Examples: --- 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"); --- */ immutable(Unqual!C1)[] defaultExtension(C1, C2)(in C1[] path, in C2[] ext) @trusted pure // TODO: nothrow (because of to()) if (isSomeChar!C1 && is(Unqual!C1 == Unqual!C2)) { import std.conv : to; auto i = extSeparatorPos(path); if (i == -1) { if (ext.length > 0 && ext[0] == '.') return cast(typeof(return))(path~ext); else return cast(typeof(return))(path~'.'~ext); } else return to!(typeof(return))(path); } 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"); 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"); } /** Combines one or more path segments. This function takes a set of path segments, given as an input range of string elements or as a set of string arguments, and concatenates them with each other. Directory separators are inserted between segments if necessary. If any of the path segments are absolute (as defined by $(LREF isAbsolute)), the preceding segments will be dropped. On Windows, if one of the path segments are rooted, but not absolute (e.g. $(D `\foo`)), all preceding path segments down to the previous root will be dropped. (See below for an example.) This function always allocates memory to hold the resulting path. The variadic overload is guaranteed to only perform a single allocation, as is the range version if $(D paths) is a forward range. */ immutable(ElementEncodingType!(ElementType!Range))[] buildPath(Range)(Range segments) if (isInputRange!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); } if (pos > 0) { if (isRooted(segment)) { version (Posix) { pos = 0; } else version (Windows) { if (isAbsolute(segment)) pos = 0; else { pos = rootName(buf[0 .. pos]).length; if (pos > 0 && isDirSeparator(buf[pos-1])) --pos; } } } else if (!isDirSeparator(buf[pos-1])) buf[pos++] = dirSeparator[0]; } buf[pos .. pos + segment.length] = segment[]; pos += segment.length; } 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); } /// 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`); } } unittest // non-documented { import std.range; // ir() wraps an array in a plain (i.e. non-forward) input range, so that // we can test both code paths InputRange!(C[]) ir(C)(C[][] p...) { return inputRangeObject(p); } version (Posix) { assert (buildPath("foo") == "foo"); assert (buildPath("/foo/") == "/foo/"); assert (buildPath("foo", "bar") == "foo/bar"); assert (buildPath("foo", "bar", "baz") == "foo/bar/baz"); assert (buildPath("foo/".dup, "bar") == "foo/bar"); assert (buildPath("foo///", "bar".dup) == "foo///bar"); assert (buildPath("/foo"w, "bar"w) == "/foo/bar"); assert (buildPath("foo"w.dup, "/bar"w) == "/bar"); assert (buildPath("foo"w, "bar/"w.dup) == "foo/bar/"); assert (buildPath("/"d, "foo"d) == "/foo"); assert (buildPath(""d.dup, "foo"d) == "foo"); assert (buildPath("foo"d, ""d.dup) == "foo"); assert (buildPath("foo", "bar".dup, "baz") == "foo/bar/baz"); assert (buildPath("foo"w, "/bar"w, "baz"w.dup) == "/bar/baz"); static assert (buildPath("foo", "bar", "baz") == "foo/bar/baz"); static assert (buildPath("foo", "/bar", "baz") == "/bar/baz"); // The following are mostly duplicates of the above, except that the // range version does not accept mixed constness. assert (buildPath(ir("foo")) == "foo"); assert (buildPath(ir("/foo/")) == "/foo/"); assert (buildPath(ir("foo", "bar")) == "foo/bar"); assert (buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz"); assert (buildPath(ir("foo/".dup, "bar".dup)) == "foo/bar"); assert (buildPath(ir("foo///".dup, "bar".dup)) == "foo///bar"); assert (buildPath(ir("/foo"w, "bar"w)) == "/foo/bar"); assert (buildPath(ir("foo"w.dup, "/bar"w.dup)) == "/bar"); assert (buildPath(ir("foo"w.dup, "bar/"w.dup)) == "foo/bar/"); assert (buildPath(ir("/"d, "foo"d)) == "/foo"); assert (buildPath(ir(""d.dup, "foo"d.dup)) == "foo"); assert (buildPath(ir("foo"d, ""d)) == "foo"); assert (buildPath(ir("foo", "bar", "baz")) == "foo/bar/baz"); assert (buildPath(ir("foo"w.dup, "/bar"w.dup, "baz"w.dup)) == "/bar/baz"); } version (Windows) { assert (buildPath("foo") == "foo"); assert (buildPath(`\foo/`) == `\foo/`); assert (buildPath("foo", "bar", "baz") == `foo\bar\baz`); assert (buildPath("foo", `\bar`) == `\bar`); assert (buildPath(`c:\foo`, "bar") == `c:\foo\bar`); assert (buildPath("foo"w, `d:\bar`w.dup) == `d:\bar`); assert (buildPath(`c:\foo\bar`, `\baz`) == `c:\baz`); assert (buildPath(`\\foo\bar\baz`d, `foo`d, `\bar`d) == `\\foo\bar\bar`d); static assert (buildPath("foo", "bar", "baz") == `foo\bar\baz`); static assert (buildPath("foo", `c:\bar`, "baz") == `c:\bar\baz`); assert (buildPath(ir("foo")) == "foo"); assert (buildPath(ir(`\foo/`)) == `\foo/`); assert (buildPath(ir("foo", "bar", "baz")) == `foo\bar\baz`); assert (buildPath(ir("foo", `\bar`)) == `\bar`); assert (buildPath(ir(`c:\foo`, "bar")) == `c:\foo\bar`); assert (buildPath(ir("foo"w.dup, `d:\bar`w.dup)) == `d:\bar`); assert (buildPath(ir(`c:\foo\bar`, `\baz`)) == `c:\baz`); assert (buildPath(ir(`\\foo\bar\baz`d, `foo`d, `\bar`d)) == `\\foo\bar\bar`d); } // Test that allocation works as it should. auto manyShort = "aaa".repeat(1000).array(); auto manyShortCombined = join(manyShort, dirSeparator); assert (buildPath(manyShort) == manyShortCombined); assert (buildPath(ir(manyShort)) == manyShortCombined); auto fewLong = 'b'.repeat(500).array().repeat(10).array(); auto fewLongCombined = join(fewLong, dirSeparator); assert (buildPath(fewLong) == fewLongCombined); assert (buildPath(ir(fewLong)) == fewLongCombined); } unittest { // Test for issue 7397 string[] ary = ["a", "b"]; version (Posix) { assert (buildPath(ary) == "a/b"); } else version (Windows) { assert (buildPath(ary) == `a\b`); } } /** Performs the same task as $(LREF buildPath), while at the same time resolving current/parent directory symbols ($(D ".") and $(D "..")) and removing superfluous directory separators. It will return "." if the path leads to the starting directory. On Windows, slashes are replaced with backslashes. Using buildNormalizedPath on null paths will always return null. Note that this function does not resolve symbolic links. This function always allocates memory to hold the resulting path. Examples: --- 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`); } --- */ immutable(C)[] buildNormalizedPath(C)(const(C[])[] paths...) @trusted pure nothrow if (isSomeChar!C) { import core.stdc.stdlib; //Remove empty fields bool allEmpty = true; foreach (ref const(C[]) path ; paths) { if (path !is null) { allEmpty = false; break; } } if (allEmpty) return null; auto paths2 = new const(C)[][](paths.length); //(cast(const(C)[]*)alloca((const(C)[]).sizeof * paths.length))[0 .. paths.length]; // Check whether the resulting path will be absolute or rooted, // calculate its maximum length, and discard segments we won't use. typeof(paths[0][0])[] rootElement; int numPaths = 0; bool seenAbsolute; size_t segmentLengthSum = 0; foreach (i; 0 .. paths.length) { auto p = paths[i]; if (p.empty) continue; else if (isRooted(p)) { immutable thisIsAbsolute = isAbsolute(p); if (thisIsAbsolute || !seenAbsolute) { if (thisIsAbsolute) seenAbsolute = true; rootElement = rootName(p); paths2[0] = p[rootElement.length .. $]; numPaths = 1; segmentLengthSum = paths2[0].length; } else { paths2[0] = p; numPaths = 1; segmentLengthSum = p.length; } } else { paths2[numPaths++] = p; segmentLengthSum += p.length; } } if (rootElement.length + segmentLengthSum == 0) return null; paths2 = paths2[0 .. numPaths]; immutable rooted = !rootElement.empty; assert (rooted || !seenAbsolute); // absolute => rooted // Allocate memory for the resulting path, including room for // extra dir separators auto fullPath = new C[rootElement.length + segmentLengthSum + paths2.length]; // Copy the root element into fullPath, and let relPart be // the remaining slice. typeof(fullPath) relPart; if (rooted) { // For Windows, we also need to perform normalization on // the root element. version (Posix) { fullPath[0 .. rootElement.length] = rootElement[]; } else version (Windows) { foreach (i, c; rootElement) { if (isDirSeparator(c)) { static assert (dirSeparator.length == 1); fullPath[i] = dirSeparator[0]; } else fullPath[i] = c; } } else static assert (0); // If the root element doesn't end with a dir separator, // we add one. if (!isDirSeparator(rootElement[$-1])) { static assert (dirSeparator.length == 1); fullPath[rootElement.length] = dirSeparator[0]; relPart = fullPath[rootElement.length + 1 .. $]; } else { relPart = fullPath[rootElement.length .. $]; } } else relPart = fullPath; // Now, we have ensured that all segments in path are relative to the // root we found earlier. bool hasParents = rooted; ptrdiff_t i; foreach (path; paths2) { path = trimDirSeparators(path); enum Prev { nonSpecial, dirSep, dot, doubleDot } Prev prev = Prev.dirSep; foreach (j; 0 .. path.length+1) { // Fake a dir separator between path segments immutable c = (j == path.length ? dirSeparator[0] : path[j]); if (isDirSeparator(c)) { final switch (prev) { case Prev.doubleDot: if (hasParents) { while (i > 0 && !isDirSeparator(relPart[i-1])) --i; if (i > 0) --i; // skip the dir separator while (i > 0 && !isDirSeparator(relPart[i-1])) --i; if (i == 0) hasParents = rooted; } else { relPart[i++] = '.'; relPart[i++] = '.'; static assert (dirSeparator.length == 1); relPart[i++] = dirSeparator[0]; } break; case Prev.dot: while (i > 0 && !isDirSeparator(relPart[i-1])) --i; break; case Prev.nonSpecial: static assert (dirSeparator.length == 1); relPart[i++] = dirSeparator[0]; hasParents = true; break; case Prev.dirSep: break; } prev = Prev.dirSep; } else if (c == '.') { final switch (prev) { case Prev.dirSep: prev = Prev.dot; break; case Prev.dot: prev = Prev.doubleDot; break; case Prev.doubleDot: prev = Prev.nonSpecial; relPart[i .. i+3] = "..."; i += 3; break; case Prev.nonSpecial: relPart[i] = '.'; ++i; break; } } else { final switch (prev) { case Prev.doubleDot: relPart[i] = '.'; ++i; goto case; case Prev.dot: relPart[i] = '.'; ++i; break; case Prev.dirSep: break; case Prev.nonSpecial: break; } relPart[i] = c; ++i; prev = Prev.nonSpecial; } } } // Return path, including root element and excluding the // final dir separator. immutable len = (relPart.ptr - fullPath.ptr) + (i > 0 ? i - 1 : 0); if (len == 0) { fullPath.length = 1; fullPath[0] = '.'; } else fullPath = fullPath[0 .. len]; version (Windows) { // On Windows, if the path is on the form `\\server\share`, // with no further segments, normalization will have turned it // into `\\server\share\`. If so, we need to remove the final // backslash. if (isUNC(fullPath) && uncRootLength(fullPath) == fullPath.length - 1) fullPath = fullPath[0 .. $-1]; } return cast(typeof(return)) fullPath; } unittest { assert (buildNormalizedPath("") is null); assert (buildNormalizedPath("foo") == "foo"); assert (buildNormalizedPath(".") == "."); 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"); // Examples in docs: 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"); assert (buildNormalizedPath("foo", "", "bar") == "foo/bar"); assert (buildNormalizedPath("foo", null, "bar") == "foo/bar"); //Curent dir path assert (buildNormalizedPath("./") == "."); assert (buildNormalizedPath("././") == "."); assert (buildNormalizedPath("./foo/..") == "."); assert (buildNormalizedPath("foo/..") == "."); } 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`); // Examples in docs: 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`); assert (buildNormalizedPath("foo", "", "bar") == `foo\bar`); assert (buildNormalizedPath("foo", null, "bar") == `foo\bar`); //Curent dir path assert (buildNormalizedPath(`.\`) == "."); assert (buildNormalizedPath(`.\.\`) == "."); assert (buildNormalizedPath(`.\foo\..`) == "."); assert (buildNormalizedPath(`foo\..`) == "."); } else static assert (0); } unittest { version (Posix) { // Trivial assert (buildNormalizedPath("").empty); assert (buildNormalizedPath("foo/bar") == "foo/bar"); // Correct handling of leading slashes assert (buildNormalizedPath("/") == "/"); assert (buildNormalizedPath("///") == "/"); assert (buildNormalizedPath("////") == "/"); assert (buildNormalizedPath("/foo/bar") == "/foo/bar"); assert (buildNormalizedPath("//foo/bar") == "/foo/bar"); assert (buildNormalizedPath("///foo/bar") == "/foo/bar"); assert (buildNormalizedPath("////foo/bar") == "/foo/bar"); // Correct handling of single-dot symbol (current directory) assert (buildNormalizedPath("/./foo") == "/foo"); assert (buildNormalizedPath("/foo/./bar") == "/foo/bar"); assert (buildNormalizedPath("./foo") == "foo"); assert (buildNormalizedPath("././foo") == "foo"); assert (buildNormalizedPath("foo/././bar") == "foo/bar"); // Correct handling of double-dot symbol (previous directory) assert (buildNormalizedPath("/foo/../bar") == "/bar"); assert (buildNormalizedPath("/foo/../../bar") == "/bar"); assert (buildNormalizedPath("/../foo") == "/foo"); assert (buildNormalizedPath("/../../foo") == "/foo"); assert (buildNormalizedPath("/foo/..") == "/"); assert (buildNormalizedPath("/foo/../..") == "/"); assert (buildNormalizedPath("foo/../bar") == "bar"); assert (buildNormalizedPath("foo/../../bar") == "../bar"); assert (buildNormalizedPath("../foo") == "../foo"); assert (buildNormalizedPath("../../foo") == "../../foo"); assert (buildNormalizedPath("../foo/../bar") == "../bar"); assert (buildNormalizedPath(".././../foo") == "../../foo"); assert (buildNormalizedPath("foo/bar/..") == "foo"); assert (buildNormalizedPath("/foo/../..") == "/"); // The ultimate path assert (buildNormalizedPath("/foo/../bar//./../...///baz//") == "/.../baz"); static assert (buildNormalizedPath("/foo/../bar//./../...///baz//") == "/.../baz"); } else version (Windows) { // Trivial assert (buildNormalizedPath("").empty); assert (buildNormalizedPath(`foo\bar`) == `foo\bar`); assert (buildNormalizedPath("foo/bar") == `foo\bar`); // Correct handling of absolute paths assert (buildNormalizedPath("/") == `\`); assert (buildNormalizedPath(`\`) == `\`); assert (buildNormalizedPath(`\\\`) == `\`); assert (buildNormalizedPath(`\\\\`) == `\`); assert (buildNormalizedPath(`\foo\bar`) == `\foo\bar`); assert (buildNormalizedPath(`\\foo`) == `\\foo`); assert (buildNormalizedPath(`\\foo\\`) == `\\foo`); assert (buildNormalizedPath(`\\foo/bar`) == `\\foo\bar`); assert (buildNormalizedPath(`\\\foo\bar`) == `\foo\bar`); assert (buildNormalizedPath(`\\\\foo\bar`) == `\foo\bar`); assert (buildNormalizedPath(`c:\`) == `c:\`); assert (buildNormalizedPath(`c:\foo\bar`) == `c:\foo\bar`); assert (buildNormalizedPath(`c:\\foo\bar`) == `c:\foo\bar`); // Correct handling of single-dot symbol (current directory) assert (buildNormalizedPath(`\./foo`) == `\foo`); assert (buildNormalizedPath(`\foo/.\bar`) == `\foo\bar`); assert (buildNormalizedPath(`.\foo`) == `foo`); assert (buildNormalizedPath(`./.\foo`) == `foo`); assert (buildNormalizedPath(`foo\.\./bar`) == `foo\bar`); // Correct handling of double-dot symbol (previous directory) assert (buildNormalizedPath(`\foo\..\bar`) == `\bar`); assert (buildNormalizedPath(`\foo\../..\bar`) == `\bar`); assert (buildNormalizedPath(`\..\foo`) == `\foo`); assert (buildNormalizedPath(`\..\..\foo`) == `\foo`); assert (buildNormalizedPath(`\foo\..`) == `\`); assert (buildNormalizedPath(`\foo\../..`) == `\`); assert (buildNormalizedPath(`foo\..\bar`) == `bar`); assert (buildNormalizedPath(`foo\..\../bar`) == `..\bar`); assert (buildNormalizedPath(`..\foo`) == `..\foo`); assert (buildNormalizedPath(`..\..\foo`) == `..\..\foo`); assert (buildNormalizedPath(`..\foo\..\bar`) == `..\bar`); assert (buildNormalizedPath(`..\.\..\foo`) == `..\..\foo`); assert (buildNormalizedPath(`foo\bar\..`) == `foo`); assert (buildNormalizedPath(`\foo\..\..`) == `\`); assert (buildNormalizedPath(`c:\foo\..\..`) == `c:\`); // Correct handling of non-root path with drive specifier assert (buildNormalizedPath(`c:foo`) == `c:foo`); assert (buildNormalizedPath(`c:..\foo\.\..\bar`) == `c:..\bar`); // The ultimate path assert (buildNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`) == `c:\...\baz`); static assert (buildNormalizedPath(`c:\foo\..\bar\\.\..\...\\\baz\\`) == `c:\...\baz`); } else static assert (false); } unittest { // Test for issue 7397 string[] ary = ["a", "b"]; version (Posix) { assert (buildNormalizedPath(ary) == "a/b"); } else version (Windows) { assert (buildNormalizedPath(ary) == `a\b`); } } /** Returns a bidirectional range that iterates over the elements of a path. Examples: --- assert (equal(pathSplitter("/"), ["/"])); assert (equal(pathSplitter("/foo/bar"), ["/", "foo", "bar"])); assert (equal(pathSplitter("//foo/bar"), ["//foo", "bar"])); 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(C)(const(C)[] path) @safe pure nothrow if (isSomeChar!C) { static struct PathSplitter { @safe pure nothrow @nogc: @property bool empty() const { return _empty; } @property const(C)[] front() const { assert (!empty, "PathSplitter: called front() on empty range"); return _front; } void popFront() { assert (!empty, "PathSplitter: called popFront() on empty range"); if (_path.empty) { if (_front is _back) { _empty = true; _front = null; _back = null; } else { _front = _back; } } else { ptrdiff_t i = 0; while (i < _path.length && !isDirSeparator(_path[i])) ++i; _front = _path[0 .. i]; _path = ltrimDirSeparators(_path[i .. $]); } } @property const(C)[] back() const { assert (!empty, "PathSplitter: called back() on empty range"); return _back; } void popBack() { assert (!empty, "PathSplitter: called popBack() on empty range"); if (_path.empty) { if (_front is _back) { _empty = true; _front = null; _back = null; } else { _back = _front; } } else { auto i = (cast(ptrdiff_t) _path.length) - 1; while (i >= 0 && !isDirSeparator(_path[i])) --i; _back = _path[i + 1 .. $]; _path = rtrimDirSeparators(_path[0 .. i+1]); } } @property auto save() { return this; } private: typeof(path) _path, _front, _back; bool _empty; this(typeof(path) p) { if (p.empty) { _empty = true; return; } _path = p; // If path is rooted, first element is special version (Windows) { if (isUNC(_path)) { auto i = uncRootLength(_path); _front = _path[0 .. i]; _path = ltrimDirSeparators(_path[i .. $]); } else if (isDriveRoot(_path)) { _front = _path[0 .. 3]; _path = ltrimDirSeparators(_path[3 .. $]); } else if (_path.length >= 1 && isDirSeparator(_path[0])) { _front = _path[0 .. 1]; _path = ltrimDirSeparators(_path[1 .. $]); } else { assert (!isRooted(_path)); popFront(); } } else version (Posix) { if (_path.length >= 1 && isDirSeparator(_path[0])) { _front = _path[0 .. 1]; _path = ltrimDirSeparators(_path[1 .. $]); } else { popFront(); } } else static assert (0); if (_path.empty) _back = _front; else { _path = rtrimDirSeparators(_path); popBack(); } } } return PathSplitter(path); } unittest { // equal2 verifies that the range is the same both ways, i.e. // through front/popFront and back/popBack. import std.range; import std.algorithm; 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"])); }); // Bugzilla 11691 // front should return a mutable array of const elements static assert(is(typeof(pathSplitter!char(null).front) == const(char)[])); } /** Determines whether a path starts at a root directory. On POSIX, this function returns true if and only if the path starts with a slash (/). --- version (Posix) { assert (isRooted("/")); assert (isRooted("/foo")); assert (!isRooted("foo")); assert (!isRooted("../foo")); } --- On Windows, this function returns true if the path starts at the root directory of the current drive, of some other drive, or of a network drive. --- version (Windows) { assert (isRooted(`\`)); assert (isRooted(`\foo`)); assert (isRooted(`d:\foo`)); assert (isRooted(`\\foo\bar`)); assert (!isRooted("foo")); assert (!isRooted("d:foo")); } --- */ bool isRooted(R)(inout 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); } 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. Examples: On POSIX, an absolute path starts at the root directory. (In fact, $(D _isAbsolute) is just an alias for $(LREF isRooted).) --- version (Posix) { assert (isAbsolute("/")); assert (isAbsolute("/foo")); assert (!isAbsolute("foo")); assert (!isAbsolute("../foo")); } --- On Windows, an absolute path starts at the root directory of a specific drive. Hence, it must start with $(D `d:\`) or $(D `d:/`), where $(D d) is the drive letter. Alternatively, it may be a network path, i.e. a path starting with a double (back)slash. --- version (Windows) { assert (isAbsolute(`d:\`)); assert (isAbsolute(`d:\foo`)); assert (isAbsolute(`\\foo\bar`)); assert (!isAbsolute(`\`)); assert (!isAbsolute(`\foo`)); assert (!isAbsolute("d:foo")); } --- */ version (StdDdoc) { bool isAbsolute(R)(const R path) @safe pure nothrow @nogc if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || is(StringTypeOf!R)); } else version (Windows) { bool isAbsolute(R)(const R path) @safe pure nothrow @nogc if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || is(StringTypeOf!R)) { return isDriveRoot!(BaseOf!R)(path) || isUNC!(BaseOf!R)(path); } } else version (Posix) { alias isAbsolute = isRooted; } 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"))); } /** Translates $(D path) into an absolute _path. The following algorithm is used: $(OL $(LI If $(D path) is empty, return $(D null).) $(LI If $(D path) is already absolute, return it.) $(LI Otherwise, append $(D path) to $(D base) and return the result. If $(D base) is not specified, the current working directory is used.) ) The function allocates memory if and only if it gets to the third stage of this algorithm. Examples: --- 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(`\file`, `c:\foo\bar`) == `c:\file`); } --- Throws: $(D Exception) if the specified _base directory is not absolute. */ string absolutePath(string path, lazy string base = getcwd()) @safe pure { if (path.empty) return null; if (isAbsolute(path)) return path; immutable baseVar = base; if (!isAbsolute(baseVar)) throw new Exception("Base directory must be absolute"); return buildPath(baseVar, path); } 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"); static assert (absolutePath("some/file", "/foo/bar") == "/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`); static assert (absolutePath(`some\file`, `c:\foo\bar`) == `c:\foo\bar\some\file`); } import std.exception; assertThrown(absolutePath("bar", "foo")); } /** Translates $(D path) into a relative _path. The returned _path is relative to $(D base), which is by default taken to be the current working directory. If specified, $(D base) must be an absolute _path, and it is always assumed to refer to a directory. If $(D path) and $(D base) refer to the same directory, the function returns $(D `.`). The following algorithm is used: $(OL $(LI If $(D path) is a relative directory, return it unaltered.) $(LI Find a common root between $(D path) and $(D base). If there is no common root, return $(D path) unaltered.) $(LI Prepare a string with as many $(D `../`) or $(D `..\`) as necessary to reach the common root from base path.) $(LI Append the remaining segments of $(D path) to the string and return.) ) In the second step, path components are compared using $(D filenameCmp!cs), where $(D cs) is an optional template parameter determining whether the comparison is case sensitive or not. See the $(LREF filenameCmp) documentation for details. The function allocates memory if and only if it reaches the third stage of the above algorithm. Examples: --- 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`); } --- Throws: $(D Exception) if the specified _base directory is not absolute. */ string relativePath(CaseSensitive cs = CaseSensitive.osDefault) (string path, lazy string base = getcwd()) //TODO: @safe (object.reserve(T[]) should be @trusted) { if (!isAbsolute(path)) return path; immutable baseVar = base; if (!isAbsolute(baseVar)) throw new Exception("Base directory must be absolute"); // Find common root with current working directory string result; if (!__ctfe) result.reserve(baseVar.length + path.length); auto basePS = pathSplitter(baseVar); auto pathPS = pathSplitter(path); if (filenameCmp!cs(basePS.front, pathPS.front) != 0) return path; basePS.popFront(); pathPS.popFront(); while (!basePS.empty && !pathPS.empty && filenameCmp!cs(basePS.front, pathPS.front) == 0) { basePS.popFront(); pathPS.popFront(); } // Append as many "../" as necessary to reach common base from path while (!basePS.empty) { result ~= ".."; result ~= dirSeparator; basePS.popFront(); } // Append the remainder of path while (!pathPS.empty) { result ~= pathPS.front; result ~= dirSeparator; pathPS.popFront(); } // base == path if (result.empty) return "."; // Strip off last path separator return result[0 .. $-1]; } unittest { import std.exception; 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"); assertThrown(relativePath("/foo", "bar")); assertCTFEable!( { assert (relativePath("/foo/bar", "/foo/baz") == "../bar"); }); } else 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\woo\wee`) == `..\..\bar\baz`); assert (relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz"); assert (relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`); assert (relativePath(`\\foo\bar`, `c:\foo`) == `\\foo\bar`); assertThrown(relativePath(`c:\foo`, "bar")); assertCTFEable!( { assert (relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`); }); } else static assert (0); } /** Compares filename characters and return $(D < 0) if $(D a < b), $(D 0) if $(D a == b) and $(D > 0) if $(D a > b). This function can perform a case-sensitive or a case-insensitive comparison. This is controlled through the $(D cs) template parameter which, if not specified, is given by $(LREF CaseSensitive)$(D .osDefault). On Windows, the backslash and slash characters ($(D `\`) and $(D `/`)) are considered equal. Examples: --- 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); } --- */ 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; a = toLower(a); b = toLower(b); } return cast(int)(a - b); } unittest { assert (filenameCharCmp!(CaseSensitive.yes)('a', 'a') == 0); assert (filenameCharCmp!(CaseSensitive.yes)('a', 'b') < 0); assert (filenameCharCmp!(CaseSensitive.yes)('b', 'a') > 0); 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 $(D < 0) if $(D filename1 < filename2), $(D 0) if $(D filename1 == filename2) and $(D > 0) if $(D filename1 > filename2). Individual characters are compared using $(D filenameCharCmp!cs), where $(D cs) is an optional template parameter determining whether the comparison is case sensitive or not. See the $(LREF filenameCharCmp) documentation for details. Examples: --- 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, C1, C2) (const(C1)[] filename1, const(C2)[] filename2) @safe pure //TODO: nothrow (because of std.array.front()) if (isSomeChar!C1 && isSomeChar!C2) { for (;;) { if (filename1.empty) return -(cast(int) !filename2.empty); if (filename2.empty) return (cast(int) !filename1.empty); auto c = filenameCharCmp!cs(filename1.front, filename2.front); if (c != 0) return c; filename1.popFront(); filename2.popFront(); } assert (0); } unittest { assert (filenameCmp!(CaseSensitive.yes)("abc", "abc") == 0); assert (filenameCmp!(CaseSensitive.yes)("abc", "abd") < 0); assert (filenameCmp!(CaseSensitive.yes)("abc", "abb") > 0); assert (filenameCmp!(CaseSensitive.yes)("abc", "abcd") < 0); assert (filenameCmp!(CaseSensitive.yes)("abcd", "abc") > 0); assert (filenameCmp!(CaseSensitive.yes)("Abc", "abc") < 0); assert (filenameCmp!(CaseSensitive.yes)("abc", "Abc") > 0); assert (filenameCmp!(CaseSensitive.no)("abc", "abc") == 0); assert (filenameCmp!(CaseSensitive.no)("abc", "abd") < 0); assert (filenameCmp!(CaseSensitive.no)("abc", "abb") > 0); assert (filenameCmp!(CaseSensitive.no)("abc", "abcd") < 0); assert (filenameCmp!(CaseSensitive.no)("abcd", "abc") > 0); assert (filenameCmp!(CaseSensitive.no)("Abc", "abc") == 0); assert (filenameCmp!(CaseSensitive.no)("abc", "Abc") == 0); assert (filenameCmp!(CaseSensitive.no)("Abc", "abD") < 0); assert (filenameCmp!(CaseSensitive.no)("abc", "AbB") > 0); version (Posix) assert (filenameCmp(`abc\def`, `abc/def`) != 0); version (Windows) assert (filenameCmp(`abc\def`, `abc/def`) == 0); } /** Matches a pattern against a path. Some characters of pattern have a special meaning (they are $(I meta-characters)) and can't be escaped. These are: $(BOOKTABLE, $(TR $(TD $(D *)) $(TD Matches 0 or more instances of any character.)) $(TR $(TD $(D ?)) $(TD Matches exactly one instance of any character.)) $(TR $(TD $(D [)$(I chars)$(D ])) $(TD Matches one instance of any character that appears between the brackets.)) $(TR $(TD $(D [!)$(I chars)$(D ])) $(TD Matches one instance of any character that does not appear between the brackets after the exclamation mark.)) $(TR $(TD $(D {)$(I string1)$(D ,)$(I string2)$(D ,)…$(D })) $(TD Matches either of the specified strings.)) ) Individual characters are compared using $(D filenameCharCmp!cs), where $(D cs) is an optional template parameter determining whether the comparison is case sensitive or not. See the $(LREF filenameCharCmp) documentation for details. Note that directory separators and dots don't stop a meta-character from matching further portions of the path. Returns: $(D true) if pattern matches path, $(D false) otherwise. See_also: $(LINK2 http://en.wikipedia.org/wiki/Glob_%28programming%29,Wikipedia: _glob (programming)) Examples: ----- 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) (const(C)[] path, const(C)[] pattern) @safe pure nothrow if (isSomeChar!C) in { // Verify that pattern[] is valid import std.algorithm : balancedParens; assert(balancedParens(pattern, '[', ']', 0)); assert(balancedParens(pattern, '{', '}', 0)); } body { size_t ni; // current character in path foreach (ref pi; 0 .. pattern.length) { C pc = pattern[pi]; switch (pc) { case '*': if (pi + 1 == pattern.length) return true; foreach (j; ni .. path.length) { if (globMatch!(cs, C)(path[j .. path.length], pattern[pi + 1 .. pattern.length])) return true; } return false; case '?': if (ni == path.length) return false; ni++; break; case '[': if (ni == path.length) return false; auto nc = path[ni]; ni++; auto not = false; pi++; if (pattern[pi] == '!') { not = true; pi++; } auto anymatch = false; while (1) { pc = pattern[pi]; if (pc == ']') break; if (!anymatch && (filenameCharCmp!cs(nc, pc) == 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) { auto pi0 = pi; pc = pattern[pi]; // find end of current alternative for (; pi:"/\|?*) $(LI $(D filename) may not end with a space ($(D ' ')) or a period ($(D '.')).) ) On POSIX, $(D filename) may not contain a forward slash ($(D '/')) or the null character ($(D '\0')). Params: filename = string to check Returns: $(D true) if and only if $(D filename) is not empty, not too long, and does not contain invalid characters. */ bool isValidFilename(Range)(Range filename) if (is(StringTypeOf!Range) || isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!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)); } @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; foreach (T; TypeTuple!(char[], const(char)[], string, wchar[], const(wchar)[], wstring, dchar[], const(dchar)[], dstring)) { foreach (fn; valid) assert (isValidFilename(to!T(fn))); foreach (fn; invalid) assert (!isValidFilename(to!T(fn))); } { auto r = MockRange!(immutable(char))(`dir/file.d`); assert(!isValidFilename(r)); } static struct DirEntry { string s; alias s this; } assert(isValidFilename(DirEntry("file.ext"))); version (Windows) { immutable string cases = "<>:\"/\\|?*"; foreach (i; 0 .. 31 + cases.length) { char[3] buf; buf[0] = 'a'; buf[1] = i <= 31 ? cast(char)i : cases[i - 32]; buf[2] = 'b'; assert(!isValidFilename(buf[])); } } } /** Checks whether $(D path) is a valid _path. Generally, this function checks that $(D path) is not empty, and that each component of the path either satisfies $(LREF isValidFilename) or is equal to $(D ".") or $(D ".."). It does $(I not) check whether the _path points to an existing file or directory; use $(XREF file,exists) for this purpose. On Windows, some special rules apply: $(UL $(LI If the second character of $(D path) is a colon ($(D ':')), the first character is interpreted as a drive letter, and must be in the range A-Z (case insensitive).) $(LI If $(D path) is on the form $(D `\\$(I server)\$(I share)\...`) (UNC path), $(LREF isValidFilename) is applied to $(I server) and $(I share) as well.) $(LI If $(D path) starts with $(D `\\?\`) (long UNC path), the only requirement for the rest of the string is that it does not contain the null character.) $(LI If $(D path) starts with $(D `\\.\`) (Win32 device namespace) this function returns $(D false); such paths are beyond the scope of this module.) ) Params: path = string or Range of characters to check Returns: true if $(D path) is a valid _path. */ bool isValidPath(Range)(Range path) if (is(StringTypeOf!Range) || isRandomAccessRange!Range && hasLength!Range && hasSlicing!Range && isSomeChar!(ElementEncodingType!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; 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)); } /** Performs tilde expansion in paths on POSIX systems. On Windows, this function does nothing. There are two ways of using tilde expansion in a path. One involves using the tilde alone or followed by a path separator. In this case, the tilde will be expanded with the value of the environment variable $(D HOME). The second way is putting a username after the tilde (i.e. $(D ~john/Mail)). Here, the username will be searched for in the user database (i.e. $(D /etc/passwd) on Unix systems) and will expand to whatever path is stored there. The username is considered the string after the tilde ending at the first instance of a path separator. Note that using the $(D ~user) syntax may give different values from just $(D ~) if the environment variable doesn't match the value stored in the user database. When the environment variable version is used, the path won't be modified if the environment variable doesn't exist or it is empty. When the database version is used, the path won't be modified if the user doesn't exist in the database or there is not enough memory to perform the query. This function performs several memory allocations. Returns: $(D inputPath) with the tilde expanded, or just $(D inputPath) if it could not be expanded. For Windows, $(D expandTilde) merely returns its argument $(D inputPath). Examples: ----- void processFile(string path) { // Allow calling this function with paths such as ~/foo auto fullPath = expandTilde(path); ... } ----- */ string expandTilde(string inputPath) { version(Posix) { import core.stdc.string : strlen; import core.stdc.stdlib : getenv, malloc, free; import core.exception : onOutOfMemoryError; import core.sys.posix.pwd : passwd, getpwnam_r; import core.stdc.errno : errno, ERANGE; /* 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) { assert(c_path != null); assert(path.length > 0); assert(char_pos >= 0); // Search end of C string size_t end = core.stdc.string.strlen(c_path); // Remove trailing path separator, if any if (end && isDirSeparator(c_path[end - 1])) end--; // Create our own copy, as lifetime of c_path is undocumented string cp = c_path[0 .. end].idup; // Do we append something from path? if (char_pos < path.length) cp ~= path[char_pos .. $]; return cp; } // Replaces the tilde from path with the environment variable HOME. static string expandFromEnvironment(string path) { assert(path.length >= 1); assert(path[0] == '~'); // Get HOME and use that to replace the tilde. auto home = core.stdc.stdlib.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) { // Android doesn't really support this, as getpwnam_r // isn't provided and getpwnam is basically just a stub version(Android) { return path; } else { import std.string : indexOf; assert(path.length > 2 || (path.length == 2 && !isDirSeparator(path[1]))); assert(path[0] == '~'); // Extract username, searching for path separator. string username; auto last_char = indexOf(path, dirSeparator[0]); if (last_char == -1) { username = path[1 .. $] ~ '\0'; last_char = username.length + 1; } else { username = path[1 .. last_char] ~ '\0'; } assert(last_char > 1); // Reserve C memory for the getpwnam_r() function. passwd result; int extra_memory_size = 5 * 1024; void* extra_memory; while (1) { extra_memory = core.stdc.stdlib.malloc(extra_memory_size); if (extra_memory == null) goto Lerror; // Obtain info from database. passwd *verify; errno = 0; if (getpwnam_r(cast(char*) username.ptr, &result, cast(char*) extra_memory, extra_memory_size, &verify) == 0) { // Failure if verify doesn't point at result. if (verify != &result) // username is not found, so return path[] goto Lnotfound; break; } if (errno != ERANGE) goto Lerror; // extra_memory isn't large enough core.stdc.stdlib.free(extra_memory); extra_memory_size *= 2; } path = combineCPathWithDPath(result.pw_dir, path, last_char); Lnotfound: core.stdc.stdlib.free(extra_memory); return path; Lerror: // Errors are going to be caused by running out of memory if (extra_memory) core.stdc.stdlib.free(extra_memory); onOutOfMemoryError(); return null; } } // Return early if there is no tilde in path. if (inputPath.length < 1 || inputPath[0] != '~') return inputPath; if (inputPath.length == 1 || isDirSeparator(inputPath[1])) return expandFromEnvironment(inputPath); else return expandFromDatabase(inputPath); } else version(Windows) { // Put here real windows implementation. return inputPath; } else { static assert(0); // Guard. Implement on other platforms. } } version(unittest) import std.process: environment; unittest { version (Posix) { // Retrieve the current home variable. auto oldHome = environment.get("HOME"); // Testing when there is no environment variable. environment.remove("HOME"); assert(expandTilde("~/") == "~/"); assert(expandTilde("~") == "~"); // Testing when an environment variable is set. environment["HOME"] = "dmd/test"; assert(expandTilde("~/") == "dmd/test/"); assert(expandTilde("~") == "dmd/test"); // The same, but with a variable ending in a slash. environment["HOME"] = "dmd/test/"; assert(expandTilde("~/") == "dmd/test/"); assert(expandTilde("~") == "dmd/test"); // Recover original HOME variable before continuing. if (oldHome !is null) environment["HOME"] = oldHome; else environment.remove("HOME"); // Test user expansion for root, no /root on Android version (OSX) { assert(expandTilde("~root") == "/var/root", expandTilde("~root")); assert(expandTilde("~root/") == "/var/root/", expandTilde("~root/")); } else version (Android) { } else { assert(expandTilde("~root") == "/root", expandTilde("~root")); assert(expandTilde("~root/") == "/root/", expandTilde("~root/")); } assert(expandTilde("~Idontexist/hey") == "~Idontexist/hey"); } } version (unittest) { /* Define a mock RandomAccessRange to use for unittesting. */ struct MockRange(C) { this(C[] array) { this.array = array; } const { @property size_t length() { return array.length; } @property bool empty() { return array.length == 0; } @property C front() { return array[0]; } @property C back() { return array[$ - 1]; } @property size_t opDollar() { return length; } C opIndex(size_t i) { return array[i]; } } void popFront() { array = array[1 .. $]; } void popBack() { array = array[0 .. $-1]; } MockRange!C opSlice( size_t lwr, size_t upr) const { return MockRange!C(array[lwr .. upr]); } @property MockRange save() { return this; } private: C[] array; } static assert( isRandomAccessRange!(MockRange!(const(char))) ); } private template BaseOf(R) { static if (isRandomAccessRange!R && isSomeChar!(ElementType!R)) alias BaseOf = R; else alias BaseOf = StringTypeOf!R; }