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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | #!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)
|
DESCRIPTION
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.
USAGE
Write the full path of COMMAND(s) to standard output.
Usage: WHICH [-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.
WHICH 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.
EASTER EGGS
I provide two shortcuts to restrict the search:
- 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. - I also allow a shortcut to specify exactMatchOnly by appending a dot.
For example,
"foo.exe."
looks for"foo.exe"
and nothing else.
DISCUSSION
I have often wanted a Windows-specific version of the Unix which
command.
GNU
-derived versions of which
are useful, but incomplete. The rules
for command lookup under Windows are sometimes bizarre. This is what I have
determined through experimentation:
If the command name exactly matches a built-in command, the built-in takes precedence.
Otherwise
CMD.EXE
will search for the command starting in the current directory, then in each directory specified by the%PATH%
environment table. At each directory in this search:- If the command name has an extension:
- If the command extension matches an extension from the
%PATHEXT%
environment variable, thenCMD.EXE
will attempt to execute the file. - If the command name extension is associated with a particular program,
then that program is invoked, with this file as input.
TODO:
My program does not handle this situation.
- Don't give up yet. This is Windows.
- If the command extension matches an extension from the
- Finally, each extension from the
%PATHEXT%
environment variable will be appended in turn to the supplied command name. If the constructed command matches one of the above rules, we have a winner!
- If the command name has an extension:
Consider this scenario:
- There is a program named
verify.exe
, somewhere on your path. - Unknown to you, there is also a program named
verify.exe.py
, at another location on your path. - At the command prompt, you type
verify
. Much to your surprise,CMD.EXE
executes the built-in programVERIFY
, which is not what you want. - You try again:
verify.exe
. Again to your surprise,CMD.EXE
finds and executesverify.exe.py
, which is also not what you want.
Granted that's not very likely, but it could happen. This is why WHICH.PY
defaults to listing every possible match it finds in the system path.
ALTERNATIVES
The GnuWin32
version of which.exe
is useful, but it does not know
about CMD.EXE's
built-in commands. It also does not know all of the
rules for command lookup
WHERE.EXE
, available in Windows since Windows Server 2003, does not know
about built-in commands.
WHERE.EXE
also provides too much information, because it does not
distinguish between files that are executable and those that are not.
TODO
- Account for file associations.
- Use
Py2Exe
to build an executable binary. - Create a GitHub repository for this project.
REQUIRED MODULES
sys os os.path argparse traceback
TAGS
Python Windows commandline utility which
LINKS
This program has been posted at several locations online:
Github
: https://github.com/BobPyron/commandline-utilitiesActiveState
: http://code.activestate.com/recipes/578642-which-for-windows/
LICENSE AND COPYRIGHT
Copyright © 2013 by Robert L. Pyron (mailto:rpyron+which@gmail.com)
Distributed under terms of the MIT License: http://opensource.org/licenses/MIT