1 /** 2 * Searching for executable files in system paths. 3 * Authors: 4 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) 5 * License: 6 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 7 * Copyright: 8 * Roman Chistokhodov 2016 9 */ 10 11 module findexecutable; 12 13 private { 14 import std.algorithm : canFind, splitter, filter, map; 15 import std.path; 16 import std.process : environment; 17 import std.range; 18 19 version(Windows) { 20 import std.uni : toLower; 21 } 22 version(Posix) { 23 import std.string : toStringz; 24 } 25 } 26 27 version(Windows) { 28 private enum pathVarSeparator = ';'; 29 private enum defaultExts = ".exe;.com;.bat;.cmd"; 30 } else version(Posix) { 31 private enum pathVarSeparator = ':'; 32 } 33 34 version(unittest) { 35 import std.algorithm : equal; 36 37 private struct EnvGuard 38 { 39 this(string env) { 40 envVar = env; 41 envValue = environment.get(env); 42 } 43 44 ~this() { 45 if (envValue is null) { 46 environment.remove(envVar); 47 } else { 48 environment[envVar] = envValue; 49 } 50 } 51 52 string envVar; 53 string envValue; 54 } 55 } 56 57 /** 58 * Default executable extensions for the current system. 59 * 60 * On Windows this functions examines $(B PATHEXT) environment variable to get the list of executables extensions. 61 * Fallbacks to .exe;.com;.bat;.cmd if $(B PATHEXT) does not list .exe extension. 62 * On other systems it always returns empty range. 63 * Note: This function does not cache its result 64 */ 65 @trusted auto executableExtensions() nothrow 66 { 67 version(Windows) { 68 static bool filenamesEqual(string first, string second) nothrow { 69 try { 70 return filenameCmp(first, second) == 0; 71 } catch(Exception e) { 72 return false; 73 } 74 } 75 76 static auto splitValues(string pathExt) { 77 return pathExt.splitter(pathVarSeparator); 78 } 79 80 try { 81 auto pathExts = splitValues(environment.get("PATHEXT").toLower()); 82 if (canFind!(filenamesEqual)(pathExts, ".exe") == false) { 83 return splitValues(defaultExts); 84 } else { 85 return pathExts; 86 } 87 } catch(Exception e) { 88 return splitValues(defaultExts); 89 } 90 91 } else { 92 return (string[]).init; 93 } 94 } 95 96 /// 97 unittest 98 { 99 version(Windows) { 100 auto guard = EnvGuard("PATHEXT"); 101 environment["PATHEXT"] = ".exe;.bat;.cmd"; 102 assert(equal(executableExtensions(), [".exe", ".bat", ".cmd"])); 103 environment["PATHEXT"] = ""; 104 assert(equal(executableExtensions(), defaultExts.splitter(pathVarSeparator))); 105 } else { 106 assert(executableExtensions().empty); 107 } 108 } 109 110 private bool isExecutable(Exts)(string filePath, Exts exts) nothrow { 111 try { 112 version(Posix) { 113 import core.sys.posix.unistd; 114 return access(toStringz(filePath), X_OK) == 0; 115 } else version(Windows) { 116 //Use GetEffectiveRightsFromAclW? 117 118 string extension = filePath.extension; 119 foreach(ext; exts) { 120 if (filenameCmp(extension, ext) == 0) 121 return true; 122 } 123 return false; 124 125 } else { 126 static assert(false, "Unsupported platform"); 127 } 128 } catch(Exception e) { 129 return false; 130 } 131 } 132 133 private string checkExecutable(Exts)(string filePath, Exts exts) nothrow { 134 import std.file : isFile; 135 try { 136 if (filePath.isFile && isExecutable(filePath, exts)) { 137 return buildNormalizedPath(filePath); 138 } else { 139 return null; 140 } 141 } 142 catch(Exception e) { 143 return null; 144 } 145 } 146 147 /** 148 * System paths where executable files can be found. 149 * Returns: Range of non-empty paths as determined by $(B PATH) environment variable. 150 * Note: This function does not cache its result 151 */ 152 @trusted auto binPaths() nothrow 153 { 154 import std.exception : collectException; 155 import std.utf : byCodeUnit; 156 string pathVar; 157 collectException(environment.get("PATH"), pathVar); 158 return splitter(pathVar.byCodeUnit, pathVarSeparator).map!(p => p.source).filter!(p => p.length != 0); 159 } 160 161 /// 162 unittest 163 { 164 auto pathGuard = EnvGuard("PATH"); 165 version(Windows) { 166 environment["PATH"] = ".;C:\\Windows\\system32;C:\\Program Files"; 167 assert(equal(binPaths(), [".", "C:\\Windows\\system32", "C:\\Program Files"])); 168 } else { 169 environment["PATH"] = ".:/usr/apps:/usr/local/apps:"; 170 assert(equal(binPaths(), [".", "/usr/apps", "/usr/local/apps"])); 171 } 172 } 173 174 /** 175 * Find executable by fileName in the paths. 176 * Returns: Absolute path to the existing executable file or an empty string if not found. 177 * Params: 178 * fileName = Name of executable to search. Should be base name or absolute path. Relative paths will not work. 179 * If it's an absolute path, this function does not try to append extensions. 180 * paths = Range of directories where executable should be searched. 181 * extensions = Range of extensions to append during searching if fileName does not have extension. 182 * Note: Currently it does not check if current user really have permission to execute the file on Windows. 183 * See_Also: $(D binPaths), $(D executableExtensions) 184 */ 185 string findExecutable(Paths, Exts)(string fileName, Paths paths, Exts extensions) 186 if (isInputRange!Paths && is(ElementType!Paths : string) && isInputRange!Exts && is(ElementType!Exts : string)) 187 { 188 try { 189 if (fileName.isAbsolute()) { 190 return checkExecutable(fileName, extensions); 191 } else if (fileName == fileName.baseName) { 192 string toReturn; 193 foreach(string path; paths) { 194 if (path.empty) { 195 continue; 196 } 197 198 string candidate = buildPath(absolutePath(path), fileName); 199 200 if (candidate.extension.empty && !extensions.empty) { 201 foreach(exeExtension; extensions) { 202 toReturn = checkExecutable(setExtension(candidate, exeExtension), extensions); 203 if (toReturn.length) { 204 return toReturn; 205 } 206 } 207 } 208 209 toReturn = checkExecutable(candidate, extensions); 210 if (toReturn.length) { 211 return toReturn; 212 } 213 } 214 } 215 } catch (Exception e) { 216 217 } 218 return null; 219 } 220 221 /** 222 * ditto, but on Windows when fileName extension is omitted, executable extensions are appended during search. 223 * See_Also: $(D binPaths), $(D executableExtensions) 224 */ 225 string findExecutable(Paths)(string fileName, Paths paths) 226 if (isInputRange!Paths && is(ElementType!Paths : string)) 227 { 228 return findExecutable(fileName, paths, executableExtensions()); 229 } 230 231 /** 232 * ditto, but searches in system paths, determined by $(B PATH) environment variable. 233 * On Windows when fileName extension is omitted, executable extensions are appended during search. 234 * See_Also: $(D binPaths), $(D executableExtensions) 235 */ 236 @trusted string findExecutable(string fileName) nothrow { 237 try { 238 return findExecutable(fileName, binPaths(), executableExtensions()); 239 } catch(Exception e) { 240 return null; 241 } 242 }