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

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