#!Python27.exe
#-*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# WHICH.PY
#
# WHICH.PY scans through all directories specified in the system %PATH%
# environment variable, looking for the specified COMMAND(s). It tries
# to follow the sometimes bizarre rules for Windows command lookup.
# Copyright (c) 2013 by Robert L. Pyron <rpyron+which@gmail.com>
# Distributed under terms of the MIT License: http://opensource.org/licenses/MIT
#
# TODO:
# - Figure out how to automatically update version info (probably related
# to version control)
# - Add option for long listing format: date, time, attributes
# (system or hidden), SYMLINK info if appropriate, and file size
# - Build executable version.
#
# HISTORY:
# Version 0.1
# - This is the first version that I am willing to make publically
# available.
# - Submitted to ActiveState Recipes, 15 August 2013.
#
#-------------------------------------------------------------------------------
"""Write the full path of COMMAND(s) to standard output.
Usage: %(PROG)s [-e] [-f] [-v] [-h] COMMAND [...]
-e, --exact Print exact matches only.
-f, --first Print just the first match.
-v, --version Print version and exit successfully.
-h, --help Print help message and exit successfully.
%(PROG)s scans through all directories specified in the system %%PATH%%
environment variable, looking for the specified COMMAND(s). It tries
to follow the sometimes bizarre rules for Windows command lookup.
Copyright (c) 2013 by Robert L. Pyron <rpyron@alum.mit.edu>
"""
#-------------------------------------------------------------------------------
import sys, os, os.path, argparse, traceback
#
# I'm on Windows, so I can arbitrarily force the script name to upper case.
PROG = os.path.basename(sys.argv[0]).upper()
#
# Expand module docstring to get USAGE string
USAGE = __doc__ % {'PROG' : PROG}
#
# Get PATH environment variable, and split it.
PATH = os.environ.get('PATH').split(';')
if True:
# Clean up the PATH info: A directory may appear in PATH more than
# once, but there is no need to scan it twice. Also, since this program
# is intended to run on Windows, we will always start in the current
# directory. I'm ignoring changes in pathname case, because I'm lazy
# and it probably doesn't matter very much.
tmpDirs = ['.']
for dir in PATH:
if dir not in tmpDirs:
tmpDirs += [dir]
PATH,tmpDirs = tmpDirs,None
#
# Get PATHEXT environment variable, and split it.
# I'm on Windows, so I can arbitrarily force the extensions to lower case.
PATHEXT = os.environ.get('PATHEXT').lower().split(';')
#
# TODO - fix this
VERSION = '$Id$'
VERSION = '0.1'
#
# These commands are built-in to CMD.EXE under Windows 8.
# Earlier and later versions of Windows may have more (or fewer) built-ins.
# Also, other command-line shells may have a different list.
BUILTINS = ["ASSOC", "BREAK", "BCDEDIT", "CALL", "CD", "CHDIR", "CLS", "COLOR",
"COPY", "DATE", "DEL", "DIR", "ECHO", "ENDLOCAL", "ERASE", "EXIT",
"FOR", "FTYPE", "GOTO", "GRAFTABL", "IF", "MD", "MKDIR", "MKLINK",
"MOVE", "PATH", "PAUSE", "POPD", "PROMPT", "PUSHD", "RD", "REM",
"REN", "RENAME", "RMDIR", "SET", "SETLOCAL", "SHIFT", "START",
"TIME", "TITLE", "TYPE", "VER", "VERIFY", "VOL" ]
#
# Global command-line arguments, to be filled in by parse_args()
args = argparse.Namespace()
#-------------------------------------------------------------------------------
#
# Utility function: split a filename into path,root,extension.
def split_name(filename):
dirname,basename = os.path.split(filename)
root,ext = os.path.splitext(basename)
return dirname,root,ext
#
# Find matches to specified filename in system %PATH%.
def which(COMMAND):
# Define a convenience exception for early exit from this routine.
# This is used as a Pythonic equivalent to GOTO.
class FoundFirstMatch(Exception):
pass
try:
print( COMMAND )
# Check whether this is a built-in command.
if COMMAND.upper() in BUILTINS:
print( ' (builtin under CMD.EXE)' )
# The obvious thing to do at this point would be to return,
# because we now know everything we need to know, right?
# Think again, buddy. This is Windows we are dealing with.
if args.firstMatchOnly:
raise FoundFirstMatch()
# Split COMMAND into parts.
specified_directory,specified_command,specified_ext = split_name(COMMAND)
# In general, we don't want to have a pathname specified as part of
# the COMMAND. However, I allow the syntax '.\COMMAND' to restrict
# search to current directory.
local_currentDirectoryOnly = False
if specified_directory == '.':
# Search only in current directory.
local_currentDirectoryOnly = True
# Reconstruct COMMAND without path.
COMMAND = specified_command + specified_ext
elif specified_directory:
# Do not include path as part of input name.
# If you know where it is, why are you calling this program?
print( ' (please do not specify a path as part of COMMAND)' )
return
# I also allow a shortcut to specify exactMatchOnly by appending a dot.
# For example, "foo.exe." looks for "foo.exe" and nothing else.
local_exactMatchOnly = args.exactMatchOnly
if specified_ext == '.':
local_exactMatchOnly = True
# Reconstruct COMMAND without extension.
COMMAND, specified_ext = specified_command, ''
# Scan through all directories in %PATH%.
count = 0
for dir in PATH:
# Expand environment variables in PATH component
dir = os.path.expandvars(dir)
# First, look for an exact match in this directory.
if specified_ext or local_exactMatchOnly:
target = os.path.join(dir,COMMAND)
if os.path.isfile(target):
print( ' ' + target )
count += 1
if args.firstMatchOnly:
raise FoundFirstMatch()
else:
# Move along to next directory in PATH.
continue
# Now, try all extensions listed in PATHEXT.
if not local_exactMatchOnly:
for ext in PATHEXT:
target = os.path.join(dir,specified_command+ext)
if os.path.isfile(target):
print( ' ' + target )
count += 1
if args.firstMatchOnly:
raise FoundFirstMatch()
else:
# Move along to next extension in PATHEXT.
continue
if count == 0:
print( ' (not found)' )
except FoundFirstMatch:
pass
finally:
print
return
#-------------------------------------------------------------------------------
#
# Parse command-line arguments
def parse_args():
"""Parse command-line arguments; fill in global variable 'args'."""
global args
parser = argparse.ArgumentParser()
parser.add_argument('-e', '--exact', help='Print exact matches only.', action='store_true', dest='exactMatchOnly' )
parser.add_argument('-f', '--first', help='Print just the first match.', action='store_true', dest='firstMatchOnly' )
parser.add_argument('-v', '--version', help='Print version and exit successfully.', action='version', version=VERSION)
parser.add_argument('COMMANDS', nargs=argparse.REMAINDER)
args = parser.parse_args()
return args
#
# Command-line arguments have already been parsed.
def main (args):
if not args.COMMANDS:
print USAGE
return
for COMMAND in args.COMMANDS:
which(COMMAND)
if __name__ == '__main__':
try:
args = parse_args()
main(args)
sys.exit(0)
except KeyboardInterrupt, e: # Ctrl-C
raise e
except SystemExit, e: # sys.exit()
raise e
except Exception, e:
print( 'ERROR, UNEXPECTED EXCEPTION' )
print( str(e) )
traceback.print_exc()
os._exit(1)
Diff to Previous Revision
--- revision 1 2013-08-15 07:05:33
+++ revision 2 2013-08-16 09:14:07
@@ -195,7 +195,6 @@
def parse_args():
"""Parse command-line arguments; fill in global variable 'args'."""
global args
-# parser = argparse.ArgumentParser(usage=USAGE)
parser = argparse.ArgumentParser()
parser.add_argument('-e', '--exact', help='Print exact matches only.', action='store_true', dest='exactMatchOnly' )
parser.add_argument('-f', '--first', help='Print just the first match.', action='store_true', dest='firstMatchOnly' )