michael@0: #!/usr/bin/env python michael@0: # Copyright (c) 2002-2005 ActiveState Corp. michael@0: # See LICENSE.txt for license details. michael@0: # Author: michael@0: # Trent Mick (TrentM@ActiveState.com) michael@0: # Home: michael@0: # http://trentm.com/projects/which/ michael@0: michael@0: r"""Find the full path to commands. michael@0: michael@0: which(command, path=None, verbose=0, exts=None) michael@0: Return the full path to the first match of the given command on the michael@0: path. michael@0: michael@0: whichall(command, path=None, verbose=0, exts=None) michael@0: Return a list of full paths to all matches of the given command on michael@0: the path. michael@0: michael@0: whichgen(command, path=None, verbose=0, exts=None) michael@0: Return a generator which will yield full paths to all matches of the michael@0: given command on the path. michael@0: michael@0: By default the PATH environment variable is searched (as well as, on michael@0: Windows, the AppPaths key in the registry), but a specific 'path' list michael@0: to search may be specified as well. On Windows, the PATHEXT environment michael@0: variable is applied as appropriate. michael@0: michael@0: If "verbose" is true then a tuple of the form michael@0: (, ) michael@0: is returned for each match. The latter element is a textual description michael@0: of where the match was found. For example: michael@0: from PATH element 0 michael@0: from HKLM\SOFTWARE\...\perl.exe michael@0: """ michael@0: michael@0: _cmdlnUsage = """ michael@0: Show the full path of commands. michael@0: michael@0: Usage: michael@0: which [...] [...] michael@0: michael@0: Options: michael@0: -h, --help Print this help and exit. michael@0: -V, --version Print the version info and exit. michael@0: michael@0: -a, --all Print *all* matching paths. michael@0: -v, --verbose Print out how matches were located and michael@0: show near misses on stderr. michael@0: -q, --quiet Just print out matches. I.e., do not print out michael@0: near misses. michael@0: michael@0: -p , --path= michael@0: An alternative path (list of directories) may michael@0: be specified for searching. michael@0: -e , --exts= michael@0: Specify a list of extensions to consider instead michael@0: of the usual list (';'-separate list, Windows michael@0: only). michael@0: michael@0: Show the full path to the program that would be run for each given michael@0: command name, if any. Which, like GNU's which, returns the number of michael@0: failed arguments, or -1 when no was given. michael@0: michael@0: Near misses include duplicates, non-regular files and (on Un*x) michael@0: files without executable access. michael@0: """ michael@0: michael@0: __revision__ = "$Id: which.py 430 2005-08-20 03:11:58Z trentm $" michael@0: __version_info__ = (1, 1, 0) michael@0: __version__ = '.'.join(map(str, __version_info__)) michael@0: michael@0: import os michael@0: import sys michael@0: import getopt michael@0: import stat michael@0: michael@0: michael@0: #---- exceptions michael@0: michael@0: class WhichError(Exception): michael@0: pass michael@0: michael@0: michael@0: michael@0: #---- internal support stuff michael@0: michael@0: def _getRegisteredExecutable(exeName): michael@0: """Windows allow application paths to be registered in the registry.""" michael@0: registered = None michael@0: if sys.platform.startswith('win'): michael@0: if os.path.splitext(exeName)[1].lower() != '.exe': michael@0: exeName += '.exe' michael@0: import _winreg michael@0: try: michael@0: key = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" +\ michael@0: exeName michael@0: value = _winreg.QueryValue(_winreg.HKEY_LOCAL_MACHINE, key) michael@0: registered = (value, "from HKLM\\"+key) michael@0: except _winreg.error: michael@0: pass michael@0: if registered and not os.path.exists(registered[0]): michael@0: registered = None michael@0: return registered michael@0: michael@0: def _samefile(fname1, fname2): michael@0: if sys.platform.startswith('win'): michael@0: return ( os.path.normpath(os.path.normcase(fname1)) ==\ michael@0: os.path.normpath(os.path.normcase(fname2)) ) michael@0: else: michael@0: return os.path.samefile(fname1, fname2) michael@0: michael@0: def _cull(potential, matches, verbose=0): michael@0: """Cull inappropriate matches. Possible reasons: michael@0: - a duplicate of a previous match michael@0: - not a disk file michael@0: - not executable (non-Windows) michael@0: If 'potential' is approved it is returned and added to 'matches'. michael@0: Otherwise, None is returned. michael@0: """ michael@0: for match in matches: # don't yield duplicates michael@0: if _samefile(potential[0], match[0]): michael@0: if verbose: michael@0: sys.stderr.write("duplicate: %s (%s)\n" % potential) michael@0: return None michael@0: else: michael@0: if not stat.S_ISREG(os.stat(potential[0]).st_mode): michael@0: if verbose: michael@0: sys.stderr.write("not a regular file: %s (%s)\n" % potential) michael@0: elif not os.access(potential[0], os.X_OK): michael@0: if verbose: michael@0: sys.stderr.write("no executable access: %s (%s)\n"\ michael@0: % potential) michael@0: else: michael@0: matches.append(potential) michael@0: return potential michael@0: michael@0: michael@0: #---- module API michael@0: michael@0: def whichgen(command, path=None, verbose=0, exts=None): michael@0: """Return a generator of full paths to the given command. michael@0: michael@0: "command" is a the name of the executable to search for. michael@0: "path" is an optional alternate path list to search. The default it michael@0: to use the PATH environment variable. michael@0: "verbose", if true, will cause a 2-tuple to be returned for each michael@0: match. The second element is a textual description of where the michael@0: match was found. michael@0: "exts" optionally allows one to specify a list of extensions to use michael@0: instead of the standard list for this system. This can michael@0: effectively be used as an optimization to, for example, avoid michael@0: stat's of "foo.vbs" when searching for "foo" and you know it is michael@0: not a VisualBasic script but ".vbs" is on PATHEXT. This option michael@0: is only supported on Windows. michael@0: michael@0: This method returns a generator which yields either full paths to michael@0: the given command or, if verbose, tuples of the form (, ). michael@0: """ michael@0: matches = [] michael@0: if path is None: michael@0: usingGivenPath = 0 michael@0: path = os.environ.get("PATH", "").split(os.pathsep) michael@0: if sys.platform.startswith("win"): michael@0: path.insert(0, os.curdir) # implied by Windows shell michael@0: else: michael@0: usingGivenPath = 1 michael@0: michael@0: # Windows has the concept of a list of extensions (PATHEXT env var). michael@0: if sys.platform.startswith("win"): michael@0: if exts is None: michael@0: exts = os.environ.get("PATHEXT", "").split(os.pathsep) michael@0: # If '.exe' is not in exts then obviously this is Win9x and michael@0: # or a bogus PATHEXT, then use a reasonable default. michael@0: for ext in exts: michael@0: if ext.lower() == ".exe": michael@0: break michael@0: else: michael@0: exts = ['.COM', '.EXE', '.BAT'] michael@0: elif not isinstance(exts, list): michael@0: raise TypeError("'exts' argument must be a list or None") michael@0: else: michael@0: if exts is not None: michael@0: raise WhichError("'exts' argument is not supported on "\ michael@0: "platform '%s'" % sys.platform) michael@0: exts = [] michael@0: michael@0: # File name cannot have path separators because PATH lookup does not michael@0: # work that way. michael@0: if os.sep in command or os.altsep and os.altsep in command: michael@0: pass michael@0: else: michael@0: for i in range(len(path)): michael@0: dirName = path[i] michael@0: # On windows the dirName *could* be quoted, drop the quotes michael@0: if sys.platform.startswith("win") and len(dirName) >= 2\ michael@0: and dirName[0] == '"' and dirName[-1] == '"': michael@0: dirName = dirName[1:-1] michael@0: for ext in ['']+exts: michael@0: absName = os.path.abspath( michael@0: os.path.normpath(os.path.join(dirName, command+ext))) michael@0: if os.path.isfile(absName): michael@0: if usingGivenPath: michael@0: fromWhere = "from given path element %d" % i michael@0: elif not sys.platform.startswith("win"): michael@0: fromWhere = "from PATH element %d" % i michael@0: elif i == 0: michael@0: fromWhere = "from current directory" michael@0: else: michael@0: fromWhere = "from PATH element %d" % (i-1) michael@0: match = _cull((absName, fromWhere), matches, verbose) michael@0: if match: michael@0: if verbose: michael@0: yield match michael@0: else: michael@0: yield match[0] michael@0: match = _getRegisteredExecutable(command) michael@0: if match is not None: michael@0: match = _cull(match, matches, verbose) michael@0: if match: michael@0: if verbose: michael@0: yield match michael@0: else: michael@0: yield match[0] michael@0: michael@0: michael@0: def which(command, path=None, verbose=0, exts=None): michael@0: """Return the full path to the first match of the given command on michael@0: the path. michael@0: michael@0: "command" is a the name of the executable to search for. michael@0: "path" is an optional alternate path list to search. The default it michael@0: to use the PATH environment variable. michael@0: "verbose", if true, will cause a 2-tuple to be returned. The second michael@0: element is a textual description of where the match was found. michael@0: "exts" optionally allows one to specify a list of extensions to use michael@0: instead of the standard list for this system. This can michael@0: effectively be used as an optimization to, for example, avoid michael@0: stat's of "foo.vbs" when searching for "foo" and you know it is michael@0: not a VisualBasic script but ".vbs" is on PATHEXT. This option michael@0: is only supported on Windows. michael@0: michael@0: If no match is found for the command, a WhichError is raised. michael@0: """ michael@0: try: michael@0: match = whichgen(command, path, verbose, exts).next() michael@0: except StopIteration: michael@0: raise WhichError("Could not find '%s' on the path." % command) michael@0: return match michael@0: michael@0: michael@0: def whichall(command, path=None, verbose=0, exts=None): michael@0: """Return a list of full paths to all matches of the given command michael@0: on the path. michael@0: michael@0: "command" is a the name of the executable to search for. michael@0: "path" is an optional alternate path list to search. The default it michael@0: to use the PATH environment variable. michael@0: "verbose", if true, will cause a 2-tuple to be returned for each michael@0: match. The second element is a textual description of where the michael@0: match was found. michael@0: "exts" optionally allows one to specify a list of extensions to use michael@0: instead of the standard list for this system. This can michael@0: effectively be used as an optimization to, for example, avoid michael@0: stat's of "foo.vbs" when searching for "foo" and you know it is michael@0: not a VisualBasic script but ".vbs" is on PATHEXT. This option michael@0: is only supported on Windows. michael@0: """ michael@0: return list( whichgen(command, path, verbose, exts) ) michael@0: michael@0: michael@0: michael@0: #---- mainline michael@0: michael@0: def main(argv): michael@0: all = 0 michael@0: verbose = 0 michael@0: altpath = None michael@0: exts = None michael@0: try: michael@0: optlist, args = getopt.getopt(argv[1:], 'haVvqp:e:', michael@0: ['help', 'all', 'version', 'verbose', 'quiet', 'path=', 'exts=']) michael@0: except getopt.GetoptError, msg: michael@0: sys.stderr.write("which: error: %s. Your invocation was: %s\n"\ michael@0: % (msg, argv)) michael@0: sys.stderr.write("Try 'which --help'.\n") michael@0: return 1 michael@0: for opt, optarg in optlist: michael@0: if opt in ('-h', '--help'): michael@0: print _cmdlnUsage michael@0: return 0 michael@0: elif opt in ('-V', '--version'): michael@0: print "which %s" % __version__ michael@0: return 0 michael@0: elif opt in ('-a', '--all'): michael@0: all = 1 michael@0: elif opt in ('-v', '--verbose'): michael@0: verbose = 1 michael@0: elif opt in ('-q', '--quiet'): michael@0: verbose = 0 michael@0: elif opt in ('-p', '--path'): michael@0: if optarg: michael@0: altpath = optarg.split(os.pathsep) michael@0: else: michael@0: altpath = [] michael@0: elif opt in ('-e', '--exts'): michael@0: if optarg: michael@0: exts = optarg.split(os.pathsep) michael@0: else: michael@0: exts = [] michael@0: michael@0: if len(args) == 0: michael@0: return -1 michael@0: michael@0: failures = 0 michael@0: for arg in args: michael@0: #print "debug: search for %r" % arg michael@0: nmatches = 0 michael@0: for match in whichgen(arg, path=altpath, verbose=verbose, exts=exts): michael@0: if verbose: michael@0: print "%s (%s)" % match michael@0: else: michael@0: print match michael@0: nmatches += 1 michael@0: if not all: michael@0: break michael@0: if not nmatches: michael@0: failures += 1 michael@0: return failures michael@0: michael@0: michael@0: if __name__ == "__main__": michael@0: sys.exit( main(sys.argv) ) michael@0: michael@0: