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 }