diff options
Diffstat (limited to 'python/which')
-rw-r--r-- | python/which/LICENSE.txt | 21 | ||||
-rw-r--r-- | python/which/MANIFEST.in | 3 | ||||
-rw-r--r-- | python/which/Makefile.win | 21 | ||||
-rw-r--r-- | python/which/PKG-INFO | 21 | ||||
-rw-r--r-- | python/which/README.txt | 229 | ||||
-rw-r--r-- | python/which/TODO.txt | 113 | ||||
-rw-r--r-- | python/which/build.py | 442 | ||||
-rw-r--r-- | python/which/launcher.cpp | 404 | ||||
-rw-r--r-- | python/which/logo.jpg | bin | 0 -> 3635 bytes | |||
-rw-r--r-- | python/which/setup.py | 70 | ||||
-rw-r--r-- | python/which/test/test_which.py | 168 | ||||
-rw-r--r-- | python/which/test/testsupport.py | 83 | ||||
-rw-r--r-- | python/which/which.py | 335 |
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 Binary files differnew file mode 100644 index 000000000..300c23f14 --- /dev/null +++ b/python/which/logo.jpg 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) ) + + |