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 auto fn = buildPath(dir, name) ~ suffix; 125 if (fn.exists) 126 return fn; 127 } 128 129 return null; 130 } 131 132 // ************************************************************************ 133 134 /** 135 Find a program's "home" directory, based on the presence of a file. 136 137 This can be a directory containing files that are included with or 138 created by the program, and should be accessible no matter how the 139 program is built/invoked of which current directory it is invoked 140 from. 141 142 Use a set of individually-unreliable methods to find the path. This 143 is necessary, because: 144 145 - __FILE__ by itself is insufficient, because it may not be an 146 absolute path, and the compiled binary may have been moved after 147 being built; 148 149 - The executable's directory by itself is insufficient, because 150 build tools such as rdmd can place it in a temporary directory; 151 152 - The current directory by itself is insufficient, because the 153 program can be launched in more than one way, e.g.: 154 155 - Running the program from the same directory as the source file 156 containing main() (e.g. rdmd program.d) 157 158 - Running the program from the upper directory containing all 159 packages and dependencies, so that there is no need to specify 160 include dirs (e.g. rdmd ae/demo/http/httpserve.d) 161 162 - Running the program from a cronjob or another location, in 163 which the current directory can be unrelated altogether. 164 165 Params: 166 testFile = Relative path to a file or directory, the presence of 167 which indicates that the "current" directory (base of 168 the relative path) is the sought-after program root 169 directory. 170 sourceFile = Path to a source file part of the program's code 171 base. Defaults to the __FILE__ of the caller. 172 173 Returns: 174 Path to the sought root directory, or `null` if one was not found. 175 */ 176 string findProgramDirectory(string testFile, string sourceFile = __FILE__) 177 { 178 import std.file : thisExePath, getcwd, exists; 179 import core.runtime : Runtime; 180 import std.range : only; 181 182 foreach (path; only(Runtime.args[0].absolutePath().dirName(), thisExePath.dirName, sourceFile.dirName, null)) 183 { 184 path = path.absolutePath().buildNormalizedPath(); 185 while (true) 186 { 187 auto indicator = path.buildPath(testFile); 188 if (indicator.exists) 189 return path; 190 auto parent = dirName(path); 191 if (parent == path) 192 break; 193 path = parent; 194 } 195 } 196 return null; 197 } 198 199 // ************************************************************************ 200 201 /// The file name for the null device 202 /// (which discards all writes). 203 version (Windows) 204 enum nullFileName = "nul"; 205 else 206 enum nullFileName = "/dev/null";