From 9cfe89272f95e17277fa2da14026a6be1e985b94 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 28 May 2016 13:19:52 -0500 Subject: [PATCH] Update which() to use os.environ['PATH'] Update to saltstack's latest `salt.util.which`. See also: https://redd.it/4laeut --- libtmux/common.py | 77 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/libtmux/common.py b/libtmux/common.py index 3c435a02e..8243803d5 100644 --- a/libtmux/common.py +++ b/libtmux/common.py @@ -11,6 +11,7 @@ import re import subprocess from distutils.version import StrictVersion +from functools import wraps from . import exc from ._compat import console_to_str @@ -312,6 +313,21 @@ def get_by_id(self, id): return None +def real_memoize(func): + ''' + Memoize aka cache the return output of a function + given a specific set of arguments + ''' + cache = {} + + @wraps(func) + def _memoize(*args): + if args not in cache: + cache[args] = func(*args) + return cache[args] + return _memoize + + def which(exe=None): """Return path of bin. Python clone of /usr/bin/which. @@ -322,24 +338,51 @@ def which(exe=None): :rtype: string """ - if exe: - if os.access(exe, os.X_OK): - return exe - - # default path based on busybox's default - search_path = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin' + def _is_executable_file_or_link(exe): + # check for os.X_OK doesn't suffice because directory may executable + return (os.access(exe, os.X_OK) and + (os.path.isfile(exe) or os.path.islink(exe))) + + if _is_executable_file_or_link(exe): + # executable in cwd or fullpath + return exe + + ext_list = os.environ.get('PATHEXT', '.EXE').split(';') + + @real_memoize + def _exe_has_ext(): + ''' + Do a case insensitive test if exe has a file extension match in + PATHEXT + ''' + for ext in ext_list: + try: + pattern = r'.*\.' + ext.lstrip('.') + r'$' + re.match(pattern, exe, re.I).groups() + return True + except AttributeError: + continue + return False + + # Enhance POSIX path for the reliability at some environments, when + # $PATH is changing. This also keeps order, where 'first came, first + # win' for cases to find optional alternatives + search_path = os.environ.get('PATH') and \ + os.environ['PATH'].split(os.pathsep) or list() + for default_path in [ + '/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/local/bin' + ]: + if default_path not in search_path: + search_path.append(default_path) + os.environ['PATH'] = os.pathsep.join(search_path) + for path in search_path: + full_path = os.path.join(path, exe) + if _is_executable_file_or_link(full_path): + return full_path + logger.trace( + '\'{0}\' could not be found in the following search path: ' + '\'{1}\''.format(exe, search_path)) - for path in search_path.split(os.pathsep): - full_path = os.path.join(path, exe) - if os.access(full_path, os.X_OK): - return full_path - raise exc.LibTmuxException( - '{0!r} could not be found in the following search ' - 'path: {1!r}'.format( - exe, search_path - ) - ) - logger.error('No executable was passed to be searched by which') return None