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