summaryrefslogtreecommitdiffstats
path: root/python/which
diff options
context:
space:
mode:
Diffstat (limited to 'python/which')
-rw-r--r--python/which/LICENSE.txt21
-rw-r--r--python/which/MANIFEST.in3
-rw-r--r--python/which/Makefile.win21
-rw-r--r--python/which/PKG-INFO21
-rw-r--r--python/which/README.txt229
-rw-r--r--python/which/TODO.txt113
-rw-r--r--python/which/build.py442
-rw-r--r--python/which/launcher.cpp404
-rw-r--r--python/which/logo.jpgbin0 -> 3635 bytes
-rw-r--r--python/which/setup.py70
-rw-r--r--python/which/test/test_which.py168
-rw-r--r--python/which/test/testsupport.py83
-rw-r--r--python/which/which.py335
13 files changed, 1910 insertions, 0 deletions
diff --git a/python/which/LICENSE.txt b/python/which/LICENSE.txt
new file mode 100644
index 000000000..de85cd53a
--- /dev/null
+++ b/python/which/LICENSE.txt
@@ -0,0 +1,21 @@
+Copyright (c) 2002-2005 ActiveState Corp.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/python/which/MANIFEST.in b/python/which/MANIFEST.in
new file mode 100644
index 000000000..3b8970ab8
--- /dev/null
+++ b/python/which/MANIFEST.in
@@ -0,0 +1,3 @@
+include *.py *.cpp *.in which.exe Makefile* *.txt logo.jpg
+exclude *~
+recursive-include test *.txt *.py
diff --git a/python/which/Makefile.win b/python/which/Makefile.win
new file mode 100644
index 000000000..c400aafd4
--- /dev/null
+++ b/python/which/Makefile.win
@@ -0,0 +1,21 @@
+# Copyright (c) 2002-2003 ActiveState Corp.
+# Author: Trent Mick (TrentM@ActiveState.com)
+#
+# A Makefile to do this: launcher.cpp -> foo.exe
+
+APPNAME=which
+
+# for release:
+CFLAGS=-D_CONSOLE -D_MBCS -DWIN32 -W3 -Ox -DNDEBUG -D_NDEBUG -MD
+LDFLAGS=/subsystem:console kernel32.lib user32.lib gdi32.lib advapi32.lib shlwapi.lib
+# for debug:
+# CFLAGS = -D_CONSOLE -D_MBCS /DWIN32 /Zi /Od /DDEBUG /D_DEBUG /MDd
+# LDFLAGS += /DEBUG
+
+$(APPNAME).exe: launcher.cpp
+ cl -nologo $(CFLAGS) -c launcher.cpp
+ link -nologo $(LDFLAGS) launcher.obj -out:$(APPNAME).exe
+
+clean:
+ if exist launcher.obj; del launcher.obj
+ if exist $(APPNAME).exe; del $(APPNAME).exe
diff --git a/python/which/PKG-INFO b/python/which/PKG-INFO
new file mode 100644
index 000000000..bfa8baa28
--- /dev/null
+++ b/python/which/PKG-INFO
@@ -0,0 +1,21 @@
+Metadata-Version: 1.0
+Name: which
+Version: 1.1.0
+Summary: a portable GNU which replacement
+Home-page: http://trentm.com/projects/which/
+Author: Trent Mick
+Author-email: TrentM@ActiveState.com
+License: MIT License
+Description: This is a GNU which replacement with the following features:
+ - it is portable (Windows, Linux);
+ - it understands PATHEXT on Windows;
+ - it can print <em>all</em> matches on the PATH;
+ - it can note "near misses" on the PATH (e.g. files that match but
+ may not, say, have execute permissions; and
+ - it can be used as a Python module.
+
+Keywords: which,find,path,where
+Platform: Windows
+Platform: Linux
+Platform: Mac OS X
+Platform: Unix
diff --git a/python/which/README.txt b/python/which/README.txt
new file mode 100644
index 000000000..6ece7f6de
--- /dev/null
+++ b/python/which/README.txt
@@ -0,0 +1,229 @@
+which.py -- a portable GNU which replacement
+============================================
+
+Download the latest which.py packages from here:
+ (source) http://trentm.com/downloads/which/1.1.0/which-1.1.0.zip
+
+
+Home : http://trentm.com/projects/which/
+License : MIT (see LICENSE.txt)
+Platforms : Windows, Linux, Mac OS X, Unix
+Current Version : 1.1
+Dev Status : mature, has been heavily used in a commercial product for
+ over 2 years
+Requirements : Python >= 2.3 (http://www.activestate.com/ActivePython/)
+
+
+What's new?
+-----------
+
+I have moved hosting of `which.py` from my old [Starship
+pages](http://starship.python.net/~tmick/) to this site. These starter
+docs have been improved a little bit. See the [Change Log](#changelog)
+below for more.
+
+**WARNING**: If you are upgrading your `which.py` and you also use my
+[process.py](../process/) module, you must upgrade `process.py` as well
+because of the `_version_/__version__` change in v1.1.0.
+
+
+Why which.py?
+-------------
+
+`which.py` is a small GNU-which replacement. It has the following
+features:
+
+- it is portable (Windows, Linux, Mac OS X, Un*x);
+- it understands PATHEXT and "App Paths" registration on Windows
+ (i.e. it will find everything that `start` does from the command shell);
+- it can print all matches on the PATH;
+- it can note "near misses" on the PATH (e.g. files that match but may
+ not, say, have execute permissions); and
+- it can be used as a Python module.
+
+I also would be happy to have this be a replacement for the `which.py` in the
+Python CVS tree at `dist/src/Tools/scripts/which.py` which is
+Unix-specific and not usable as a module; and perhaps for inclusion in
+the stdlib.
+
+Please send any feedback to [Trent Mick](mailto:TrentM@ActiveState.com).
+
+
+Install Notes
+-------------
+
+Download the latest `which.py` source package, unzip it, and run
+`python setup.py install`:
+
+ unzip which-1.1.0.zip
+ cd which-1.1.0
+ python setup.py install
+
+If your install fails then please visit [the Troubleshooting
+FAQ](http://trentm.com/faq.html#troubleshooting-python-package-installation).
+
+`which.py` can be used both as a module and as a script. By default,
+`which.py` will be installed into your Python's `site-packages`
+directory so it can be used as a module. On *Windows only*, `which.py`
+(and the launcher stub `which.exe`) will be installed in the Python
+install dir to (hopefully) put `which` on your PATH.
+
+On Un*x platforms (including Linux and Mac OS X) there is often a
+`which` executable already on your PATH. To use this `which` instead of
+your system's on those platforms you can manually do one of the
+following:
+
+- Copy `which.py` to `which` somewhere on your PATH ahead of the system
+ `which`. This can be a symlink, as well:
+
+ ln -s /PATH/TO/site-packages/which.py /usr/local/bin/which
+
+- Python 2.4 users might want to use Python's new '-m' switch and setup
+ and alias:
+
+ alias which='python -m which'
+
+ or stub script like this:
+
+ #!/bin/sh
+ python -m which $@
+
+
+Getting Started
+---------------
+
+Currently the best intro to using `which.py` as a module is its module
+documentation. Either install `which.py` and run:
+
+ pydoc which
+
+take a look at `which.py` in your editor or [here](which.py), or read
+on. Most commonly you'll use the `which()` method to find an
+executable:
+
+ >>> import which
+ >>> which.which("perl")
+ '/usr/local/bin/perl'
+
+Or you might want to know if you have multiple versions on your path:
+
+ >>> which.whichall("perl")
+ ['/usr/local/bin/perl', '/usr/bin/perl']
+
+Use `verbose` to see where your executable is being found. (On Windows
+this might not always be so obvious as your PATH environment variable.
+There is an "App Paths" area of the registry where the `start` command
+will find "registered" executables -- `which.py` mimics this.)
+
+ >>> which.whichall("perl", verbose=True)
+ [('/usr/local/bin/perl', 'from PATH element 10'),
+ ('/usr/bin/perl', 'from PATH element 15')]
+
+You can restrict the searched path:
+
+ >>> which.whichall("perl", path=["/usr/bin"])
+ ['/usr/bin/perl']
+
+There is a generator interface:
+
+ >>> for perl in which.whichgen("perl"):
+ ... print "found a perl here:", perl
+ ...
+ found a perl here: /usr/local/bin/perl
+ found a perl here: /usr/bin/perl
+
+An exception is raised if your executable is not found:
+
+ >>> which.which("fuzzywuzzy")
+ Traceback (most recent call last):
+ ...
+ which.WhichError: Could not find 'fuzzywuzzy' on the path.
+ >>>
+
+There are some other options too:
+
+ >>> help(which.which)
+ ...
+
+Run `which --help` to see command-line usage:
+
+ $ which --help
+ Show the full path of commands.
+
+ Usage:
+ which [<options>...] [<command-name>...]
+
+ Options:
+ -h, --help Print this help and exit.
+ -V, --version Print the version info and exit.
+
+ -a, --all Print *all* matching paths.
+ -v, --verbose Print out how matches were located and
+ show near misses on stderr.
+ -q, --quiet Just print out matches. I.e., do not print out
+ near misses.
+
+ -p <altpath>, --path=<altpath>
+ An alternative path (list of directories) may
+ be specified for searching.
+ -e <exts>, --exts=<exts>
+ Specify a list of extensions to consider instead
+ of the usual list (';'-separate list, Windows
+ only).
+
+ Show the full path to the program that would be run for each given
+ command name, if any. Which, like GNU's which, returns the number of
+ failed arguments, or -1 when no <command-name> was given.
+
+ Near misses include duplicates, non-regular files and (on Un*x)
+ files without executable access.
+
+
+Change Log
+----------
+
+### v1.1.0
+- Change version attributes and semantics. Before: had a _version_
+ tuple. After: __version__ is a string, __version_info__ is a tuple.
+
+### v1.0.3
+- Move hosting of which.py to trentm.com. Tweaks to associated bits
+ (README.txt, etc.)
+
+### v1.0.2:
+- Rename mainline handler function from _main() to main(). I can
+ conceive of it being called from externally.
+
+### v1.0.1:
+- Add an optimization for Windows to allow the optional
+ specification of a list of exts to consider when searching the
+ path.
+
+### v1.0.0:
+- Simpler interface: What was which() is now called whichgen() -- it
+ is a generator of matches. The simpler which() and whichall()
+ non-generator interfaces were added.
+
+### v0.8.1:
+- API change: 0.8.0's API change making "verbose" output the default
+ was a mistake -- it breaks backward compatibility for existing
+ uses of which in scripts. This makes verbose, once again, optional
+ but NOT the default.
+
+### v0.8.0:
+- bug fix: "App Paths" lookup had been crippled in 0.7.0. Restore that.
+- feature/module API change: Now print out (and return for the module
+ interface) from where a match was found, e.g. "(from PATH element 3)".
+ The module interfaces now returns (match, from-where) tuples.
+- bug fix: --path argument was broken (-p shortform was fine)
+
+### v0.7.0:
+- bug fix: Handle "App Paths" registered executable that does not
+ exist.
+- feature: Allow an alternate PATH to be specified via 'path'
+ optional argument to which.which() and via -p|--path command line
+ option.
+
+### v0.6.1:
+- first public release
+
diff --git a/python/which/TODO.txt b/python/which/TODO.txt
new file mode 100644
index 000000000..6df2de7f7
--- /dev/null
+++ b/python/which/TODO.txt
@@ -0,0 +1,113 @@
+# High Priority
+
+- Figure out the script story on the various platforms. On Windows, look into
+ the launcher thing that effbot has. Unix, don't install the script my
+ default. They can always do "python -m which ..." with Python >= 2.4.
+ Suggest an alias that some folks might want to use for that.
+
+
+# Medium Priority
+
+- define __all__?
+- improve test suite
+- test with other versions of Python
+- get the PATHEXT attached extension to reflect the actual canonical
+ case of file matches on Windows, currently the extension from PATHEXT
+ is always uppercase
+- What to do with Change 145624 by shanec. It is a bit of a
+ bastardization. Maybe allow this with a special option to allow the change
+ in semantics.
+
+ > Change 145624 by shanec@shanec-ocelotl on 2005/05/24 16:51:55
+ >
+ > make which work better on OSX
+ > - add support for searching /Applications and /Network/Applications
+ > - add support for .app bundles
+ >
+ > Affected files ...
+ >
+ > ... //depot/main/Apps/Komodo-devel/src/python-sitelib/which.py#7 edit
+ >
+ > Differences ...
+ >
+ > ==== //depot/main/Apps/Komodo-devel/src/python-sitelib/which.py#7 (text) ====
+ >
+ > @@ -126,10 +126,11 @@
+ > sys.stderr.write("duplicate: %s (%s)\n" % potential)
+ > return None
+ > else:
+ > - if not stat.S_ISREG(os.stat(potential[0]).st_mode):
+ > + darwinApp = sys.platform == 'darwin' and potential[0][-4:]=='.app'
+ > + if not darwinApp and not stat.S_ISREG(os.stat(potential[0]).st_mode):
+ > if verbose:
+ > sys.stderr.write("not a regular file: %s (%s)\n" % potential)
+ > - elif not os.access(potential[0], os.X_OK):
+ > + elif not darwinApp and not os.access(potential[0], os.X_OK):
+ > if verbose:
+ > sys.stderr.write("no executable access: %s (%s)\n"\
+ > % potential)
+ > @@ -166,6 +167,9 @@
+ > path = os.environ.get("PATH", "").split(os.pathsep)
+ > if sys.platform.startswith("win"):
+ > path.insert(0, os.curdir) # implied by Windows shell
+ > + if sys.platform == 'darwin':
+ > + path.insert(0, '/Network/Applications')
+ > + path.insert(0, '/Applications')
+ > else:
+ > usingGivenPath = 1
+ >
+ > @@ -182,6 +186,9 @@
+ > exts = ['.COM', '.EXE', '.BAT']
+ > elif not isinstance(exts, list):
+ > raise TypeError("'exts' argument must be a list or None")
+ > + elif sys.platform == 'darwin':
+ > + if exts is None:
+ > + exts = ['.app']
+ > else:
+ > if exts is not None:
+ > raise WhichError("'exts' argument is not supported on "\
+ > @@ -202,7 +209,8 @@
+ > for ext in ['']+exts:
+ > absName = os.path.abspath(
+ > os.path.normpath(os.path.join(dirName, command+ext)))
+ > - if os.path.isfile(absName):
+ > + if os.path.isfile(absName) or (sys.platform == 'darwin' and \
+ > + absName[-4:]=='.app' and os.path.isdir(absName)):
+ > if usingGivenPath:
+ > fromWhere = "from given path element %d" % i
+ > elif not sys.platform.startswith("win"):
+
+ Here is a start with slight improvements:
+
+ > Index: which.py
+ > ===================================================================
+ > --- which.py (revision 270)
+ > +++ which.py (working copy)
+ > @@ -126,9 +126,18 @@
+ > sys.stderr.write("duplicate: %s (%s)\n" % potential)
+ > return None
+ > else:
+ > - if not stat.S_ISREG(os.stat(potential[0]).st_mode):
+ > + st_mode = os.stat(potential[0]).st_mode
+ > + isMacAppBundle = sys.platform == "darwin" \
+ > + and potential[0].endswith(".app") \
+ > + and stat.S_ISDIR(st_mode)
+ > + if not isMacAppBundle and not stat.S_ISREG(st_mode):
+ > if verbose:
+ > - sys.stderr.write("not a regular file: %s (%s)\n" % potential)
+ > + if sys.platform == "darwin":
+ > + sys.stderr.write("not a regular file or .app bundle: "
+ > + "%s (%s)\n" % potential)
+ > + else:
+ > + sys.stderr.write("not a regular file: %s (%s)\n"
+ > + % potential)
+ > elif not os.access(potential[0], os.X_OK):
+ > if verbose:
+ > sys.stderr.write("no executable access: %s (%s)\n"\
+
+
+# Low Priority
+
+- have a version for pre-generators (i.e. Python 2.1)
+- add a "logging" interface
+
diff --git a/python/which/build.py b/python/which/build.py
new file mode 100644
index 000000000..3c8f09d39
--- /dev/null
+++ b/python/which/build.py
@@ -0,0 +1,442 @@
+#!/usr/bin/env python
+# Copyright (c) 2002-2005 ActiveState
+# See LICENSE.txt for license details.
+
+"""
+ which.py dev build script
+
+ Usage:
+ python build.py [<options>...] [<targets>...]
+
+ Options:
+ --help, -h Print this help and exit.
+ --targets, -t List all available targets.
+
+ This is the primary build script for the which.py project. It exists
+ to assist in building, maintaining, and distributing this project.
+
+ It is intended to have Makefile semantics. I.e. 'python build.py'
+ will build execute the default target, 'python build.py foo' will
+ build target foo, etc. However, there is no intelligent target
+ interdependency tracking (I suppose I could do that with function
+ attributes).
+"""
+
+import os
+from os.path import basename, dirname, splitext, isfile, isdir, exists, \
+ join, abspath, normpath
+import sys
+import getopt
+import types
+import getpass
+import shutil
+import glob
+import logging
+import re
+
+
+
+#---- exceptions
+
+class Error(Exception):
+ pass
+
+
+
+#---- globals
+
+log = logging.getLogger("build")
+
+
+
+
+#---- globals
+
+_project_name_ = "which"
+
+
+
+#---- internal support routines
+
+def _get_trentm_com_dir():
+ """Return the path to the local trentm.com source tree."""
+ d = normpath(join(dirname(__file__), os.pardir, "trentm.com"))
+ if not isdir(d):
+ raise Error("could not find 'trentm.com' src dir at '%s'" % d)
+ return d
+
+def _get_local_bits_dir():
+ import imp
+ info = imp.find_module("tmconfig", [_get_trentm_com_dir()])
+ tmconfig = imp.load_module("tmconfig", *info)
+ return tmconfig.bitsDir
+
+def _get_project_bits_dir():
+ d = normpath(join(dirname(__file__), "bits"))
+ return d
+
+def _get_project_version():
+ import imp, os
+ data = imp.find_module(_project_name_, [os.path.dirname(__file__)])
+ mod = imp.load_module(_project_name_, *data)
+ return mod.__version__
+
+
+# Recipe: run (0.5.1) in /Users/trentm/tm/recipes/cookbook
+_RUN_DEFAULT_LOGSTREAM = ("RUN", "DEFAULT", "LOGSTREAM")
+def __run_log(logstream, msg, *args, **kwargs):
+ if not logstream:
+ pass
+ elif logstream is _RUN_DEFAULT_LOGSTREAM:
+ try:
+ log.debug(msg, *args, **kwargs)
+ except NameError:
+ pass
+ else:
+ logstream(msg, *args, **kwargs)
+
+def _run(cmd, logstream=_RUN_DEFAULT_LOGSTREAM):
+ """Run the given command.
+
+ "cmd" is the command to run
+ "logstream" is an optional logging stream on which to log the command.
+ If None, no logging is done. If unspecifed, this looks for a Logger
+ instance named 'log' and logs the command on log.debug().
+
+ Raises OSError is the command returns a non-zero exit status.
+ """
+ __run_log(logstream, "running '%s'", cmd)
+ retval = os.system(cmd)
+ if hasattr(os, "WEXITSTATUS"):
+ status = os.WEXITSTATUS(retval)
+ else:
+ status = retval
+ if status:
+ #TODO: add std OSError attributes or pick more approp. exception
+ raise OSError("error running '%s': %r" % (cmd, status))
+
+def _run_in_dir(cmd, cwd, logstream=_RUN_DEFAULT_LOGSTREAM):
+ old_dir = os.getcwd()
+ try:
+ os.chdir(cwd)
+ __run_log(logstream, "running '%s' in '%s'", cmd, cwd)
+ _run(cmd, logstream=None)
+ finally:
+ os.chdir(old_dir)
+
+
+# Recipe: rmtree (0.5) in /Users/trentm/tm/recipes/cookbook
+def _rmtree_OnError(rmFunction, filePath, excInfo):
+ if excInfo[0] == OSError:
+ # presuming because file is read-only
+ os.chmod(filePath, 0777)
+ rmFunction(filePath)
+def _rmtree(dirname):
+ import shutil
+ shutil.rmtree(dirname, 0, _rmtree_OnError)
+
+
+# Recipe: pretty_logging (0.1) in /Users/trentm/tm/recipes/cookbook
+class _PerLevelFormatter(logging.Formatter):
+ """Allow multiple format string -- depending on the log level.
+
+ A "fmtFromLevel" optional arg is added to the constructor. It can be
+ a dictionary mapping a log record level to a format string. The
+ usual "fmt" argument acts as the default.
+ """
+ def __init__(self, fmt=None, datefmt=None, fmtFromLevel=None):
+ logging.Formatter.__init__(self, fmt, datefmt)
+ if fmtFromLevel is None:
+ self.fmtFromLevel = {}
+ else:
+ self.fmtFromLevel = fmtFromLevel
+ def format(self, record):
+ record.levelname = record.levelname.lower()
+ if record.levelno in self.fmtFromLevel:
+ #XXX This is a non-threadsafe HACK. Really the base Formatter
+ # class should provide a hook accessor for the _fmt
+ # attribute. *Could* add a lock guard here (overkill?).
+ _saved_fmt = self._fmt
+ self._fmt = self.fmtFromLevel[record.levelno]
+ try:
+ return logging.Formatter.format(self, record)
+ finally:
+ self._fmt = _saved_fmt
+ else:
+ return logging.Formatter.format(self, record)
+
+def _setup_logging():
+ hdlr = logging.StreamHandler()
+ defaultFmt = "%(name)s: %(levelname)s: %(message)s"
+ infoFmt = "%(name)s: %(message)s"
+ fmtr = _PerLevelFormatter(fmt=defaultFmt,
+ fmtFromLevel={logging.INFO: infoFmt})
+ hdlr.setFormatter(fmtr)
+ logging.root.addHandler(hdlr)
+ log.setLevel(logging.INFO)
+
+
+def _getTargets():
+ """Find all targets and return a dict of targetName:targetFunc items."""
+ targets = {}
+ for name, attr in sys.modules[__name__].__dict__.items():
+ if name.startswith('target_'):
+ targets[ name[len('target_'):] ] = attr
+ return targets
+
+def _listTargets(targets):
+ """Pretty print a list of targets."""
+ width = 77
+ nameWidth = 15 # min width
+ for name in targets.keys():
+ nameWidth = max(nameWidth, len(name))
+ nameWidth += 2 # space btwn name and doc
+ format = "%%-%ds%%s" % nameWidth
+ print format % ("TARGET", "DESCRIPTION")
+ for name, func in sorted(targets.items()):
+ doc = _first_paragraph(func.__doc__ or "", True)
+ if len(doc) > (width - nameWidth):
+ doc = doc[:(width-nameWidth-3)] + "..."
+ print format % (name, doc)
+
+
+# Recipe: first_paragraph (1.0.1) in /Users/trentm/tm/recipes/cookbook
+def _first_paragraph(text, join_lines=False):
+ """Return the first paragraph of the given text."""
+ para = text.lstrip().split('\n\n', 1)[0]
+ if join_lines:
+ lines = [line.strip() for line in para.splitlines(0)]
+ para = ' '.join(lines)
+ return para
+
+
+
+#---- build targets
+
+def target_default():
+ target_all()
+
+def target_all():
+ """Build all release packages."""
+ log.info("target: default")
+ if sys.platform == "win32":
+ target_launcher()
+ target_sdist()
+ target_webdist()
+
+
+def target_clean():
+ """remove all build/generated bits"""
+ log.info("target: clean")
+ if sys.platform == "win32":
+ _run("nmake -f Makefile.win clean")
+
+ ver = _get_project_version()
+ dirs = ["dist", "build", "%s-%s" % (_project_name_, ver)]
+ for d in dirs:
+ print "removing '%s'" % d
+ if os.path.isdir(d): _rmtree(d)
+
+ patterns = ["*.pyc", "*~", "MANIFEST",
+ os.path.join("test", "*~"),
+ os.path.join("test", "*.pyc"),
+ ]
+ for pattern in patterns:
+ for file in glob.glob(pattern):
+ print "removing '%s'" % file
+ os.unlink(file)
+
+
+def target_launcher():
+ """Build the Windows launcher executable."""
+ log.info("target: launcher")
+ assert sys.platform == "win32", "'launcher' target only supported on Windows"
+ _run("nmake -f Makefile.win")
+
+
+def target_docs():
+ """Regenerate some doc bits from project-info.xml."""
+ log.info("target: docs")
+ _run("projinfo -f project-info.xml -R -o README.txt --force")
+ _run("projinfo -f project-info.xml --index-markdown -o index.markdown --force")
+
+
+def target_sdist():
+ """Build a source distribution."""
+ log.info("target: sdist")
+ target_docs()
+ bitsDir = _get_project_bits_dir()
+ _run("python setup.py sdist -f --formats zip -d %s" % bitsDir,
+ log.info)
+
+
+def target_webdist():
+ """Build a web dist package.
+
+ "Web dist" packages are zip files with '.web' package. All files in
+ the zip must be under a dir named after the project. There must be a
+ webinfo.xml file at <projname>/webinfo.xml. This file is "defined"
+ by the parsing in trentm.com/build.py.
+ """
+ assert sys.platform != "win32", "'webdist' not implemented for win32"
+ log.info("target: webdist")
+ bitsDir = _get_project_bits_dir()
+ buildDir = join("build", "webdist")
+ distDir = join(buildDir, _project_name_)
+ if exists(buildDir):
+ _rmtree(buildDir)
+ os.makedirs(distDir)
+
+ target_docs()
+
+ # Copy the webdist bits to the build tree.
+ manifest = [
+ "project-info.xml",
+ "index.markdown",
+ "LICENSE.txt",
+ "which.py",
+ "logo.jpg",
+ ]
+ for src in manifest:
+ if dirname(src):
+ dst = join(distDir, dirname(src))
+ os.makedirs(dst)
+ else:
+ dst = distDir
+ _run("cp %s %s" % (src, dst))
+
+ # Zip up the webdist contents.
+ ver = _get_project_version()
+ bit = abspath(join(bitsDir, "%s-%s.web" % (_project_name_, ver)))
+ if exists(bit):
+ os.remove(bit)
+ _run_in_dir("zip -r %s %s" % (bit, _project_name_), buildDir, log.info)
+
+
+def target_install():
+ """Use the setup.py script to install."""
+ log.info("target: install")
+ _run("python setup.py install")
+
+
+def target_upload_local():
+ """Update release bits to *local* trentm.com bits-dir location.
+
+ This is different from the "upload" target, which uploads release
+ bits remotely to trentm.com.
+ """
+ log.info("target: upload_local")
+ assert sys.platform != "win32", "'upload_local' not implemented for win32"
+
+ ver = _get_project_version()
+ localBitsDir = _get_local_bits_dir()
+ uploadDir = join(localBitsDir, _project_name_, ver)
+
+ bitsPattern = join(_get_project_bits_dir(),
+ "%s-*%s*" % (_project_name_, ver))
+ bits = glob.glob(bitsPattern)
+ if not bits:
+ log.info("no bits matching '%s' to upload", bitsPattern)
+ else:
+ if not exists(uploadDir):
+ os.makedirs(uploadDir)
+ for bit in bits:
+ _run("cp %s %s" % (bit, uploadDir), log.info)
+
+
+def target_upload():
+ """Upload binary and source distribution to trentm.com bits
+ directory.
+ """
+ log.info("target: upload")
+
+ ver = _get_project_version()
+ bitsDir = _get_project_bits_dir()
+ bitsPattern = join(bitsDir, "%s-*%s*" % (_project_name_, ver))
+ bits = glob.glob(bitsPattern)
+ if not bits:
+ log.info("no bits matching '%s' to upload", bitsPattern)
+ return
+
+ # Ensure have all the expected bits.
+ expectedBits = [
+ re.compile("%s-.*\.zip$" % _project_name_),
+ re.compile("%s-.*\.web$" % _project_name_)
+ ]
+ for expectedBit in expectedBits:
+ for bit in bits:
+ if expectedBit.search(bit):
+ break
+ else:
+ raise Error("can't find expected bit matching '%s' in '%s' dir"
+ % (expectedBit.pattern, bitsDir))
+
+ # Upload the bits.
+ user = "trentm"
+ host = "trentm.com"
+ remoteBitsBaseDir = "~/data/bits"
+ remoteBitsDir = join(remoteBitsBaseDir, _project_name_, ver)
+ if sys.platform == "win32":
+ ssh = "plink"
+ scp = "pscp -unsafe"
+ else:
+ ssh = "ssh"
+ scp = "scp"
+ _run("%s %s@%s 'mkdir -p %s'" % (ssh, user, host, remoteBitsDir), log.info)
+ for bit in bits:
+ _run("%s %s %s@%s:%s" % (scp, bit, user, host, remoteBitsDir),
+ log.info)
+
+
+def target_check_version():
+ """grep for version strings in source code
+
+ List all things that look like version strings in the source code.
+ Used for checking that versioning is updated across the board.
+ """
+ sources = [
+ "which.py",
+ "project-info.xml",
+ ]
+ pattern = r'[0-9]\+\(\.\|, \)[0-9]\+\(\.\|, \)[0-9]\+'
+ _run('grep -n "%s" %s' % (pattern, ' '.join(sources)), None)
+
+
+
+#---- mainline
+
+def build(targets=[]):
+ log.debug("build(targets=%r)" % targets)
+ available = _getTargets()
+ if not targets:
+ if available.has_key('default'):
+ return available['default']()
+ else:
+ log.warn("No default target available. Doing nothing.")
+ else:
+ for target in targets:
+ if available.has_key(target):
+ retval = available[target]()
+ if retval:
+ raise Error("Error running '%s' target: retval=%s"\
+ % (target, retval))
+ else:
+ raise Error("Unknown target: '%s'" % target)
+
+def main(argv):
+ _setup_logging()
+
+ # Process options.
+ optlist, targets = getopt.getopt(argv[1:], 'ht', ['help', 'targets'])
+ for opt, optarg in optlist:
+ if opt in ('-h', '--help'):
+ sys.stdout.write(__doc__ + '\n')
+ return 0
+ elif opt in ('-t', '--targets'):
+ return _listTargets(_getTargets())
+
+ return build(targets)
+
+if __name__ == "__main__":
+ sys.exit( main(sys.argv) )
+
diff --git a/python/which/launcher.cpp b/python/which/launcher.cpp
new file mode 100644
index 000000000..36bbbe866
--- /dev/null
+++ b/python/which/launcher.cpp
@@ -0,0 +1,404 @@
+/*
+ * Copyright (c) 2002-2003 ActiveState Corp.
+ * Author: Trent Mick (TrentM@ActiveState.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/* Console launch executable.
+ *
+ * This program exists solely to launch:
+ * python <installdir>/<exename>.py <argv>
+ * on Windows. "<exename>.py" must be in the same directory.
+ *
+ * Rationale:
+ * - On some Windows flavours .py *can* be put on the PATHEXT to be
+ * able to find "<exename>.py" if it is on the PATH. This is fine
+ * until you need shell redirection to work. It does NOT for
+ * extensions to PATHEXT. Redirection *does* work for "python
+ * <script>.py" so we will try to do that.
+ */
+
+#ifdef WIN32
+ #include <windows.h>
+ #include <process.h>
+ #include <direct.h>
+ #include <shlwapi.h>
+#else /* linux */
+ #include <unistd.h>
+#endif /* WIN32 */
+#include <sys/stat.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+
+//---- constants
+
+#define BUF_LENGTH 2048
+#define MAX_PYTHON_ARGS 50
+#define MAX_FILES 50
+#define MAXPATHLEN 1024
+#ifdef WIN32
+ #define SEP '\\'
+ #define ALTSEP '/'
+ // path list element separator
+ #define DELIM ';'
+#else /* linux */
+ #define SEP '/'
+ // path list element separator
+ #define DELIM ':'
+#endif
+
+#ifdef WIN32
+ #define spawnvp _spawnvp
+ #if defined(_MSC_VER) && _MSC_VER < 1900
+ #define snprintf _snprintf
+ #define vsnprintf _vsnprintf
+ #endif
+ //NOTE: this is for the stat *call* and the stat *struct*
+ #define stat _stat
+#endif
+
+
+//---- globals
+
+char* programName = NULL;
+char* programPath = NULL;
+#ifndef WIN32 /* i.e. linux */
+ extern char **environ; // the user environment
+#endif /* linux */
+
+//---- error logging functions
+
+void _LogError(const char* format ...)
+{
+ va_list ap;
+ va_start(ap, format);
+#if defined(WIN32) && defined(_WINDOWS)
+ // put up a MessageBox
+ char caption[BUF_LENGTH+1];
+ snprintf(caption, BUF_LENGTH, "Error in %s", programName);
+ char msg[BUF_LENGTH+1];
+ vsnprintf(msg, BUF_LENGTH, format, ap);
+ va_end(ap);
+ MessageBox(NULL, msg, caption, MB_OK | MB_ICONEXCLAMATION);
+#else
+ fprintf(stderr, "%s: error: ", programName);
+ vfprintf(stderr, format, ap);
+ va_end(ap);
+#endif /* WIN32 && _WINDOWS */
+}
+
+
+void _LogWarning(const char* format ...)
+{
+ va_list ap;
+ va_start(ap, format);
+#if defined(WIN32) && defined(_WINDOWS)
+ // put up a MessageBox
+ char caption[BUF_LENGTH+1];
+ snprintf(caption, BUF_LENGTH, "Warning in %s", programName);
+ char msg[BUF_LENGTH+1];
+ vsnprintf(msg, BUF_LENGTH, format, ap);
+ va_end(ap);
+ MessageBox(NULL, msg, caption, MB_OK | MB_ICONWARNING);
+#else
+ fprintf(stderr, "%s: warning: ", programName);
+ vfprintf(stderr, format, ap);
+ va_end(ap);
+#endif /* WIN32 && _WINDOWS */
+}
+
+
+
+//---- utilities functions
+
+/* _IsDir: Is the given dirname an existing directory */
+static int _IsDir(char *dirname)
+{
+#ifdef WIN32
+ DWORD dwAttrib;
+ dwAttrib = GetFileAttributes(dirname);
+ if (dwAttrib == -1) {
+ return 0;
+ }
+ if (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) {
+ return 1;
+ }
+ return 0;
+#else /* i.e. linux */
+ struct stat buf;
+ if (stat(dirname, &buf) != 0)
+ return 0;
+ if (!S_ISDIR(buf.st_mode))
+ return 0;
+ return 1;
+#endif
+}
+
+
+/* _IsLink: Is the given filename a symbolic link */
+static int _IsLink(char *filename)
+{
+#ifdef WIN32
+ return 0;
+#else /* i.e. linux */
+ struct stat buf;
+ if (lstat(filename, &buf) != 0)
+ return 0;
+ if (!S_ISLNK(buf.st_mode))
+ return 0;
+ return 1;
+#endif
+}
+
+
+/* Is executable file
+ * On Linux: check 'x' permission. On Windows: just check existence.
+ */
+static int _IsExecutableFile(char *filename)
+{
+#ifdef WIN32
+ return (int)PathFileExists(filename);
+#else /* i.e. linux */
+ struct stat buf;
+ if (stat(filename, &buf) != 0)
+ return 0;
+ if (!S_ISREG(buf.st_mode))
+ return 0;
+ if ((buf.st_mode & 0111) == 0)
+ return 0;
+ return 1;
+#endif /* WIN32 */
+}
+
+
+/* _GetProgramPath: Determine the absolute path to the given program name.
+ *
+ * Takes into account the current working directory, etc.
+ * The implementations require the global 'programName' to be set.
+ */
+#ifdef WIN32
+ static char* _GetProgramPath(void)
+ {
+ //XXX this is ugly but I didn't want to use malloc, no reason
+ static char progPath[MAXPATHLEN+1];
+ // get absolute path to module
+ if (!GetModuleFileName(NULL, progPath, MAXPATHLEN)) {
+ _LogError("could not get absolute program name from "\
+ "GetModuleFileName\n");
+ exit(1);
+ }
+ // just need dirname
+ for (char* p = progPath+strlen(progPath);
+ *p != SEP && *p != ALTSEP;
+ --p)
+ {
+ *p = '\0';
+ }
+ *p = '\0'; // remove the trailing SEP as well
+
+ return progPath;
+ }
+#else
+
+ /* _JoinPath requires that any buffer argument passed to it has at
+ least MAXPATHLEN + 1 bytes allocated. If this requirement is met,
+ it guarantees that it will never overflow the buffer. If stuff
+ is too long, buffer will contain a truncated copy of stuff.
+ */
+ static void
+ _JoinPath(char *buffer, char *stuff)
+ {
+ size_t n, k;
+ if (stuff[0] == SEP)
+ n = 0;
+ else {
+ n = strlen(buffer);
+ if (n > 0 && buffer[n-1] != SEP && n < MAXPATHLEN)
+ buffer[n++] = SEP;
+ }
+ k = strlen(stuff);
+ if (n + k > MAXPATHLEN)
+ k = MAXPATHLEN - n;
+ strncpy(buffer+n, stuff, k);
+ buffer[n+k] = '\0';
+ }
+
+
+ static char*
+ _GetProgramPath(void)
+ {
+ /* XXX this routine does *no* error checking */
+ char* path = getenv("PATH");
+ static char progPath[MAXPATHLEN+1];
+
+ /* If there is no slash in the argv0 path, then we have to
+ * assume the program is on the user's $PATH, since there's no
+ * other way to find a directory to start the search from. If
+ * $PATH isn't exported, you lose.
+ */
+ if (strchr(programName, SEP)) {
+ strncpy(progPath, programName, MAXPATHLEN);
+ }
+ else if (path) {
+ int bufspace = MAXPATHLEN;
+ while (1) {
+ char *delim = strchr(path, DELIM);
+
+ if (delim) {
+ size_t len = delim - path;
+ if (len > bufspace) {
+ len = bufspace;
+ }
+ strncpy(progPath, path, len);
+ *(progPath + len) = '\0';
+ bufspace -= len;
+ }
+ else {
+ strncpy(progPath, path, bufspace);
+ }
+
+ _JoinPath(progPath, programName);
+ if (_IsExecutableFile(progPath)) {
+ break;
+ }
+
+ if (!delim) {
+ progPath[0] = '\0';
+ break;
+ }
+ path = delim + 1;
+ }
+ }
+ else {
+ progPath[0] = '\0';
+ }
+
+ // now we have to resolve a string of possible symlinks
+ // - we'll just handle the simple case of a single level of
+ // indirection
+ //
+ // XXX note this does not handle multiple levels of symlinks
+ // here is pseudo-code for that (please implement it :):
+ // while 1:
+ // if islink(progPath):
+ // linkText = readlink(progPath)
+ // if isabsolute(linkText):
+ // progPath = os.path.join(dirname(progPath), linkText)
+ // else:
+ // progPath = linkText
+ // else:
+ // break
+ if (_IsLink(progPath)) {
+ char newProgPath[MAXPATHLEN+1];
+ readlink(progPath, newProgPath, MAXPATHLEN);
+ strncpy(progPath, newProgPath, MAXPATHLEN);
+ }
+
+
+ // prefix with the current working directory if the path is
+ // relative to conform with the Windows version of this
+ if (strlen(progPath) != 0 && progPath[0] != SEP) {
+ char cwd[MAXPATHLEN+1];
+ char tmp[MAXPATHLEN+1];
+ //XXX should check for failure retvals
+ getcwd(cwd, MAXPATHLEN);
+ snprintf(tmp, MAXPATHLEN, "%s%c%s", cwd, SEP, progPath);
+ strncpy(progPath, tmp, MAXPATHLEN);
+ }
+
+ // 'progPath' now contains the full path to the program *and* the program
+ // name. The latter is not desire.
+ char* pLetter = progPath + strlen(progPath);
+ for (;pLetter != progPath && *pLetter != SEP; --pLetter) {
+ /* do nothing */
+ }
+ *pLetter = '\0';
+
+ return progPath;
+ }
+#endif /* WIN32 */
+
+
+//---- mainline
+
+int main(int argc, char** argv)
+{
+ programName = argv[0];
+ programPath = _GetProgramPath();
+
+ // Determine the extension-less program basename.
+ // XXX Will not always handle app names with '.' in them (other than
+ // the '.' for the extension.
+ char programNameNoExt[MAXPATHLEN+1];
+ char *pStart, *pEnd;
+ pStart = pEnd = programName + strlen(programName) - 1;
+ while (pStart != programName && *(pStart-1) != SEP) {
+ pStart--;
+ }
+ while (1) {
+ if (pEnd == pStart) {
+ pEnd = programName + strlen(programName) - 1;
+ break;
+ }
+ pEnd--;
+ if (*(pEnd+1) == '.') {
+ break;
+ }
+ }
+ strncpy(programNameNoExt, pStart, pEnd-pStart+1);
+ *(programNameNoExt+(pEnd-pStart+1)) = '\0';
+
+ // determine the full path to "<exename>.py"
+ char pyFile[MAXPATHLEN+1];
+ snprintf(pyFile, MAXPATHLEN, "%s%c%s.py", programPath, SEP,
+ programNameNoExt);
+
+ // Build the argument array for launching.
+ char* pythonArgs[MAX_PYTHON_ARGS+1];
+ int nPythonArgs = 0;
+ pythonArgs[nPythonArgs++] = "python";
+ pythonArgs[nPythonArgs++] = "-tt";
+ pythonArgs[nPythonArgs++] = pyFile;
+ for (int i = 1; i < argc; ++i) {
+ pythonArgs[nPythonArgs++] = argv[i];
+ }
+ pythonArgs[nPythonArgs++] = NULL;
+
+ return _spawnvp(_P_WAIT, pythonArgs[0], pythonArgs);
+}
+
+
+//---- mainline for win32 subsystem:windows app
+#ifdef WIN32
+ int WINAPI WinMain(
+ HINSTANCE hInstance, /* handle to current instance */
+ HINSTANCE hPrevInstance, /* handle to previous instance */
+ LPSTR lpCmdLine, /* pointer to command line */
+ int nCmdShow /* show state of window */
+ )
+ {
+ return main(__argc, __argv);
+ }
+#endif
+
diff --git a/python/which/logo.jpg b/python/which/logo.jpg
new file mode 100644
index 000000000..300c23f14
--- /dev/null
+++ b/python/which/logo.jpg
Binary files differ
diff --git a/python/which/setup.py b/python/which/setup.py
new file mode 100644
index 000000000..3f6d1072c
--- /dev/null
+++ b/python/which/setup.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# Copyright (c) 2002-2005 ActiveState Corp.
+# Author: Trent Mick (TrentM@ActiveState.com)
+
+"""Distutils setup script for 'which'."""
+
+import sys
+import os
+import shutil
+from distutils.core import setup
+
+
+#---- support routines
+
+def _getVersion():
+ import which
+ return which.__version__
+
+def _getBinDir():
+ """Return the current Python's bindir."""
+ if sys.platform.startswith("win"):
+ bindir = sys.prefix
+ else:
+ bindir = os.path.join(sys.prefix, "bin")
+ return bindir
+
+
+#---- setup mainline
+
+if sys.platform == "win32":
+ scripts = []
+ binFiles = ["which.exe", "which.py"]
+else:
+ #XXX Disable installing which as a script on non-Windows platforms.
+ # It can get in the way of the system which.
+ #
+ #if os.path.exists("which"):
+ # os.remove("which")
+ #shutil.copy2("which.py", "which")
+ #scripts = ["which"]
+ binFiles = []
+ scripts = []
+
+setup(name="which",
+ version=_getVersion(),
+ description="a portable GNU which replacement",
+ author="Trent Mick",
+ author_email="TrentM@ActiveState.com",
+ url="http://trentm.com/projects/which/",
+ license="MIT License",
+ platforms=["Windows", "Linux", "Mac OS X", "Unix"],
+ long_description="""\
+This is a GNU which replacement with the following features:
+ - it is portable (Windows, Linux);
+ - it understands PATHEXT on Windows;
+ - it can print <em>all</em> matches on the PATH;
+ - it can note "near misses" on the PATH (e.g. files that match but
+ may not, say, have execute permissions; and
+ - it can be used as a Python module.
+""",
+ keywords=["which", "find", "path", "where"],
+
+ py_modules=['which'],
+ scripts=scripts,
+ # Install the Windows script/executable bits as data files with
+ # distutils chosen scripts install dir on Windows,
+ # "<prefix>/Scripts", is just wrong.
+ data_files=[ (_getBinDir(), binFiles) ],
+ )
+
diff --git a/python/which/test/test_which.py b/python/which/test/test_which.py
new file mode 100644
index 000000000..b56976989
--- /dev/null
+++ b/python/which/test/test_which.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+# Copyright (c) 2002-2003 ActiveState Corp.
+# Author: Trent Mick (TrentM@ActiveState.com)
+
+"""Test suite for which.py."""
+
+import sys
+import os
+import re
+import tempfile
+import unittest
+
+import testsupport
+
+#XXX:TODO
+# - def test_registry_success(self): ...App Paths setting
+# - def test_registry_noexist(self):
+# - test all the other options
+# - test on linux
+# - test the module API
+
+class WhichTestCase(unittest.TestCase):
+ def setUp(self):
+ """Create a temp directory with a couple test "commands".
+ The temp dir can be added to the PATH, etc, for testing purposes.
+ """
+ # Find the which.py to call.
+ whichPy = os.path.join(os.path.dirname(__file__),
+ os.pardir, "which.py")
+ self.which = sys.executable + " " + whichPy
+
+ # Setup the test environment.
+ self.tmpdir = tempfile.mktemp()
+ os.makedirs(self.tmpdir)
+ if sys.platform.startswith("win"):
+ self.testapps = ['whichtestapp1.exe',
+ 'whichtestapp2.exe',
+ 'whichtestapp3.wta']
+ else:
+ self.testapps = ['whichtestapp1', 'whichtestapp2']
+ for app in self.testapps:
+ path = os.path.join(self.tmpdir, app)
+ open(path, 'wb').write('\n')
+ os.chmod(path, 0755)
+
+ def tearDown(self):
+ testsupport.rmtree(self.tmpdir)
+
+ def test_opt_h(self):
+ output, error, retval = testsupport.run(self.which+' --h')
+ token = 'Usage:'
+ self.failUnless(output.find(token) != -1,
+ "'%s' was not found in 'which -h' output: '%s' "\
+ % (token, output))
+ self.failUnless(retval == 0,
+ "'which -h' did not return 0: retval=%d" % retval)
+
+ def test_opt_help(self):
+ output, error, retval = testsupport.run(self.which+' --help')
+ token = 'Usage:'
+ self.failUnless(output.find(token) != -1,
+ "'%s' was not found in 'which --help' output: '%s' "\
+ % (token, output))
+ self.failUnless(retval == 0,
+ "'which --help' did not return 0: retval=%d" % retval)
+
+ def test_opt_version(self):
+ output, error, retval = testsupport.run(self.which+' --version')
+ versionRe = re.compile("^which \d+\.\d+\.\d+$")
+ versionMatch = versionRe.search(output.strip())
+ self.failUnless(versionMatch,
+ "Version, '%s', from 'which --version' does not "\
+ "match pattern, '%s'."\
+ % (output.strip(), versionRe.pattern))
+ self.failUnless(retval == 0,
+ "'which --version' did not return 0: retval=%d"\
+ % retval)
+
+ def test_no_args(self):
+ output, error, retval = testsupport.run(self.which)
+ self.failUnless(retval == -1,
+ "'which' with no args should return -1: retval=%d"\
+ % retval)
+
+ def test_one_failure(self):
+ output, error, retval = testsupport.run(
+ self.which+' whichtestapp1')
+ self.failUnless(retval == 1,
+ "One failure did not return 1: retval=%d" % retval)
+
+ def test_two_failures(self):
+ output, error, retval = testsupport.run(
+ self.which+' whichtestapp1 whichtestapp2')
+ self.failUnless(retval == 2,
+ "Two failures did not return 2: retval=%d" % retval)
+
+ def _match(self, path1, path2):
+ #print "_match: %r =?= %r" % (path1, path2)
+ if sys.platform.startswith('win'):
+ path1 = os.path.normpath(os.path.normcase(path1))
+ path2 = os.path.normpath(os.path.normcase(path2))
+ path1 = os.path.splitext(path1)[0]
+ path2 = os.path.splitext(path2)[0]
+ return path1 == path2
+ else:
+ return os.path.samefile(path1, path2)
+
+ def test_one_success(self):
+ os.environ["PATH"] += os.pathsep + self.tmpdir
+ output, error, retval = testsupport.run(self.which+' -q whichtestapp1')
+ expectedOutput = os.path.join(self.tmpdir, "whichtestapp1")
+ self.failUnless(self._match(output.strip(), expectedOutput),
+ "Output, %r, and expected output, %r, do not match."\
+ % (output.strip(), expectedOutput))
+ self.failUnless(retval == 0,
+ "'which ...' should have returned 0: retval=%d" % retval)
+
+ def test_two_successes(self):
+ os.environ["PATH"] += os.pathsep + self.tmpdir
+ apps = ['whichtestapp1', 'whichtestapp2']
+ output, error, retval = testsupport.run(
+ self.which + ' -q ' + ' '.join(apps))
+ lines = output.strip().split("\n")
+ for app, line in zip(apps, lines):
+ expected = os.path.join(self.tmpdir, app)
+ self.failUnless(self._match(line, expected),
+ "Output, %r, and expected output, %r, do not match."\
+ % (line, expected))
+ self.failUnless(retval == 0,
+ "'which ...' should have returned 0: retval=%d" % retval)
+
+ if sys.platform.startswith("win"):
+ def test_PATHEXT_failure(self):
+ os.environ["PATH"] += os.pathsep + self.tmpdir
+ output, error, retval = testsupport.run(self.which+' whichtestapp3')
+ self.failUnless(retval == 1,
+ "'which ...' should have returned 1: retval=%d" % retval)
+
+ def test_PATHEXT_success(self):
+ os.environ["PATH"] += os.pathsep + self.tmpdir
+ os.environ["PATHEXT"] += os.pathsep + '.wta'
+ output, error, retval = testsupport.run(self.which+' whichtestapp3')
+ expectedOutput = os.path.join(self.tmpdir, "whichtestapp3")
+ self.failUnless(self._match(output.strip(), expectedOutput),
+ "Output, %r, and expected output, %r, do not match."\
+ % (output.strip(), expectedOutput))
+ self.failUnless(retval == 0,
+ "'which ...' should have returned 0: retval=%d" % retval)
+
+ def test_exts(self):
+ os.environ["PATH"] += os.pathsep + self.tmpdir
+ output, error, retval = testsupport.run(self.which+' -e .wta whichtestapp3')
+ expectedOutput = os.path.join(self.tmpdir, "whichtestapp3")
+ self.failUnless(self._match(output.strip(), expectedOutput),
+ "Output, %r, and expected output, %r, do not match."\
+ % (output.strip(), expectedOutput))
+ self.failUnless(retval == 0,
+ "'which ...' should have returned 0: retval=%d" % retval)
+
+
+
+def suite():
+ """Return a unittest.TestSuite to be used by test.py."""
+ return unittest.makeSuite(WhichTestCase)
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/python/which/test/testsupport.py b/python/which/test/testsupport.py
new file mode 100644
index 000000000..8ff519fad
--- /dev/null
+++ b/python/which/test/testsupport.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+# Copyright (c) 2002-2003 ActiveState Corp.
+# Author: Trent Mick (TrentM@ActiveState.com)
+
+import os
+import sys
+import types
+
+
+#---- Support routines
+
+def _escapeArg(arg):
+ """Escape the given command line argument for the shell."""
+ #XXX There is a *lot* more that we should escape here.
+ return arg.replace('"', r'\"')
+
+
+def _joinArgv(argv):
+ r"""Join an arglist to a string appropriate for running.
+ >>> import os
+ >>> _joinArgv(['foo', 'bar "baz'])
+ 'foo "bar \\"baz"'
+ """
+ cmdstr = ""
+ for arg in argv:
+ if ' ' in arg:
+ cmdstr += '"%s"' % _escapeArg(arg)
+ else:
+ cmdstr += _escapeArg(arg)
+ cmdstr += ' '
+ if cmdstr.endswith(' '): cmdstr = cmdstr[:-1] # strip trailing space
+ return cmdstr
+
+
+def run(argv):
+ """Prepare and run the given arg vector, 'argv', and return the
+ results. Returns (<stdout lines>, <stderr lines>, <return value>).
+ Note: 'argv' may also just be the command string.
+ """
+ if type(argv) in (types.ListType, types.TupleType):
+ cmd = _joinArgv(argv)
+ else:
+ cmd = argv
+ if sys.platform.startswith('win'):
+ i, o, e = os.popen3(cmd)
+ output = o.read()
+ error = e.read()
+ i.close()
+ e.close()
+ try:
+ retval = o.close()
+ except IOError:
+ # IOError is raised iff the spawned app returns -1. Go
+ # figure.
+ retval = -1
+ if retval is None:
+ retval = 0
+ else:
+ import popen2
+ p = popen2.Popen3(cmd, 1)
+ i, o, e = p.tochild, p.fromchild, p.childerr
+ output = o.read()
+ error = e.read()
+ i.close()
+ o.close()
+ e.close()
+ retval = (p.wait() & 0xFF00) >> 8
+ if retval > 2**7: # 8-bit signed 1's-complement conversion
+ retval -= 2**8
+ return output, error, retval
+
+
+def _rmtreeOnError(rmFunction, filePath, excInfo):
+ if excInfo[0] == OSError:
+ # presuming because file is read-only
+ os.chmod(filePath, 0777)
+ rmFunction(filePath)
+
+def rmtree(dirname):
+ import shutil
+ shutil.rmtree(dirname, 0, _rmtreeOnError)
+
+
diff --git a/python/which/which.py b/python/which/which.py
new file mode 100644
index 000000000..8dd790518
--- /dev/null
+++ b/python/which/which.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python
+# Copyright (c) 2002-2005 ActiveState Corp.
+# See LICENSE.txt for license details.
+# Author:
+# Trent Mick (TrentM@ActiveState.com)
+# Home:
+# http://trentm.com/projects/which/
+
+r"""Find the full path to commands.
+
+which(command, path=None, verbose=0, exts=None)
+ Return the full path to the first match of the given command on the
+ path.
+
+whichall(command, path=None, verbose=0, exts=None)
+ Return a list of full paths to all matches of the given command on
+ the path.
+
+whichgen(command, path=None, verbose=0, exts=None)
+ Return a generator which will yield full paths to all matches of the
+ given command on the path.
+
+By default the PATH environment variable is searched (as well as, on
+Windows, the AppPaths key in the registry), but a specific 'path' list
+to search may be specified as well. On Windows, the PATHEXT environment
+variable is applied as appropriate.
+
+If "verbose" is true then a tuple of the form
+ (<fullpath>, <matched-where-description>)
+is returned for each match. The latter element is a textual description
+of where the match was found. For example:
+ from PATH element 0
+ from HKLM\SOFTWARE\...\perl.exe
+"""
+
+_cmdlnUsage = """
+ Show the full path of commands.
+
+ Usage:
+ which [<options>...] [<command-name>...]
+
+ Options:
+ -h, --help Print this help and exit.
+ -V, --version Print the version info and exit.
+
+ -a, --all Print *all* matching paths.
+ -v, --verbose Print out how matches were located and
+ show near misses on stderr.
+ -q, --quiet Just print out matches. I.e., do not print out
+ near misses.
+
+ -p <altpath>, --path=<altpath>
+ An alternative path (list of directories) may
+ be specified for searching.
+ -e <exts>, --exts=<exts>
+ Specify a list of extensions to consider instead
+ of the usual list (';'-separate list, Windows
+ only).
+
+ Show the full path to the program that would be run for each given
+ command name, if any. Which, like GNU's which, returns the number of
+ failed arguments, or -1 when no <command-name> was given.
+
+ Near misses include duplicates, non-regular files and (on Un*x)
+ files without executable access.
+"""
+
+__revision__ = "$Id: which.py 430 2005-08-20 03:11:58Z trentm $"
+__version_info__ = (1, 1, 0)
+__version__ = '.'.join(map(str, __version_info__))
+
+import os
+import sys
+import getopt
+import stat
+
+
+#---- exceptions
+
+class WhichError(Exception):
+ pass
+
+
+
+#---- internal support stuff
+
+def _getRegisteredExecutable(exeName):
+ """Windows allow application paths to be registered in the registry."""
+ registered = None
+ if sys.platform.startswith('win'):
+ if os.path.splitext(exeName)[1].lower() != '.exe':
+ exeName += '.exe'
+ import _winreg
+ try:
+ key = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" +\
+ exeName
+ value = _winreg.QueryValue(_winreg.HKEY_LOCAL_MACHINE, key)
+ registered = (value, "from HKLM\\"+key)
+ except _winreg.error:
+ pass
+ if registered and not os.path.exists(registered[0]):
+ registered = None
+ return registered
+
+def _samefile(fname1, fname2):
+ if sys.platform.startswith('win'):
+ return ( os.path.normpath(os.path.normcase(fname1)) ==\
+ os.path.normpath(os.path.normcase(fname2)) )
+ else:
+ return os.path.samefile(fname1, fname2)
+
+def _cull(potential, matches, verbose=0):
+ """Cull inappropriate matches. Possible reasons:
+ - a duplicate of a previous match
+ - not a disk file
+ - not executable (non-Windows)
+ If 'potential' is approved it is returned and added to 'matches'.
+ Otherwise, None is returned.
+ """
+ for match in matches: # don't yield duplicates
+ if _samefile(potential[0], match[0]):
+ if verbose:
+ sys.stderr.write("duplicate: %s (%s)\n" % potential)
+ return None
+ else:
+ if not stat.S_ISREG(os.stat(potential[0]).st_mode):
+ if verbose:
+ sys.stderr.write("not a regular file: %s (%s)\n" % potential)
+ elif not os.access(potential[0], os.X_OK):
+ if verbose:
+ sys.stderr.write("no executable access: %s (%s)\n"\
+ % potential)
+ else:
+ matches.append(potential)
+ return potential
+
+
+#---- module API
+
+def whichgen(command, path=None, verbose=0, exts=None):
+ """Return a generator of full paths to the given command.
+
+ "command" is a the name of the executable to search for.
+ "path" is an optional alternate path list to search. The default it
+ to use the PATH environment variable.
+ "verbose", if true, will cause a 2-tuple to be returned for each
+ match. The second element is a textual description of where the
+ match was found.
+ "exts" optionally allows one to specify a list of extensions to use
+ instead of the standard list for this system. This can
+ effectively be used as an optimization to, for example, avoid
+ stat's of "foo.vbs" when searching for "foo" and you know it is
+ not a VisualBasic script but ".vbs" is on PATHEXT. This option
+ is only supported on Windows.
+
+ This method returns a generator which yields either full paths to
+ the given command or, if verbose, tuples of the form (<path to
+ command>, <where path found>).
+ """
+ matches = []
+ if path is None:
+ usingGivenPath = 0
+ path = os.environ.get("PATH", "").split(os.pathsep)
+ if sys.platform.startswith("win"):
+ path.insert(0, os.curdir) # implied by Windows shell
+ else:
+ usingGivenPath = 1
+
+ # Windows has the concept of a list of extensions (PATHEXT env var).
+ if sys.platform.startswith("win"):
+ if exts is None:
+ exts = os.environ.get("PATHEXT", "").split(os.pathsep)
+ # If '.exe' is not in exts then obviously this is Win9x and
+ # or a bogus PATHEXT, then use a reasonable default.
+ for ext in exts:
+ if ext.lower() == ".exe":
+ break
+ else:
+ exts = ['.COM', '.EXE', '.BAT']
+ elif not isinstance(exts, list):
+ raise TypeError("'exts' argument must be a list or None")
+ else:
+ if exts is not None:
+ raise WhichError("'exts' argument is not supported on "\
+ "platform '%s'" % sys.platform)
+ exts = []
+
+ # File name cannot have path separators because PATH lookup does not
+ # work that way.
+ if os.sep in command or os.altsep and os.altsep in command:
+ pass
+ else:
+ for i in range(len(path)):
+ dirName = path[i]
+ # On windows the dirName *could* be quoted, drop the quotes
+ if sys.platform.startswith("win") and len(dirName) >= 2\
+ and dirName[0] == '"' and dirName[-1] == '"':
+ dirName = dirName[1:-1]
+ for ext in ['']+exts:
+ absName = os.path.abspath(
+ os.path.normpath(os.path.join(dirName, command+ext)))
+ if os.path.isfile(absName):
+ if usingGivenPath:
+ fromWhere = "from given path element %d" % i
+ elif not sys.platform.startswith("win"):
+ fromWhere = "from PATH element %d" % i
+ elif i == 0:
+ fromWhere = "from current directory"
+ else:
+ fromWhere = "from PATH element %d" % (i-1)
+ match = _cull((absName, fromWhere), matches, verbose)
+ if match:
+ if verbose:
+ yield match
+ else:
+ yield match[0]
+ match = _getRegisteredExecutable(command)
+ if match is not None:
+ match = _cull(match, matches, verbose)
+ if match:
+ if verbose:
+ yield match
+ else:
+ yield match[0]
+
+
+def which(command, path=None, verbose=0, exts=None):
+ """Return the full path to the first match of the given command on
+ the path.
+
+ "command" is a the name of the executable to search for.
+ "path" is an optional alternate path list to search. The default it
+ to use the PATH environment variable.
+ "verbose", if true, will cause a 2-tuple to be returned. The second
+ element is a textual description of where the match was found.
+ "exts" optionally allows one to specify a list of extensions to use
+ instead of the standard list for this system. This can
+ effectively be used as an optimization to, for example, avoid
+ stat's of "foo.vbs" when searching for "foo" and you know it is
+ not a VisualBasic script but ".vbs" is on PATHEXT. This option
+ is only supported on Windows.
+
+ If no match is found for the command, a WhichError is raised.
+ """
+ try:
+ match = whichgen(command, path, verbose, exts).next()
+ except StopIteration:
+ raise WhichError("Could not find '%s' on the path." % command)
+ return match
+
+
+def whichall(command, path=None, verbose=0, exts=None):
+ """Return a list of full paths to all matches of the given command
+ on the path.
+
+ "command" is a the name of the executable to search for.
+ "path" is an optional alternate path list to search. The default it
+ to use the PATH environment variable.
+ "verbose", if true, will cause a 2-tuple to be returned for each
+ match. The second element is a textual description of where the
+ match was found.
+ "exts" optionally allows one to specify a list of extensions to use
+ instead of the standard list for this system. This can
+ effectively be used as an optimization to, for example, avoid
+ stat's of "foo.vbs" when searching for "foo" and you know it is
+ not a VisualBasic script but ".vbs" is on PATHEXT. This option
+ is only supported on Windows.
+ """
+ return list( whichgen(command, path, verbose, exts) )
+
+
+
+#---- mainline
+
+def main(argv):
+ all = 0
+ verbose = 0
+ altpath = None
+ exts = None
+ try:
+ optlist, args = getopt.getopt(argv[1:], 'haVvqp:e:',
+ ['help', 'all', 'version', 'verbose', 'quiet', 'path=', 'exts='])
+ except getopt.GetoptError, msg:
+ sys.stderr.write("which: error: %s. Your invocation was: %s\n"\
+ % (msg, argv))
+ sys.stderr.write("Try 'which --help'.\n")
+ return 1
+ for opt, optarg in optlist:
+ if opt in ('-h', '--help'):
+ print _cmdlnUsage
+ return 0
+ elif opt in ('-V', '--version'):
+ print "which %s" % __version__
+ return 0
+ elif opt in ('-a', '--all'):
+ all = 1
+ elif opt in ('-v', '--verbose'):
+ verbose = 1
+ elif opt in ('-q', '--quiet'):
+ verbose = 0
+ elif opt in ('-p', '--path'):
+ if optarg:
+ altpath = optarg.split(os.pathsep)
+ else:
+ altpath = []
+ elif opt in ('-e', '--exts'):
+ if optarg:
+ exts = optarg.split(os.pathsep)
+ else:
+ exts = []
+
+ if len(args) == 0:
+ return -1
+
+ failures = 0
+ for arg in args:
+ #print "debug: search for %r" % arg
+ nmatches = 0
+ for match in whichgen(arg, path=altpath, verbose=verbose, exts=exts):
+ if verbose:
+ print "%s (%s)" % match
+ else:
+ print match
+ nmatches += 1
+ if not all:
+ break
+ if not nmatches:
+ failures += 1
+ return failures
+
+
+if __name__ == "__main__":
+ sys.exit( main(sys.argv) )
+
+