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