[
  {
    "path": ".github/dependabot.yml",
    "content": "# Keep GitHub Actions up to date with GitHub's Dependabot...\n# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot\n# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem\nversion: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    groups:\n      github-actions:\n        patterns:\n          - \"*\"  # Group all Actions updates into a single larger pull request\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci\n\non:\n  - push\n  - pull_request\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n    - uses: actions/setup-python@v6\n      with:\n          python-version: 3.x\n    - run: pip install black ruff\n    - run: |\n        ruff check src\n        black --check src\n\n  build:\n    runs-on: ${{ matrix.platform }}\n    continue-on-error: true\n    strategy:\n      matrix:\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n        python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install pipenv tox tox-gh-actions\n\n      - name: Test with tox\n        run: tox\n        env:\n          PLATFORM: ${{ matrix.platform }}\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: publish\n\non:\n  push:\n    tags:\n    - '[0-9]+.[0-9]+.[0-9]+'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    name: Build a pure Python wheel and source distribution\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Install build dependencies\n      run: python -m pip install --upgrade build\n\n    - name: Build\n      run: python -m build\n\n    - uses: actions/upload-artifact@v3\n      with:\n        name: artifacts\n        path: dist/*\n        if-no-files-found: error\n\n  publish:\n    name: Publish release\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n    environment:\n      name: publish\n      url: https://pypi.org/p/shellingham\n    needs:\n    - build\n\n    steps:\n    - uses: actions/download-artifact@v4.1.7\n      with:\n        name: artifacts\n        path: dist\n\n    - name: Push build artifacts to PyPI\n      uses: pypa/gh-action-pypi-publish@v1.8.14\n      with:\n        skip-existing: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".env\n.venv\n__pycache__\n\nbuild\ndist\n\n*.egg-info\n\n*.py[co]\n\n# Editors\n.vscode\n\n# Testing\n.tox\n.pytest_cache\n"
  },
  {
    "path": "CHANGELOG.rst",
    "content": "1.5.4 (2023-10-24)\n==================\n\nBug Fixes\n---------\n\n- Fix a bug that ``detect_shell()`` always returns the host shell on MacOS.  `#81 <https://github.com/sarugaku/shellingham/issues/81>`_\n\n\n1.5.3 (2023-08-17)\n==================\n\nBug Fixes\n---------\n\n- Detect proc format eagerly so it throws the error at the correct location.  `#78 <https://github.com/sarugaku/shellingham/issues/78>`_\n\n\n1.5.2 (2023-08-16)\n==================\n\nBug Fixes\n---------\n\n- Fixed a regression causing ``ShellDetectionFailure`` if ``/proc`` is used for process lookup.  `#72 <https://github.com/sarugaku/shellingham/issues/72>`_\n\n\n1.5.1 (2023-02-13)\n==================\n\nBug Fixes\n---------\n\n- Select the correct status file fields on BSD systems.  `#68 <https://github.com/sarugaku/shellingham/issues/68>`_\n\n\n1.5.0.post1 (2023-01-03)\n==================\n\n- Fix package metadata to disallow installation on Python prior to 3.7. This was already done in 1.5.0,\n  but the metadata of the release was incorrectly set to ``>=3.4``.\n\n\n1.5.0 (2022-08-04)\n==================\n\nFeatures\n--------\n\n- Drop support for Python version older than 3.7.  `#50 <https://github.com/sarugaku/shellingham/issues/50>`_\n- Support detecting NuShell.  `#56 <https://github.com/sarugaku/shellingham/issues/56>`_\n\n\n1.4.0 (2021-02-01)\n==================\n\nFeatures\n--------\n\n- On Windows, the full path to the shell executable is now returned instead of\n  just the base name.  `#42 <https://github.com/sarugaku/shellingham/issues/42>`_\n\n\n1.3.2 (2020-02-12)\n==================\n\nBug Fixes\n---------\n\n- Parse argument list to detect shells run via an interpreter, e.g. xonsh via Python.  `#27 <https://github.com/sarugaku/shellingham/issues/27>`_\n\n\n1.3.1 (2019-04-10)\n==================\n\nBug Fixes\n---------\n\n- Fix a typo that prevents ash and csh from being detected.  `#24\n  <https://github.com/sarugaku/shellingham/issues/24>`_\n\n\n1.3.0 (2019-03-06)\n==================\n\nFeatures\n--------\n\n- Add `Almquist shell <https://en.wikipedia.org/wiki/Almquist_shell>`_\n  (``ash``) detection support.  `#20\n  <https://github.com/sarugaku/shellingham/issues/20>`_\n\n\n1.2.8 (2018-12-16)\n==================\n\nBug Fixes\n---------\n\n- Parse ``ps`` output according to how it is actually formatted, instead of\n  incorrectly using ``shlex.split()``.  `#14\n  <https://github.com/sarugaku/shellingham/issues/14>`_\n\n- Improve process parsing on Windows to so executables with non-ASCII names are\n  handled better.  `#16 <https://github.com/sarugaku/shellingham/issues/16>`_\n\n\n1.2.7 (2018-10-15)\n==================\n\nBug Fixes\n---------\n\n- Suppress subprocess errors from ``ps`` if the output is empty.  `#15\n  <https://github.com/sarugaku/shellingham/issues/15>`_\n\n\n1.2.6 (2018-09-14)\n==================\n\nNo significant changes.\n\n\n1.2.5 (2018-09-14)\n==================\n\nBug Fixes\n---------\n\n- Improve ``/proc`` content parsing robustness to not fail with non-decodable\n  command line arguments.  `#10\n  <https://github.com/sarugaku/shellingham/issues/10>`_\n\n\n1.2.4 (2018-07-27)\n==================\n\nBug Fixes\n---------\n\n- Fix exception on Windows when the executable path is too long to fit into the\n  PROCESSENTRY32 struct. Generally the shell shouldn't be buried this deep, and\n  we can always fix it when that actually happens, if ever.  `#8\n  <https://github.com/sarugaku/shellingham/issues/8>`_\n\n\n1.2.3 (2018-07-10)\n=======================\n\nBug Fixes\n---------\n\n- Check a process’s argument list is valid before peeking into it. This works\n  around a Heisenbug in VS Code, where a process read from ``/proc`` may\n  contain an empty argument list.\n\n\n1.2.2 (2018-07-09)\n==================\n\nFeatures\n--------\n\n- Support BSD-style ``/proc`` format.  `#4\n  <https://github.com/sarugaku/shellingham/issues/4>`_\n\n\nBug Fixes\n---------\n\n- Better ``ps`` output decoding to fix compatibility.  `#7\n  <https://github.com/sarugaku/shellingham/issues/7>`_\n\n\n1.2.1 (2018-07-04)\n==================\n\nBug Fixes\n---------\n\n- Fix login shell detection if it is ``chsh``-ed to point to an absolute path.\n  `#6 <https://github.com/sarugaku/shellingham/issues/6>`_\n\n\n1.2.0 (2018-07-04)\n==================\n\nFeatures\n--------\n\n- Prefer the ``/proc``-based approach on POSIX whenever it is likely to work.\n  `#5 <https://github.com/sarugaku/shellingham/issues/5>`_\n\n\n1.1.0 (2018-06-19)\n==================\n\nFeatures\n--------\n\n- Use ``/proc`` on Linux to build process tree. This is more reliable than\n  ``ps``, which may not be available on a bare installation.  `#3\n  <https://github.com/sarugaku/shellingham/issues/3>`_\n\n\n1.0.1 (2018-06-19)\n==================\n\nBug Fixes\n---------\n\n- Fix POSIX usage on Python 2 by providing more compatible arguments to parse\n  ``ps`` results. Thanks to @glehmann for the patch.  `#2\n  <https://github.com/sarugaku/shellingham/issues/2>`_\n\n\n1.0.0.dev1 (2018-06-15)\n=======================\n\nBug Fixes\n---------\n\n- Prevent the lookup from exploding when running in non-hierarchical process\n  structure. (1-b2e9bef5)\n\n\n1.0.0.dev0 (2018-06-14)\n=======================\n\nInitial release.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2018, Tzu-ping Chung <uranusjr@gmail.com>\n\nPermission to use, copy, modify, and distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE* README*\ninclude pyproject.toml\n"
  },
  {
    "path": "Pipfile",
    "content": "[packages]\nshellingham = { path = '.', editable = true }\n\n[dev-packages]\ninvoke = '*'\nparver = '*'\npytest = '*'\npytest-mock = '*'\nsetl = '*'\ntowncrier = '*'\ntox = '*'\ntox-gh-actions = '*'\n\n[scripts]\nrelease = 'inv release'\n"
  },
  {
    "path": "README.rst",
    "content": "=============================================\nShellingham: Tool to Detect Surrounding Shell\n=============================================\n\n.. image:: https://img.shields.io/pypi/v/shellingham.svg\n    :target: https://pypi.org/project/shellingham/\n\nShellingham detects what shell the current Python executable is running in.\n\n\nUsage\n=====\n\n.. code-block:: python\n\n    >>> import shellingham\n    >>> shellingham.detect_shell()\n    ('bash', '/bin/bash')\n\n``detect_shell`` pokes around the process's running environment to determine\nwhat shell it is run in. It returns a 2-tuple:\n\n* The shell name, always lowercased.\n* The command used to run the shell.\n\n``ShellDetectionFailure`` is raised if ``detect_shell`` fails to detect the\nsurrounding shell.\n\n\nNotes\n=====\n\n* The shell name is always lowercased.\n* On Windows, the shell name is the name of the executable, minus the file\n  extension.\n\n\nNotes for Application Developers\n================================\n\nRemember, your application's user is not necessarily using a shell.\nShellingham raises ``ShellDetectionFailure`` if there is no shell to detect,\nbut *your application should almost never do this to your user*.\n\nA practical approach to this is to wrap ``detect_shell`` in a try block, and\nprovide a sane default on failure\n\n.. code-block:: python\n\n    try:\n        shell = shellingham.detect_shell()\n    except shellingham.ShellDetectionFailure:\n        shell = provide_default()\n\n\nThere are a few choices for you to choose from.\n\n* The POSIX standard mandates the environment variable ``SHELL`` to refer to\n  \"the user's preferred command language interpreter\". This is always available\n  (even if the user is not in an interactive session), and likely the correct\n  choice to launch an interactive sub-shell with.\n* A command ``sh`` is almost guaranteed to exist, likely at ``/bin/sh``, since\n  several POSIX tools rely on it. This should be suitable if you want to run a\n  (possibly non-interactive) script.\n* All versions of DOS and Windows have an environment variable ``COMSPEC``.\n  This can always be used to launch a usable command prompt (e.g. `cmd.exe` on\n  Windows).\n\nHere's a simple implementation to provide a default shell\n\n.. code-block:: python\n\n    import os\n\n    def provide_default():\n        if os.name == 'posix':\n            return os.environ['SHELL']\n        elif os.name == 'nt':\n            return os.environ['COMSPEC']\n        raise NotImplementedError(f'OS {os.name!r} support not available')\n"
  },
  {
    "path": "news/.gitignore",
    "content": "!.gitignore\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\", \"wheel\"]\n\n[tool.black]\nline-length = 79\n\n[tool.towncrier]\npackage = \"shellingham\"\npackage_dir = \"src\"\nfilename = \"CHANGELOG.rst\"\ndirectory = \"news/\"\ntitle_format = \"{version} ({project_date})\"\nissue_format = \"`#{issue} <https://github.com/sarugaku/shellingham/issues/{issue}>`_\"\ntemplate = \"tasks/CHANGELOG.rst.jinja2\"\n\n[[tool.towncrier.type]]\ndirectory = \"feature\"\nname = \"Features\"\nshowcontent = true\n\n[[tool.towncrier.type]]\ndirectory = \"bugfix\"\nname = \"Bug Fixes\"\nshowcontent = true\n\n[[tool.towncrier.type]]\ndirectory = \"trivial\"\nname = \"Trivial Changes\"\nshowcontent = false\n\n[[tool.towncrier.type]]\ndirectory = \"removal\"\nname = \"Removals and Deprecations\"\nshowcontent = true\n"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\nname = shellingham\nversion = attr: shellingham.__version__\ndescription = Tool to Detect Surrounding Shell\nurl = https://github.com/sarugaku/shellingham\nauthor = Tzu-ping Chung\nauthor_email = uranusjr@gmail.com\nlong_description = file: README.rst\nlong_description_content_type = text/x-rst\nlicense = ISC License\nkeywords =\n    shell\nclassifier =\n    Development Status :: 3 - Alpha\n    Environment :: Console\n    Intended Audience :: Developers\n    License :: OSI Approved :: ISC License (ISCL)\n    Operating System :: OS Independent\n    Programming Language :: Python :: 3 :: Only\n    Programming Language :: Python :: 3.7\n    Programming Language :: Python :: 3.8\n    Programming Language :: Python :: 3.9\n    Programming Language :: Python :: 3.10\n    Programming Language :: Python :: 3.11\n    Programming Language :: Python :: 3.12\n    Topic :: Software Development :: Libraries :: Python Modules\n\n[options]\npackage_dir =\n    = src\npackages = find:\npython_requires = >=3.7\ninstall_requires =\nzip_safe = true\n\n[options.packages.find]\nwhere = src\n\n[bdist_wheel]\nuniversal = 1\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\nsetup()\n"
  },
  {
    "path": "src/shellingham/__init__.py",
    "content": "import importlib\nimport os\n\nfrom ._core import ShellDetectionFailure\n\n__version__ = \"1.5.4\"\n\n\ndef detect_shell(pid=None, max_depth=10):\n    name = os.name\n    try:\n        impl = importlib.import_module(\".{}\".format(name), __name__)\n    except ImportError:\n        message = \"Shell detection not implemented for {0!r}\".format(name)\n        raise RuntimeError(message)\n    try:\n        get_shell = impl.get_shell\n    except AttributeError:\n        raise RuntimeError(\"get_shell not implemented for {0!r}\".format(name))\n    shell = get_shell(pid, max_depth=max_depth)\n    if shell:\n        return shell\n    raise ShellDetectionFailure()\n"
  },
  {
    "path": "src/shellingham/_core.py",
    "content": "SHELL_NAMES = (\n    {\"sh\", \"bash\", \"dash\", \"ash\"}  # Bourne.\n    | {\"csh\", \"tcsh\"}  # C.\n    | {\"ksh\", \"zsh\", \"fish\"}  # Common alternatives.\n    | {\"cmd\", \"powershell\", \"pwsh\"}  # Microsoft.\n    | {\"elvish\", \"xonsh\", \"nu\"}  # More exotic.\n)\n\n\nclass ShellDetectionFailure(EnvironmentError):\n    pass\n"
  },
  {
    "path": "src/shellingham/nt.py",
    "content": "import contextlib\nimport ctypes\nimport os\n\nfrom ctypes.wintypes import (\n    BOOL,\n    CHAR,\n    DWORD,\n    HANDLE,\n    LONG,\n    LPWSTR,\n    MAX_PATH,\n    PDWORD,\n    ULONG,\n)\n\nfrom shellingham._core import SHELL_NAMES\n\n\nINVALID_HANDLE_VALUE = HANDLE(-1).value\nERROR_NO_MORE_FILES = 18\nERROR_INSUFFICIENT_BUFFER = 122\nTH32CS_SNAPPROCESS = 2\nPROCESS_QUERY_LIMITED_INFORMATION = 0x1000\n\n\nkernel32 = ctypes.windll.kernel32\n\n\ndef _check_handle(error_val=0):\n    def check(ret, func, args):\n        if ret == error_val:\n            raise ctypes.WinError()\n        return ret\n\n    return check\n\n\ndef _check_expected(expected):\n    def check(ret, func, args):\n        if ret:\n            return True\n        code = ctypes.GetLastError()\n        if code == expected:\n            return False\n        raise ctypes.WinError(code)\n\n    return check\n\n\nclass ProcessEntry32(ctypes.Structure):\n    _fields_ = (\n        (\"dwSize\", DWORD),\n        (\"cntUsage\", DWORD),\n        (\"th32ProcessID\", DWORD),\n        (\"th32DefaultHeapID\", ctypes.POINTER(ULONG)),\n        (\"th32ModuleID\", DWORD),\n        (\"cntThreads\", DWORD),\n        (\"th32ParentProcessID\", DWORD),\n        (\"pcPriClassBase\", LONG),\n        (\"dwFlags\", DWORD),\n        (\"szExeFile\", CHAR * MAX_PATH),\n    )\n\n\nkernel32.CloseHandle.argtypes = [HANDLE]\nkernel32.CloseHandle.restype = BOOL\n\nkernel32.CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD]\nkernel32.CreateToolhelp32Snapshot.restype = HANDLE\nkernel32.CreateToolhelp32Snapshot.errcheck = _check_handle(  # type: ignore\n    INVALID_HANDLE_VALUE,\n)\n\nkernel32.Process32First.argtypes = [HANDLE, ctypes.POINTER(ProcessEntry32)]\nkernel32.Process32First.restype = BOOL\nkernel32.Process32First.errcheck = _check_expected(  # type: ignore\n    ERROR_NO_MORE_FILES,\n)\n\nkernel32.Process32Next.argtypes = [HANDLE, ctypes.POINTER(ProcessEntry32)]\nkernel32.Process32Next.restype = BOOL\nkernel32.Process32Next.errcheck = _check_expected(  # type: ignore\n    ERROR_NO_MORE_FILES,\n)\n\nkernel32.GetCurrentProcessId.argtypes = []\nkernel32.GetCurrentProcessId.restype = DWORD\n\nkernel32.OpenProcess.argtypes = [DWORD, BOOL, DWORD]\nkernel32.OpenProcess.restype = HANDLE\nkernel32.OpenProcess.errcheck = _check_handle(  # type: ignore\n    INVALID_HANDLE_VALUE,\n)\n\nkernel32.QueryFullProcessImageNameW.argtypes = [HANDLE, DWORD, LPWSTR, PDWORD]\nkernel32.QueryFullProcessImageNameW.restype = BOOL\nkernel32.QueryFullProcessImageNameW.errcheck = _check_expected(  # type: ignore\n    ERROR_INSUFFICIENT_BUFFER,\n)\n\n\n@contextlib.contextmanager\ndef _handle(f, *args, **kwargs):\n    handle = f(*args, **kwargs)\n    try:\n        yield handle\n    finally:\n        kernel32.CloseHandle(handle)\n\n\ndef _iter_processes():\n    f = kernel32.CreateToolhelp32Snapshot\n    with _handle(f, TH32CS_SNAPPROCESS, 0) as snap:\n        entry = ProcessEntry32()\n        entry.dwSize = ctypes.sizeof(entry)\n        ret = kernel32.Process32First(snap, entry)\n        while ret:\n            yield entry\n            ret = kernel32.Process32Next(snap, entry)\n\n\ndef _get_full_path(proch):\n    size = DWORD(MAX_PATH)\n    while True:\n        path_buff = ctypes.create_unicode_buffer(\"\", size.value)\n        if kernel32.QueryFullProcessImageNameW(proch, 0, path_buff, size):\n            return path_buff.value\n        size.value *= 2\n\n\ndef get_shell(pid=None, max_depth=10):\n    proc_map = {\n        proc.th32ProcessID: (proc.th32ParentProcessID, proc.szExeFile)\n        for proc in _iter_processes()\n    }\n    pid = pid or os.getpid()\n\n    for _ in range(0, max_depth + 1):\n        try:\n            ppid, executable = proc_map[pid]\n        except KeyError:  # No such process? Give up.\n            break\n\n        # The executable name would be encoded with the current code page if\n        # we're in ANSI mode (usually). Try to decode it into str/unicode,\n        # replacing invalid characters to be safe (not thoeratically necessary,\n        # I think). Note that we need to use 'mbcs' instead of encoding\n        # settings from sys because this is from the Windows API, not Python\n        # internals (which those settings reflect). (pypa/pipenv#3382)\n        if isinstance(executable, bytes):\n            executable = executable.decode(\"mbcs\", \"replace\")\n\n        name = executable.rpartition(\".\")[0].lower()\n        if name not in SHELL_NAMES:\n            pid = ppid\n            continue\n\n        key = PROCESS_QUERY_LIMITED_INFORMATION\n        with _handle(kernel32.OpenProcess, key, 0, pid) as proch:\n            return (name, _get_full_path(proch))\n\n    return None\n"
  },
  {
    "path": "src/shellingham/posix/__init__.py",
    "content": "import os\nimport re\n\nfrom .._core import SHELL_NAMES, ShellDetectionFailure\nfrom . import proc, ps\n\n# Based on QEMU docs: https://www.qemu.org/docs/master/user/main.html\nQEMU_BIN_REGEX = re.compile(\n    r\"\"\"qemu-\n        (alpha\n        |armeb\n        |arm\n        |m68k\n        |cris\n        |i386\n        |x86_64\n        |microblaze\n        |mips\n        |mipsel\n        |mips64\n        |mips64el\n        |mipsn32\n        |mipsn32el\n        |nios2\n        |ppc64\n        |ppc\n        |sh4eb\n        |sh4\n        |sparc\n        |sparc32plus\n        |sparc64\n    )\"\"\",\n    re.VERBOSE,\n)\n\n\ndef _iter_process_parents(pid, max_depth=10):\n    \"\"\"Select a way to obtain process information from the system.\n\n    * `/proc` is used if supported.\n    * The system `ps` utility is used as a fallback option.\n    \"\"\"\n    for impl in (proc, ps):\n        try:\n            iterator = impl.iter_process_parents(pid, max_depth)\n        except EnvironmentError:\n            continue\n        return iterator\n    raise ShellDetectionFailure(\"compatible proc fs or ps utility is required\")\n\n\ndef _get_login_shell(proc_cmd):\n    \"\"\"Form shell information from SHELL environ if possible.\"\"\"\n    login_shell = os.environ.get(\"SHELL\", \"\")\n    if login_shell:\n        proc_cmd = login_shell\n    else:\n        proc_cmd = proc_cmd[1:]\n    return (os.path.basename(proc_cmd).lower(), proc_cmd)\n\n\n_INTERPRETER_SHELL_NAMES = [\n    (re.compile(r\"^python(\\d+(\\.\\d+)?)?$\"), {\"xonsh\"}),\n]\n\n\ndef _get_interpreter_shell(proc_name, proc_args):\n    \"\"\"Get shell invoked via an interpreter.\n\n    Some shells are implemented on, and invoked with an interpreter, e.g. xonsh\n    is commonly executed with an executable Python script. This detects what\n    script the interpreter is actually running, and check whether that looks\n    like a shell.\n\n    See sarugaku/shellingham#26 for rational.\n    \"\"\"\n    for pattern, shell_names in _INTERPRETER_SHELL_NAMES:\n        if not pattern.match(proc_name):\n            continue\n        for arg in proc_args:\n            name = os.path.basename(arg).lower()\n            if os.path.isfile(arg) and name in shell_names:\n                return (name, arg)\n    return None\n\n\ndef _get_shell(cmd, *args):\n    if cmd.startswith(\"-\"):  # Login shell! Let's use this.\n        return _get_login_shell(cmd)\n    name = os.path.basename(cmd).lower()\n    if name == \"rosetta\" or QEMU_BIN_REGEX.fullmatch(name):\n        # If the current process is Rosetta or QEMU, this likely is a\n        # containerized process. Parse out the actual command instead.\n        cmd = args[0]\n        args = args[1:]\n        name = os.path.basename(cmd).lower()\n    if name in SHELL_NAMES:  # Command looks like a shell.\n        return (name, cmd)\n    shell = _get_interpreter_shell(name, args)\n    if shell:\n        return shell\n    return None\n\n\ndef get_shell(pid=None, max_depth=10):\n    \"\"\"Get the shell that the supplied pid or os.getpid() is running in.\"\"\"\n    pid = str(pid or os.getpid())\n    for proc_args, _, _ in _iter_process_parents(pid, max_depth):\n        shell = _get_shell(*proc_args)\n        if shell:\n            return shell\n    return None\n"
  },
  {
    "path": "src/shellingham/posix/_core.py",
    "content": "import collections\n\nProcess = collections.namedtuple(\"Process\", \"args pid ppid\")\n"
  },
  {
    "path": "src/shellingham/posix/proc.py",
    "content": "import io\nimport os\nimport re\nimport sys\n\nfrom ._core import Process\n\n# FreeBSD: https://www.freebsd.org/cgi/man.cgi?query=procfs\n# NetBSD: https://man.netbsd.org/NetBSD-9.3-STABLE/mount_procfs.8\n# DragonFlyBSD: https://www.dragonflybsd.org/cgi/web-man?command=procfs\nBSD_STAT_PPID = 2\n\n# See https://docs.kernel.org/filesystems/proc.html\nLINUX_STAT_PPID = 3\n\nSTAT_PATTERN = re.compile(r\"\\(.+\\)|\\S+\")\n\n\ndef detect_proc():\n    \"\"\"Detect /proc filesystem style.\n\n    This checks the /proc/{pid} directory for possible formats. Returns one of\n    the following as str:\n\n    * `stat`: Linux-style, i.e. ``/proc/{pid}/stat``.\n    * `status`: BSD-style, i.e. ``/proc/{pid}/status``.\n    \"\"\"\n    pid = os.getpid()\n    for name in (\"stat\", \"status\"):\n        if os.path.exists(os.path.join(\"/proc\", str(pid), name)):\n            return name\n    raise ProcFormatError(\"unsupported proc format\")\n\n\ndef _use_bsd_stat_format():\n    try:\n        return os.uname().sysname.lower() in (\"freebsd\", \"netbsd\", \"dragonfly\")\n    except Exception:\n        return False\n\n\ndef _get_ppid(pid, name):\n    path = os.path.join(\"/proc\", str(pid), name)\n    with io.open(path, encoding=\"ascii\", errors=\"replace\") as f:\n        parts = STAT_PATTERN.findall(f.read())\n    # We only care about TTY and PPID -- both are numbers.\n    if _use_bsd_stat_format():\n        return parts[BSD_STAT_PPID]\n    return parts[LINUX_STAT_PPID]\n\n\ndef _get_cmdline(pid):\n    path = os.path.join(\"/proc\", str(pid), \"cmdline\")\n    encoding = sys.getfilesystemencoding() or \"utf-8\"\n    with io.open(path, encoding=encoding, errors=\"replace\") as f:\n        # XXX: Command line arguments can be arbitrary byte sequences, not\n        # necessarily decodable. For Shellingham's purpose, however, we don't\n        # care. (pypa/pipenv#2820)\n        # cmdline appends an extra NULL at the end, hence the [:-1].\n        return tuple(f.read().split(\"\\0\")[:-1])\n\n\nclass ProcFormatError(EnvironmentError):\n    pass\n\n\ndef iter_process_parents(pid, max_depth=10):\n    \"\"\"Try to look up the process tree via the /proc interface.\"\"\"\n    stat_name = detect_proc()\n\n    # Inner generator function so we correctly throw an error eagerly if proc\n    # is not supported, rather than on the first call to the iterator. This\n    # allows the call site detects the correct implementation.\n    def _iter_process_parents(pid, max_depth):\n        for _ in range(max_depth):\n            ppid = _get_ppid(pid, stat_name)\n            args = _get_cmdline(pid)\n            yield Process(args=args, pid=pid, ppid=ppid)\n            if ppid == \"0\":\n                break\n            pid = ppid\n\n    return _iter_process_parents(pid, max_depth)\n"
  },
  {
    "path": "src/shellingham/posix/ps.py",
    "content": "import errno\nimport subprocess\nimport sys\n\nfrom ._core import Process\n\n\nclass PsNotAvailable(EnvironmentError):\n    pass\n\n\ndef iter_process_parents(pid, max_depth=10):\n    \"\"\"Try to look up the process tree via the output of `ps`.\"\"\"\n    try:\n        cmd = [\"ps\", \"-ww\", \"-o\", \"pid=\", \"-o\", \"ppid=\", \"-o\", \"args=\"]\n        output = subprocess.check_output(cmd)\n    except OSError as e:  # Python 2-compatible FileNotFoundError.\n        if e.errno != errno.ENOENT:\n            raise\n        raise PsNotAvailable(\"ps not found\")\n    except subprocess.CalledProcessError as e:\n        # `ps` can return 1 if the process list is completely empty.\n        # (sarugaku/shellingham#15)\n        if not e.output.strip():\n            return\n        raise\n    if not isinstance(output, str):\n        encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()\n        output = output.decode(encoding)\n\n    processes_mapping = {}\n    for line in output.split(\"\\n\"):\n        try:\n            _pid, ppid, args = line.strip().split(None, 2)\n            # XXX: This is not right, but we are really out of options.\n            # ps does not offer a sane way to decode the argument display,\n            # and this is \"Good Enough\" for obtaining shell names. Hopefully\n            # people don't name their shell with a space, or have something\n            # like \"/usr/bin/xonsh is uber\". (sarugaku/shellingham#14)\n            args = tuple(a.strip() for a in args.split(\" \"))\n        except ValueError:\n            continue\n        processes_mapping[_pid] = Process(args=args, pid=_pid, ppid=ppid)\n\n    for _ in range(max_depth):\n        try:\n            process = processes_mapping[pid]\n        except KeyError:\n            return\n        yield process\n        pid = process.ppid\n"
  },
  {
    "path": "tasks/CHANGELOG.rst.jinja2",
    "content": "{% for section in sections %}\n{% set underline = \"-\" %}\n{% if section %}\n{{section}}\n{{ underline * section|length }}{% set underline = \"~\" %}\n\n{% endif %}\n{% if sections[section] %}\n{% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %}\n\n{{ definitions[category]['name'] }}\n{{ underline * definitions[category]['name']|length }}\n\n{% if definitions[category]['showcontent'] %}\n{% for text, values in sections[section][category]|dictsort(by='value') %}\n- {{ text }}{% if category != 'process' %}\n  {{ values|sort|join(',\\n  ') }}\n  {% endif %}\n\n{% endfor %}\n{% else %}\n- {{ sections[section][category]['']|sort|join(', ') }}\n\n\n{% endif %}\n{% if sections[section][category]|length == 0 %}\n\nNo significant changes.\n\n\n{% else %}\n{% endif %}\n{% endfor %}\n{% else %}\n\nNo significant changes.\n\n\n{% endif %}\n{% endfor %}\n"
  },
  {
    "path": "tasks/__init__.py",
    "content": "import pathlib\nimport shutil\nimport subprocess\n\nimport invoke\nimport parver\n\nfrom towncrier._builder import (\n    find_fragments, render_fragments, split_fragments,\n)\nfrom towncrier._settings import load_config\n\n\nROOT = pathlib.Path(__file__).resolve().parent.parent\n\nINIT_PY = ROOT.joinpath('src', 'shellingham', '__init__.py')\n\n\n@invoke.task()\ndef clean(ctx):\n    \"\"\"Clean previously built package artifacts.\n    \"\"\"\n    ctx.run(f'python setup.py clean')\n    dist = ROOT.joinpath('dist')\n    print(f'[clean] Removing {dist}')\n    if dist.exists():\n        shutil.rmtree(str(dist))\n\n\ndef _read_version():\n    out = subprocess.check_output(['git', 'tag'], encoding='ascii')\n    version = max(parver.Version.parse(v).normalize() for v in (\n        line.strip() for line in out.split('\\n')\n    ) if v)\n    return version\n\n\ndef _write_version(v):\n    lines = []\n    with INIT_PY.open() as f:\n        for line in f:\n            if line.startswith('__version__ = '):\n                line = f'__version__ = {repr(str(v))}\\n'\n            lines.append(line)\n    with INIT_PY.open('w', newline='\\n') as f:\n        f.write(''.join(lines))\n\n\ndef _render_log():\n    \"\"\"Totally tap into Towncrier internals to get an in-memory result.\n    \"\"\"\n    config = load_config(ROOT)\n    definitions = config['types']\n    fragments, fragment_filenames = find_fragments(\n        pathlib.Path(config['directory']).absolute(),\n        config['sections'],\n        None,\n        definitions,\n    )\n    rendered = render_fragments(\n        pathlib.Path(config['template']).read_text(encoding='utf-8'),\n        config['issue_format'],\n        split_fragments(fragments, definitions),\n        definitions,\n        config['underlines'][1:],\n    )\n    return rendered\n\n\nREL_TYPES = ('major', 'minor', 'patch',)\n\n\ndef _bump_release(version, type_):\n    if type_ not in REL_TYPES:\n        raise ValueError(f'{type_} not in {REL_TYPES}')\n    index = REL_TYPES.index(type_)\n    next_version = version.base_version().bump_release(index)\n    print(f'[bump] {version} -> {next_version}')\n    return next_version\n\n\nPREBUMP = 2     # Default to next patch number.\n\n\ndef _prebump(version):\n    next_version = version.bump_release(PREBUMP).bump_dev()\n    print(f'[bump] {version} -> {next_version}')\n    return next_version\n\n\n@invoke.task(pre=[clean])\ndef release(ctx, type_, repo):\n    \"\"\"Make a new release.\n    \"\"\"\n    version = _read_version()\n    version = _bump_release(version, type_)\n    _write_version(version)\n\n    # Needs to happen before Towncrier deletes fragment files.\n    tag_content = _render_log()\n\n    ctx.run('towncrier')\n\n    ctx.run(f'git commit -am \"Release {version}\"')\n\n    tag_content = tag_content.replace('\"', '\\\\\"')\n    ctx.run(f'git tag -a {version} -m \"Version {version}\\n\\n{tag_content}\"')\n\n    ctx.run(f'setl publish --repository=\"{repo}\"')\n\n    version = _prebump(version)\n    _write_version(version)\n\n    ctx.run(f'git commit -am \"Prebump to {version}\"')\n"
  },
  {
    "path": "tests/test_posix.py",
    "content": "import os\n\nimport pytest\n\nfrom shellingham import posix\nfrom shellingham.posix._core import Process\n\n\nclass EnvironManager(object):\n\n    def __init__(self):\n        self.backup = {}\n        self.changed = set()\n\n    def patch(self, **kwargs):\n        self.backup.update({\n            k: os.environ[k] for k in kwargs if k in os.environ\n        })\n        self.changed.update(kwargs.keys())\n        os.environ.update(kwargs)\n\n    def unpatch(self):\n        for k in self.changed:\n            try:\n                v = self.backup[k]\n            except KeyError:\n                os.environ.pop(k, None)\n            else:\n                os.environ[k] = v\n\n\n@pytest.fixture()\ndef environ(request):\n    \"\"\"Provide environment variable override, and restore on finalize.\n    \"\"\"\n    manager = EnvironManager()\n    request.addfinalizer(manager.unpatch)\n    return manager\n\n\nMAPPING_EXAMPLE_KEEGANCSMITH = [\n    Process(\n        args=(\n            \"/Applications/iTerm.app/Contents/MacOS/iTerm2\",\n            \"--server\",\n            \"login\",\n            \"-fp\",\n            \"keegan\",\n        ),\n        pid=\"1480\",\n        ppid=\"1477\",\n    ),\n    Process(args=(\"-bash\",), pid=\"1482\", ppid=\"1481\"),\n    Process(args=(\"screen\",), pid=\"1556\", ppid=\"1482\"),\n    Process(args=(\"-/usr/local/bin/bash\",), pid=\"1558\", ppid=\"1557\"),\n    Process(\n        args=(\n            \"/Applications/Emacs.app/Contents/MacOS/Emacs-x86_64-10_10\",\n            \"-nw\",\n        ),\n        pid=\"1706\",\n        ppid=\"1558\",\n    ),\n    Process(\n        args=(\"/usr/local/bin/aspell\", \"-a\", \"-m\", \"-B\", \"--encoding=utf-8\"),\n        pid=\"77061\",\n        ppid=\"1706\",\n    ),\n    Process(args=(\"-/usr/local/bin/bash\",), pid=\"1562\", ppid=\"1557\"),\n    Process(args=(\"-/usr/local/bin/bash\",), pid=\"87033\", ppid=\"1557\"),\n    Process(args=(\"-/usr/local/bin/bash\",), pid=\"84732\", ppid=\"1557\"),\n    Process(args=(\"-/usr/local/bin/bash\",), pid=\"89065\", ppid=\"1557\"),\n    Process(args=(\"-/usr/local/bin/bash\",), pid=\"80216\", ppid=\"1557\"),\n]\n\n\n@pytest.mark.parametrize('mapping, result', [\n    (   # Based on pypa/pipenv#2496, provided by @keegancsmith.\n        MAPPING_EXAMPLE_KEEGANCSMITH, ('bash', '==MOCKED=LOGIN=SHELL==/bash'),\n    ),\n])\ndef test_get_shell(mocker, environ, mapping, result):\n    environ.patch(SHELL=\"==MOCKED=LOGIN=SHELL==/bash\")\n    mocker.patch.object(posix, \"_iter_process_parents\", return_value=mapping)\n    assert posix.get_shell(pid=77061) == result\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py37, py38, py39, py310, py311, py312\n\n[gh-actions]\npython =\n    3.7: py37\n    3.8: py38\n    3.9: py39\n    3.10: py310\n    3.11: py311\n    3.12: py312\n\n[testenv]\nallowlist_externals = pipenv\ncommands =\n    pipenv install --dev\n\n[testenv:test]\ndeps =\n    pytest\n\ncommands =\n    pytest\n"
  }
]