|
1 #!/usr/bin/env python |
|
2 # Copyright (c) 2002-2005 ActiveState Corp. |
|
3 # See LICENSE.txt for license details. |
|
4 # Author: |
|
5 # Trent Mick (TrentM@ActiveState.com) |
|
6 # Home: |
|
7 # http://trentm.com/projects/which/ |
|
8 |
|
9 r"""Find the full path to commands. |
|
10 |
|
11 which(command, path=None, verbose=0, exts=None) |
|
12 Return the full path to the first match of the given command on the |
|
13 path. |
|
14 |
|
15 whichall(command, path=None, verbose=0, exts=None) |
|
16 Return a list of full paths to all matches of the given command on |
|
17 the path. |
|
18 |
|
19 whichgen(command, path=None, verbose=0, exts=None) |
|
20 Return a generator which will yield full paths to all matches of the |
|
21 given command on the path. |
|
22 |
|
23 By default the PATH environment variable is searched (as well as, on |
|
24 Windows, the AppPaths key in the registry), but a specific 'path' list |
|
25 to search may be specified as well. On Windows, the PATHEXT environment |
|
26 variable is applied as appropriate. |
|
27 |
|
28 If "verbose" is true then a tuple of the form |
|
29 (<fullpath>, <matched-where-description>) |
|
30 is returned for each match. The latter element is a textual description |
|
31 of where the match was found. For example: |
|
32 from PATH element 0 |
|
33 from HKLM\SOFTWARE\...\perl.exe |
|
34 """ |
|
35 |
|
36 _cmdlnUsage = """ |
|
37 Show the full path of commands. |
|
38 |
|
39 Usage: |
|
40 which [<options>...] [<command-name>...] |
|
41 |
|
42 Options: |
|
43 -h, --help Print this help and exit. |
|
44 -V, --version Print the version info and exit. |
|
45 |
|
46 -a, --all Print *all* matching paths. |
|
47 -v, --verbose Print out how matches were located and |
|
48 show near misses on stderr. |
|
49 -q, --quiet Just print out matches. I.e., do not print out |
|
50 near misses. |
|
51 |
|
52 -p <altpath>, --path=<altpath> |
|
53 An alternative path (list of directories) may |
|
54 be specified for searching. |
|
55 -e <exts>, --exts=<exts> |
|
56 Specify a list of extensions to consider instead |
|
57 of the usual list (';'-separate list, Windows |
|
58 only). |
|
59 |
|
60 Show the full path to the program that would be run for each given |
|
61 command name, if any. Which, like GNU's which, returns the number of |
|
62 failed arguments, or -1 when no <command-name> was given. |
|
63 |
|
64 Near misses include duplicates, non-regular files and (on Un*x) |
|
65 files without executable access. |
|
66 """ |
|
67 |
|
68 __revision__ = "$Id: which.py 430 2005-08-20 03:11:58Z trentm $" |
|
69 __version_info__ = (1, 1, 0) |
|
70 __version__ = '.'.join(map(str, __version_info__)) |
|
71 |
|
72 import os |
|
73 import sys |
|
74 import getopt |
|
75 import stat |
|
76 |
|
77 |
|
78 #---- exceptions |
|
79 |
|
80 class WhichError(Exception): |
|
81 pass |
|
82 |
|
83 |
|
84 |
|
85 #---- internal support stuff |
|
86 |
|
87 def _getRegisteredExecutable(exeName): |
|
88 """Windows allow application paths to be registered in the registry.""" |
|
89 registered = None |
|
90 if sys.platform.startswith('win'): |
|
91 if os.path.splitext(exeName)[1].lower() != '.exe': |
|
92 exeName += '.exe' |
|
93 import _winreg |
|
94 try: |
|
95 key = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" +\ |
|
96 exeName |
|
97 value = _winreg.QueryValue(_winreg.HKEY_LOCAL_MACHINE, key) |
|
98 registered = (value, "from HKLM\\"+key) |
|
99 except _winreg.error: |
|
100 pass |
|
101 if registered and not os.path.exists(registered[0]): |
|
102 registered = None |
|
103 return registered |
|
104 |
|
105 def _samefile(fname1, fname2): |
|
106 if sys.platform.startswith('win'): |
|
107 return ( os.path.normpath(os.path.normcase(fname1)) ==\ |
|
108 os.path.normpath(os.path.normcase(fname2)) ) |
|
109 else: |
|
110 return os.path.samefile(fname1, fname2) |
|
111 |
|
112 def _cull(potential, matches, verbose=0): |
|
113 """Cull inappropriate matches. Possible reasons: |
|
114 - a duplicate of a previous match |
|
115 - not a disk file |
|
116 - not executable (non-Windows) |
|
117 If 'potential' is approved it is returned and added to 'matches'. |
|
118 Otherwise, None is returned. |
|
119 """ |
|
120 for match in matches: # don't yield duplicates |
|
121 if _samefile(potential[0], match[0]): |
|
122 if verbose: |
|
123 sys.stderr.write("duplicate: %s (%s)\n" % potential) |
|
124 return None |
|
125 else: |
|
126 if not stat.S_ISREG(os.stat(potential[0]).st_mode): |
|
127 if verbose: |
|
128 sys.stderr.write("not a regular file: %s (%s)\n" % potential) |
|
129 elif not os.access(potential[0], os.X_OK): |
|
130 if verbose: |
|
131 sys.stderr.write("no executable access: %s (%s)\n"\ |
|
132 % potential) |
|
133 else: |
|
134 matches.append(potential) |
|
135 return potential |
|
136 |
|
137 |
|
138 #---- module API |
|
139 |
|
140 def whichgen(command, path=None, verbose=0, exts=None): |
|
141 """Return a generator of full paths to the given command. |
|
142 |
|
143 "command" is a the name of the executable to search for. |
|
144 "path" is an optional alternate path list to search. The default it |
|
145 to use the PATH environment variable. |
|
146 "verbose", if true, will cause a 2-tuple to be returned for each |
|
147 match. The second element is a textual description of where the |
|
148 match was found. |
|
149 "exts" optionally allows one to specify a list of extensions to use |
|
150 instead of the standard list for this system. This can |
|
151 effectively be used as an optimization to, for example, avoid |
|
152 stat's of "foo.vbs" when searching for "foo" and you know it is |
|
153 not a VisualBasic script but ".vbs" is on PATHEXT. This option |
|
154 is only supported on Windows. |
|
155 |
|
156 This method returns a generator which yields either full paths to |
|
157 the given command or, if verbose, tuples of the form (<path to |
|
158 command>, <where path found>). |
|
159 """ |
|
160 matches = [] |
|
161 if path is None: |
|
162 usingGivenPath = 0 |
|
163 path = os.environ.get("PATH", "").split(os.pathsep) |
|
164 if sys.platform.startswith("win"): |
|
165 path.insert(0, os.curdir) # implied by Windows shell |
|
166 else: |
|
167 usingGivenPath = 1 |
|
168 |
|
169 # Windows has the concept of a list of extensions (PATHEXT env var). |
|
170 if sys.platform.startswith("win"): |
|
171 if exts is None: |
|
172 exts = os.environ.get("PATHEXT", "").split(os.pathsep) |
|
173 # If '.exe' is not in exts then obviously this is Win9x and |
|
174 # or a bogus PATHEXT, then use a reasonable default. |
|
175 for ext in exts: |
|
176 if ext.lower() == ".exe": |
|
177 break |
|
178 else: |
|
179 exts = ['.COM', '.EXE', '.BAT'] |
|
180 elif not isinstance(exts, list): |
|
181 raise TypeError("'exts' argument must be a list or None") |
|
182 else: |
|
183 if exts is not None: |
|
184 raise WhichError("'exts' argument is not supported on "\ |
|
185 "platform '%s'" % sys.platform) |
|
186 exts = [] |
|
187 |
|
188 # File name cannot have path separators because PATH lookup does not |
|
189 # work that way. |
|
190 if os.sep in command or os.altsep and os.altsep in command: |
|
191 pass |
|
192 else: |
|
193 for i in range(len(path)): |
|
194 dirName = path[i] |
|
195 # On windows the dirName *could* be quoted, drop the quotes |
|
196 if sys.platform.startswith("win") and len(dirName) >= 2\ |
|
197 and dirName[0] == '"' and dirName[-1] == '"': |
|
198 dirName = dirName[1:-1] |
|
199 for ext in ['']+exts: |
|
200 absName = os.path.abspath( |
|
201 os.path.normpath(os.path.join(dirName, command+ext))) |
|
202 if os.path.isfile(absName): |
|
203 if usingGivenPath: |
|
204 fromWhere = "from given path element %d" % i |
|
205 elif not sys.platform.startswith("win"): |
|
206 fromWhere = "from PATH element %d" % i |
|
207 elif i == 0: |
|
208 fromWhere = "from current directory" |
|
209 else: |
|
210 fromWhere = "from PATH element %d" % (i-1) |
|
211 match = _cull((absName, fromWhere), matches, verbose) |
|
212 if match: |
|
213 if verbose: |
|
214 yield match |
|
215 else: |
|
216 yield match[0] |
|
217 match = _getRegisteredExecutable(command) |
|
218 if match is not None: |
|
219 match = _cull(match, matches, verbose) |
|
220 if match: |
|
221 if verbose: |
|
222 yield match |
|
223 else: |
|
224 yield match[0] |
|
225 |
|
226 |
|
227 def which(command, path=None, verbose=0, exts=None): |
|
228 """Return the full path to the first match of the given command on |
|
229 the path. |
|
230 |
|
231 "command" is a the name of the executable to search for. |
|
232 "path" is an optional alternate path list to search. The default it |
|
233 to use the PATH environment variable. |
|
234 "verbose", if true, will cause a 2-tuple to be returned. The second |
|
235 element is a textual description of where the match was found. |
|
236 "exts" optionally allows one to specify a list of extensions to use |
|
237 instead of the standard list for this system. This can |
|
238 effectively be used as an optimization to, for example, avoid |
|
239 stat's of "foo.vbs" when searching for "foo" and you know it is |
|
240 not a VisualBasic script but ".vbs" is on PATHEXT. This option |
|
241 is only supported on Windows. |
|
242 |
|
243 If no match is found for the command, a WhichError is raised. |
|
244 """ |
|
245 try: |
|
246 match = whichgen(command, path, verbose, exts).next() |
|
247 except StopIteration: |
|
248 raise WhichError("Could not find '%s' on the path." % command) |
|
249 return match |
|
250 |
|
251 |
|
252 def whichall(command, path=None, verbose=0, exts=None): |
|
253 """Return a list of full paths to all matches of the given command |
|
254 on the path. |
|
255 |
|
256 "command" is a the name of the executable to search for. |
|
257 "path" is an optional alternate path list to search. The default it |
|
258 to use the PATH environment variable. |
|
259 "verbose", if true, will cause a 2-tuple to be returned for each |
|
260 match. The second element is a textual description of where the |
|
261 match was found. |
|
262 "exts" optionally allows one to specify a list of extensions to use |
|
263 instead of the standard list for this system. This can |
|
264 effectively be used as an optimization to, for example, avoid |
|
265 stat's of "foo.vbs" when searching for "foo" and you know it is |
|
266 not a VisualBasic script but ".vbs" is on PATHEXT. This option |
|
267 is only supported on Windows. |
|
268 """ |
|
269 return list( whichgen(command, path, verbose, exts) ) |
|
270 |
|
271 |
|
272 |
|
273 #---- mainline |
|
274 |
|
275 def main(argv): |
|
276 all = 0 |
|
277 verbose = 0 |
|
278 altpath = None |
|
279 exts = None |
|
280 try: |
|
281 optlist, args = getopt.getopt(argv[1:], 'haVvqp:e:', |
|
282 ['help', 'all', 'version', 'verbose', 'quiet', 'path=', 'exts=']) |
|
283 except getopt.GetoptError, msg: |
|
284 sys.stderr.write("which: error: %s. Your invocation was: %s\n"\ |
|
285 % (msg, argv)) |
|
286 sys.stderr.write("Try 'which --help'.\n") |
|
287 return 1 |
|
288 for opt, optarg in optlist: |
|
289 if opt in ('-h', '--help'): |
|
290 print _cmdlnUsage |
|
291 return 0 |
|
292 elif opt in ('-V', '--version'): |
|
293 print "which %s" % __version__ |
|
294 return 0 |
|
295 elif opt in ('-a', '--all'): |
|
296 all = 1 |
|
297 elif opt in ('-v', '--verbose'): |
|
298 verbose = 1 |
|
299 elif opt in ('-q', '--quiet'): |
|
300 verbose = 0 |
|
301 elif opt in ('-p', '--path'): |
|
302 if optarg: |
|
303 altpath = optarg.split(os.pathsep) |
|
304 else: |
|
305 altpath = [] |
|
306 elif opt in ('-e', '--exts'): |
|
307 if optarg: |
|
308 exts = optarg.split(os.pathsep) |
|
309 else: |
|
310 exts = [] |
|
311 |
|
312 if len(args) == 0: |
|
313 return -1 |
|
314 |
|
315 failures = 0 |
|
316 for arg in args: |
|
317 #print "debug: search for %r" % arg |
|
318 nmatches = 0 |
|
319 for match in whichgen(arg, path=altpath, verbose=verbose, exts=exts): |
|
320 if verbose: |
|
321 print "%s (%s)" % match |
|
322 else: |
|
323 print match |
|
324 nmatches += 1 |
|
325 if not all: |
|
326 break |
|
327 if not nmatches: |
|
328 failures += 1 |
|
329 return failures |
|
330 |
|
331 |
|
332 if __name__ == "__main__": |
|
333 sys.exit( main(sys.argv) ) |
|
334 |
|
335 |