diff --git a/xonsh/environ.py b/xonsh/environ.py index 8f66b2752..0fd12485f 100644 --- a/xonsh/environ.py +++ b/xonsh/environ.py @@ -816,17 +816,19 @@ def _yield_executables(directory, name): def locate_binary(name): - if os.path.isfile(name) and name != os.path.basename(name): - return name - directories = builtins.__xonsh_env__.get('PATH') - # Windows users expect to be able to execute files in the same directory - # without `./` - if ON_WINDOWS: - directories = [_get_cwd()] + list(directories) - try: - return next(chain.from_iterable(_yield_executables(directory, name) for - directory in directories if os.path.isdir(directory))) - except StopIteration: + """Locates an executable on the file system.""" + if os.path.isfile(name): + if ON_WINDOWS: + # Windows users expect to be able to execute files in the same + # directory without `./` + return os.path.abspath(os.path.relpath(name, _get_cwd())) + elif name != os.path.basename(name): + return name + cc = builtins.__xonsh_commands_cache__ + if name in cc: + # can be lazy here since we know name is already available + return cc.lazyget(name)[0] + else: return None diff --git a/xonsh/tools.py b/xonsh/tools.py index 9509e5b63..77f4870fd 100644 --- a/xonsh/tools.py +++ b/xonsh/tools.py @@ -1476,17 +1476,22 @@ def expanduser_abs_path(inp): return os.path.abspath(os.path.expanduser(inp)) -class CommandsCache(abc.Set): - """A lazy cache representing the commands available on the file system.""" +class CommandsCache(abc.Mapping): + """A lazy cache representing the commands available on the file system. + The keys are the command names and the values a tuple of (loc, has_alias) + where loc is either a str pointing to the executable on the file system or + None (if no executable exists) and has_alias is a boolean flag for whether + the command has an alias. + """ def __init__(self): - self._cmds_cache = frozenset() + self._cmds_cache = {} self._path_checksum = None self._alias_checksum = None self._path_mtime = -1 - def __contains__(self, item): - return item in self.all_commands + def __contains__(self, key): + return key in self.all_commands def __iter__(self): return iter(self.all_commands) @@ -1494,21 +1499,25 @@ class CommandsCache(abc.Set): def __len__(self): return len(self.all_commands) + def __getitem__(self, key): + return self.all_commands[key] + @property def all_commands(self): paths = builtins.__xonsh_env__.get('PATH', []) - paths = frozenset(x for x in paths if os.path.isdir(x)) + pathset = frozenset(x for x in paths if os.path.isdir(x)) # did PATH change? - path_hash = hash(paths) + path_hash = hash(pathset) cache_valid = path_hash == self._path_checksum self._path_checksum = path_hash # did aliases change? - al_hash = hash(frozenset(builtins.aliases)) + alss = builtins.aliases + al_hash = hash(frozenset(alss)) cache_valid = cache_valid and al_hash == self._alias_checksum self._alias_checksum = al_hash # did the contents of any directory in PATH change? max_mtime = 0 - for path in paths: + for path in pathset: mtime = os.stat(path).st_mtime if mtime > max_mtime: max_mtime = mtime @@ -1516,12 +1525,18 @@ class CommandsCache(abc.Set): self._path_mtime = max_mtime if cache_valid: return self._cmds_cache - allcmds = set() - for path in paths: - allcmds |= set(executables_in(path)) - allcmds |= set(builtins.aliases) - self._cmds_cache = frozenset(allcmds) - return self._cmds_cache + allcmds = {} + for path in reversed(paths): + # iterate backwards so that entries at the front of PATH overwrite + # entries at the back. + for cmd in executables_in(path): + allcmds[cmd] = (os.path.join(path, cmd), cmd in alss) + only_alias = (None, True) + for cmd in alss: + if cmd not in allcmds: + allcmds[cmd] = only_alias + self._cmds_cache = allcmds + return allcmds def lazyin(self, value): """Checks if the value is in the current cache without the potential to @@ -1544,6 +1559,10 @@ class CommandsCache(abc.Set): """ return len(self._cmds_cache) + def lazyget(self, key, default=None): + """A lazy value getter.""" + return self._cmd_cache.get(key, default) + WINDOWS_DRIVE_MATCHER = LazyObject(lambda: re.compile(r'^\w:'), globals(), 'WINDOWS_DRIVE_MATCHER')