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