1 /** 2 * ae.utils.path 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.path; 15 16 import std.algorithm.searching; 17 import std.path; 18 19 /// Modify a path under oldBase to a new path with the same subpath under newBase. 20 /// E.g.: `/foo/bar`.rebasePath(`/foo`, `/quux`) == `/quux/bar` 21 string rebasePath(string path, string oldBase, string newBase) 22 { 23 return buildPath(newBase, path.relPath(oldBase)); 24 } 25 26 /// Variant of std.path.relativePath with the following differences: 27 /// - Works with relative paths. 28 /// If either path is relative, it is first resolved to an absolute path. 29 /// - If `path` starts with `base`, avoids allocating. 30 string relPath(string path, string base) 31 { 32 if (base.length && path.length > base.length && 33 path[0..base.length] == base) 34 { 35 if (base[$-1].isDirSeparator) 36 return path[base.length..$]; 37 if (path[base.length].isDirSeparator) 38 return path[base.length+1..$]; 39 } 40 return relativePath(path.absolutePath, base.absolutePath); 41 } 42 43 deprecated alias fastRelativePath = relPath; 44 45 unittest 46 { 47 version(Windows) 48 { 49 assert(relPath(`C:\a\b\c`, `C:\a`) == `b\c`); 50 assert(relPath(`C:\a\b\c`, `C:\a\`) == `b\c`); 51 assert(relPath(`C:\a\b\c`, `C:\a/`) == `b\c`); 52 assert(relPath(`C:\a\b\c`, `C:\a\d`) == `..\b\c`); 53 } 54 else 55 { 56 assert(relPath("/a/b/c", "/a") == "b/c"); 57 assert(relPath("/a/b/c", "/a/") == "b/c"); 58 assert(relPath("/a/b/c", "/a/d") == "../b/c"); 59 } 60 } 61 62 /// Like Pascal's IncludeTrailingPathDelimiter 63 string includeTrailingPathSeparator(string path) 64 { 65 if (path.length && !path[$-1].isDirSeparator()) 66 path ~= dirSeparator; 67 return path; 68 } 69 70 /// Like Pascal's ExcludeTrailingPathDelimiter 71 string excludeTrailingPathSeparator(string path) 72 { 73 if (path.length && path[$-1].isDirSeparator()) 74 path = path[0..$-1]; 75 return path; 76 } 77 78 /// Like startsWith, but pathStartsWith("/foo/barbara", "/foo/bar") is false. 79 bool pathStartsWith(in char[] path, in char[] prefix) 80 { 81 // Special cases to accommodate relativePath(path, path) results 82 if (prefix == "" || prefix == ".") 83 return true; 84 85 return path.startsWith(prefix) && 86 (path.length == prefix.length || isDirSeparator(path[prefix.length])); 87 } 88 89 unittest 90 { 91 assert( "/foo/bar" .pathStartsWith("/foo/bar")); 92 assert( "/foo/bar/baz".pathStartsWith("/foo/bar")); 93 assert(!"/foo/barbara".pathStartsWith("/foo/bar")); 94 assert( "/foo/bar" .pathStartsWith("")); 95 assert( "/foo/bar" .pathStartsWith(".")); 96 } 97 98 // ************************************************************************ 99 100 import std.process : environment; 101 import std.string : split; 102 103 @property string[] pathDirs() 104 { 105 return environment["PATH"].split(pathSeparator); 106 } 107 108 bool haveExecutable(string name) 109 { 110 return findExecutable(name, pathDirs) !is null; 111 } 112 113 /// Find an executable with the given name 114 /// (no extension) in the given directories. 115 /// Returns null if not found. 116 string findExecutable(string name, string[] dirs) 117 { 118 import std.file : exists; 119 120 version (Windows) 121 enum executableSuffixes = [".exe", ".bat", ".cmd"]; 122 else 123 enum executableSuffixes = [""]; 124 125 foreach (dir; dirs) 126 foreach (suffix; executableSuffixes) 127 { 128 version (Posix) 129 if (dir == "") 130 dir = "."; 131 auto fn = buildPath(dir, name) ~ suffix; 132 if (fn.exists) 133 return fn; 134 } 135 136 return null; 137 } 138 139 // ************************************************************************ 140 141 /** 142 Find a program's "home" directory, based on the presence of a file. 143 144 This can be a directory containing files that are included with or 145 created by the program, and should be accessible no matter how the 146 program is built/invoked of which current directory it is invoked 147 from. 148 149 Use a set of individually-unreliable methods to find the path. This 150 is necessary, because: 151 152 - __FILE__ by itself is insufficient, because it may not be an 153 absolute path, and the compiled binary may have been moved after 154 being built; 155 156 - The executable's directory by itself is insufficient, because 157 build tools such as rdmd can place it in a temporary directory; 158 159 - The current directory by itself is insufficient, because the 160 program can be launched in more than one way, e.g.: 161 162 - Running the program from the same directory as the source file 163 containing main() (e.g. rdmd program.d) 164 165 - Running the program from the upper directory containing all 166 packages and dependencies, so that there is no need to specify 167 include dirs (e.g. rdmd ae/demo/http/httpserve.d) 168 169 - Running the program from a cronjob or another location, in 170 which the current directory can be unrelated altogether. 171 172 Params: 173 testFile = Relative path to a file or directory, the presence of 174 which indicates that the "current" directory (base of 175 the relative path) is the sought-after program root 176 directory. 177 sourceFile = Path to a source file part of the program's code 178 base. Defaults to the __FILE__ of the caller. 179 180 Returns: 181 Path to the sought root directory, or `null` if one was not found. 182 */ 183 string findProgramDirectory(string testFile, string sourceFile = __FILE__) 184 { 185 import std.file : thisExePath, getcwd, exists; 186 import core.runtime : Runtime; 187 import std.range : only; 188 189 foreach (path; only(Runtime.args[0].absolutePath().dirName(), thisExePath.dirName, sourceFile.dirName, null)) 190 { 191 path = path.absolutePath().buildNormalizedPath(); 192 while (true) 193 { 194 auto indicator = path.buildPath(testFile); 195 if (indicator.exists) 196 return path; 197 auto parent = dirName(path); 198 if (parent == path) 199 break; 200 path = parent; 201 } 202 } 203 return null; 204 } 205 206 // ************************************************************************ 207 208 /// The file name for the null device 209 /// (which discards all writes). 210 version (Windows) 211 enum nullFileName = "nul"; 212 else 213 enum nullFileName = "/dev/null";