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 }