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