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