mirror of
https://github.com/dlang/phobos.git
synced 2025-04-27 13:40:20 +03:00

imports of `std.range, std.algorithm, std.array, std.string, std.format, std.uni` are affected.
3068 lines
101 KiB
D
3068 lines
101 KiB
D
// 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.
|
|
|
|
Authors:
|
|
Lars Tandle Kyllingstad,
|
|
$(WEB digitalmars.com, Walter Bright),
|
|
Grzegorz Adam Hankiewicz,
|
|
Thomas Kü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.constraints;
|
|
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)(inout 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)(inout 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)(inout 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.
|
|
On Windows, slashes are replaced with backslashes.
|
|
|
|
Note that this function does not resolve symbolic links.
|
|
|
|
This function always allocates memory to hold the resulting path.
|
|
|
|
Examples:
|
|
---
|
|
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;
|
|
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);
|
|
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");
|
|
|
|
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");
|
|
}
|
|
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`);
|
|
}
|
|
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<pattern.length && pc!='}' && pc!=','; pi++)
|
|
{
|
|
pc = pattern[pi];
|
|
}
|
|
|
|
if (pi0 == pi)
|
|
{
|
|
if (globMatch!(cs, C)(path[ni..$], pattern[piRemain..$]))
|
|
{
|
|
return true;
|
|
}
|
|
pi++;
|
|
}
|
|
else
|
|
{
|
|
if (globMatch!(cs, C)(path[ni..$],
|
|
pattern[pi0..pi-1]
|
|
~ pattern[piRemain..$]))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
if (pc == '}')
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
|
|
default:
|
|
if (ni == path.length)
|
|
return false;
|
|
if (filenameCharCmp!cs(pc, path[ni]) != 0)
|
|
return false;
|
|
ni++;
|
|
break;
|
|
}
|
|
}
|
|
assert(ni <= path.length);
|
|
return ni == path.length;
|
|
}
|
|
|
|
unittest
|
|
{
|
|
assert (globMatch!(CaseSensitive.no)("foo", "Foo"));
|
|
assert (!globMatch!(CaseSensitive.yes)("foo", "Foo"));
|
|
|
|
assert(globMatch("foo", "*"));
|
|
assert(globMatch("foo.bar"w, "*"w));
|
|
assert(globMatch("foo.bar"d, "*.*"d));
|
|
assert(globMatch("foo.bar", "foo*"));
|
|
assert(globMatch("foo.bar"w, "f*bar"w));
|
|
assert(globMatch("foo.bar"d, "f*b*r"d));
|
|
assert(globMatch("foo.bar", "f???bar"));
|
|
assert(globMatch("foo.bar"w, "[fg]???bar"w));
|
|
assert(globMatch("foo.bar"d, "[!gh]*bar"d));
|
|
|
|
assert(!globMatch("foo", "bar"));
|
|
assert(!globMatch("foo"w, "*.*"w));
|
|
assert(!globMatch("foo.bar"d, "f*baz"d));
|
|
assert(!globMatch("foo.bar", "f*b*x"));
|
|
assert(!globMatch("foo.bar", "[gh]???bar"));
|
|
assert(!globMatch("foo.bar"w, "[!fg]*bar"w));
|
|
assert(!globMatch("foo.bar"d, "[fg]???baz"d));
|
|
assert(!globMatch("foo.di", "*.d")); // test issue 6634: triggered bad assertion
|
|
|
|
assert(globMatch("foo.bar", "{foo,bif}.bar"));
|
|
assert(globMatch("bif.bar"w, "{foo,bif}.bar"w));
|
|
|
|
assert(globMatch("bar.foo"d, "bar.{foo,bif}"d));
|
|
assert(globMatch("bar.bif", "bar.{foo,bif}"));
|
|
|
|
assert(globMatch("bar.fooz"w, "bar.{foo,bif}z"w));
|
|
assert(globMatch("bar.bifz"d, "bar.{foo,bif}z"d));
|
|
|
|
assert(globMatch("bar.foo", "bar.{biz,,baz}foo"));
|
|
assert(globMatch("bar.foo"w, "bar.{biz,}foo"w));
|
|
assert(globMatch("bar.foo"d, "bar.{,biz}foo"d));
|
|
assert(globMatch("bar.foo", "bar.{}foo"));
|
|
|
|
assert(globMatch("bar.foo"w, "bar.{ar,,fo}o"w));
|
|
assert(globMatch("bar.foo"d, "bar.{,ar,fo}o"d));
|
|
assert(globMatch("bar.o", "bar.{,ar,fo}o"));
|
|
|
|
static assert(globMatch("foo.bar", "[!gh]*bar"));
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Checks that the given file or directory name is valid.
|
|
|
|
This function returns $(D true) if and only if $(D filename) is not
|
|
empty, not too long, and does not contain invalid characters.
|
|
|
|
The maximum length of $(D filename) is given by the constant
|
|
$(D core.stdc.stdio.FILENAME_MAX). (On Windows, this number is
|
|
defined as the maximum number of UTF-16 code points, and the
|
|
test will therefore only yield strictly correct results when
|
|
$(D filename) is a string of $(D wchar)s.)
|
|
|
|
On Windows, the following criteria must be satisfied
|
|
($(LINK2 http://msdn.microsoft.com/en-us/library/aa365247(v=vs.85).aspx,source)):
|
|
$(UL
|
|
$(LI $(D filename) must not contain any characters whose integer
|
|
representation is in the range 0-31.)
|
|
$(LI $(D filename) must not contain any of the following $(I reserved
|
|
characters): <>:"/\|?*)
|
|
$(LI $(D filename) may not end with a space ($(D ' ')) or a period
|
|
($(D '.')).)
|
|
)
|
|
|
|
On POSIX, $(D filename) may not contain a forward slash ($(D '/')) or
|
|
the null character ($(D '\0')).
|
|
*/
|
|
bool isValidFilename(R)(R filename)
|
|
if (isRandomAccessRange!R && isSomeChar!(ElementType!R) ||
|
|
is(StringTypeOf!R))
|
|
{
|
|
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;
|
|
}
|
|
|
|
|
|
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.typetuple;
|
|
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")));
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 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.)
|
|
)
|
|
*/
|
|
bool isValidPath(C)(in C[] path) @safe pure nothrow if (isSomeChar!C)
|
|
{
|
|
if (path.empty) return false;
|
|
|
|
// Check whether component is "." or "..", or whether it satisfies
|
|
// isValidFilename.
|
|
bool isValidComponent(in C[] component) @safe pure nothrow
|
|
{
|
|
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);
|
|
|
|
const(C)[] 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);
|
|
assert (remainder !is null);
|
|
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;
|
|
}
|
|
|
|
|
|
unittest
|
|
{
|
|
assert (isValidPath("/foo/bar"));
|
|
assert (!isValidPath("/foo\0/bar"));
|
|
|
|
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`));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 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;
|
|
}
|