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