Source code for hbutils.system.os.executable

"""
Executable discovery utilities for system PATH inspection.

This module provides utilities to locate executable files in the system ``PATH``,
similar to the Unix ``which``/``where`` commands and the Windows ``where`` command.
It supports both Unix-like systems and Windows, handling platform-specific search
behavior and executable file extensions.

The module contains the following main public components:

* :func:`where` - Return all matching executable paths in ``PATH``
* :func:`which` - Return the first matching executable path (deprecated)

.. note::
   On Windows, the search includes the current directory and checks common
   executable extensions (``.bat``, ``.cmd``, ``.com``, ``.exe``) as defined by
   the implementation.

Example::

    >>> from hbutils.system.os.executable import where
    >>> where('python')  # doctest: +SKIP
    ['/usr/bin/python', '/bin/python']

"""
import itertools
import os
from typing import Iterable, Iterator, Optional, List

from deprecation import deprecated

from .type import is_windows
from ...config.meta import __VERSION__

__all__ = [
    'where', 'which',
]


[docs] def where(execfile: str) -> List[str]: """ Return all matching file paths for the given executable. This function searches through all directories in the system ``PATH`` environment variable and returns a list of all absolute paths where the executable file can be found. On Windows, it also checks for common executable extensions (``.bat``, ``.cmd``, ``.com``, ``.exe``) and includes the current directory in the search path. :param execfile: Executable file to locate (such as ``python``). :type execfile: str :return: A list of normalized absolute paths to executable files. :rtype: List[str] Example:: >>> from hbutils.system.os.executable import where >>> where('bash') # doctest: +SKIP ['/usr/bin/bash', '/bin/bash'] >>> where('not_installed') [] """ return list(_iter_where(execfile))
[docs] @deprecated(deprecated_in="0.9", removed_in="1.0", current_version=__VERSION__, details="Use the native :func:`shutil.which` instead") def which(execfile: str) -> Optional[str]: """ Return the first matching executable path in the system ``PATH`` (deprecated). This function returns the first executable file found in the system ``PATH``, which is typically the one that would be executed when running the command in a terminal. Returns ``None`` if no matching executable is found. .. deprecated:: 0.9 Use the native :func:`shutil.which` instead. This function will be removed in version 1.0. :param execfile: Executable file to locate (such as ``python``). :type execfile: str :return: Absolute path of the executable file, or ``None`` if not found. :rtype: Optional[str] Example:: >>> from hbutils.system.os.executable import which >>> which('bash') # doctest: +SKIP '/usr/bin/bash' >>> which('not_installed') is None True """ try: return next(_iter_where(execfile)) except StopIteration: return None
def _iter_where(filename: str) -> Iterator[str]: """ Iterate over matching executable paths. This internal helper yields all matching executable file paths for the given filename, applying the same logic used by :func:`where`. :param filename: The executable filename to search for. :type filename: str :return: An iterator yielding absolute paths of matching executable files. :rtype: Iterator[str] """ possible_paths = _gen_possible_matches(filename) existing_file_paths = filter(_is_executable, possible_paths) return existing_file_paths def _is_executable(filename: str) -> bool: """ Determine whether the given path points to an executable file. A path is considered executable if it: * Exists * Is a file (not a directory) * Has executable permissions :param filename: The file path to check. :type filename: str :return: ``True`` if the file is executable, ``False`` otherwise. :rtype: bool """ return os.path.exists(filename) and os.path.isfile(filename) and os.access(filename, os.X_OK) def _normpath(filename: str) -> str: """ Normalize a file path to a canonical absolute form. This function applies both case normalization and path normalization to ensure consistent path representation across different platforms, which is especially important on Windows where path representations can vary. .. note:: ``os.path.normcase`` and ``os.path.normpath`` are critical here because the expression form of a path is not unique, especially on Windows. :param filename: The file path to normalize. :type filename: str :return: The normalized absolute path. :rtype: str """ return os.path.normcase(os.path.normpath(os.path.abspath(filename))) def _unique_str(siter: Iterable[str]) -> Iterator[str]: """ Yield unique strings from an iterable, preserving order. This function maintains the order of first occurrence while removing duplicates from the input iterable. :param siter: An iterable of strings. :type siter: Iterable[str] :return: An iterator yielding unique strings in order of first occurrence. :rtype: Iterator[str] """ _exist_str = set() for s in siter: if s not in _exist_str: yield s _exist_str.add(s) def _gen_possible_matches(filename: str) -> Iterator[str]: """ Generate all possible executable path candidates. This function generates potential paths by combining the filename with all directories in the system ``PATH``. On Windows, it also: * Includes the current directory in the search * Appends common executable extensions (``.bat``, ``.cmd``, ``.com``, ``.exe``) All generated paths are normalized to ensure uniqueness and consistency. :param filename: The executable filename to search for. :type filename: str :return: An iterator yielding normalized possible file paths. :rtype: Iterator[str] """ path_parts = os.environ.get("PATH", "").split(os.pathsep) if is_windows(): # Only in Windows, the executable file in current directory can be called. path_parts = itertools.chain((os.curdir,), path_parts) possible_paths = map(lambda x: os.path.join(x, filename), path_parts) if is_windows(): possible_paths = itertools.chain( *map(lambda path: (path, f"{path}.bat", f"{path}.cmd", f"{path}.com", f"{path}.exe"), possible_paths)) return _unique_str(map(_normpath, possible_paths))