1111import re
1212import subprocess
1313from distutils .version import StrictVersion
14+ from functools import wraps
1415
1516from . import exc
1617from ._compat import console_to_str
@@ -312,6 +313,21 @@ def get_by_id(self, id):
312313 return None
313314
314315
316+ def real_memoize (func ):
317+ '''
318+ Memoize aka cache the return output of a function
319+ given a specific set of arguments
320+ '''
321+ cache = {}
322+
323+ @wraps (func )
324+ def _memoize (* args ):
325+ if args not in cache :
326+ cache [args ] = func (* args )
327+ return cache [args ]
328+ return _memoize
329+
330+
315331def which (exe = None ):
316332 """Return path of bin. Python clone of /usr/bin/which.
317333
@@ -322,24 +338,51 @@ def which(exe=None):
322338 :rtype: string
323339
324340 """
325- if exe :
326- if os .access (exe , os .X_OK ):
327- return exe
328-
329- # default path based on busybox's default
330- search_path = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin'
341+ def _is_executable_file_or_link (exe ):
342+ # check for os.X_OK doesn't suffice because directory may executable
343+ return (os .access (exe , os .X_OK ) and
344+ (os .path .isfile (exe ) or os .path .islink (exe )))
345+
346+ if _is_executable_file_or_link (exe ):
347+ # executable in cwd or fullpath
348+ return exe
349+
350+ ext_list = os .environ .get ('PATHEXT' , '.EXE' ).split (';' )
351+
352+ @real_memoize
353+ def _exe_has_ext ():
354+ '''
355+ Do a case insensitive test if exe has a file extension match in
356+ PATHEXT
357+ '''
358+ for ext in ext_list :
359+ try :
360+ pattern = r'.*\.' + ext .lstrip ('.' ) + r'$'
361+ re .match (pattern , exe , re .I ).groups ()
362+ return True
363+ except AttributeError :
364+ continue
365+ return False
366+
367+ # Enhance POSIX path for the reliability at some environments, when
368+ # $PATH is changing. This also keeps order, where 'first came, first
369+ # win' for cases to find optional alternatives
370+ search_path = os .environ .get ('PATH' ) and \
371+ os .environ ['PATH' ].split (os .pathsep ) or list ()
372+ for default_path in [
373+ '/bin' , '/sbin' , '/usr/bin' , '/usr/sbin' , '/usr/local/bin'
374+ ]:
375+ if default_path not in search_path :
376+ search_path .append (default_path )
377+ os .environ ['PATH' ] = os .pathsep .join (search_path )
378+ for path in search_path :
379+ full_path = os .path .join (path , exe )
380+ if _is_executable_file_or_link (full_path ):
381+ return full_path
382+ logger .trace (
383+ '\' {0}\' could not be found in the following search path: '
384+ '\' {1}\' ' .format (exe , search_path ))
331385
332- for path in search_path .split (os .pathsep ):
333- full_path = os .path .join (path , exe )
334- if os .access (full_path , os .X_OK ):
335- return full_path
336- raise exc .LibTmuxException (
337- '{0!r} could not be found in the following search '
338- 'path: {1!r}' .format (
339- exe , search_path
340- )
341- )
342- logger .error ('No executable was passed to be searched by which' )
343386 return None
344387
345388
0 commit comments