From fcccadc8f3d269b80ce1229e38bed28783c79c59 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Oct 2025 21:44:01 +0530 Subject: [PATCH] Make reading resolved shell env more robust We pass -0 to env so that it works even for env vars that have newlines in them. --- kitty/utils.py | 112 +++++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/kitty/utils.py b/kitty/utils.py index 8836ddc76..1cd7b44ca 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -11,6 +11,7 @@ from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Se from contextlib import contextmanager, suppress from functools import lru_cache from re import Match, Pattern +from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, @@ -743,62 +744,71 @@ def which(name: str, only_system: bool = False) -> str | None: return None -def read_shell_environment(opts: Options | None = None) -> dict[str, str]: - ans: dict[str, str] | None = getattr(read_shell_environment, 'ans', None) - if ans is None: - from .child import openpty - ans = {} - setattr(read_shell_environment, 'ans', ans) - import subprocess - shell = resolved_shell(opts) - master, slave = openpty() - os.set_blocking(master, False) - if '-l' not in shell and '--login' not in shell: - shell += ['-l'] - if '-i' not in shell and '--interactive' not in shell: - shell += ['-i'] - try: - p = subprocess.Popen( - shell + ['-c', 'env'], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True, - preexec_fn=clear_handled_signals) - except FileNotFoundError: - log_error('Could not find shell to read environment') - return ans - with os.fdopen(master, 'rb') as stdout, os.fdopen(slave, 'wb'): - raw = b'' - from time import monotonic - start_time = monotonic() - while monotonic() - start_time < 1.5: +@lru_cache(4) +def read_resolved_shell_environment(shell: tuple[str, ...]) -> MappingProxyType[str, str]: + import subprocess + cmdline = list(shell) + if '-l' not in cmdline and '--login' not in shell: + cmdline += ['-l'] + if '-i' not in shell and '--interactive' not in shell: + cmdline += ['-i'] + is_fish = 'fish' in os.path.basename(cmdline[0]).lower() + cmd = 'command env -0' if is_fish else 'builtin command env -0' + ans: MappingProxyType[str, str] = MappingProxyType({}) + + from .child import openpty + master, slave = openpty() + os.set_blocking(master, False) + try: + p = subprocess.Popen( + cmdline + ['-c', cmd], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True, + preexec_fn=clear_handled_signals) + except FileNotFoundError: + log_error('Could not find shell to read environment') + return ans + with os.fdopen(master, 'rb') as stdout, os.fdopen(slave, 'wb'): + raw = b'' + from time import monotonic + start_time = monotonic() + ret: int | None = None + while monotonic() - start_time < 1.5: + try: + ret = p.wait(0.01) + except subprocess.TimeoutExpired: + ret = None + with suppress(Exception): + raw += stdout.read() + if ret is not None: + break + if ret is None: + log_error('Timed out waiting for shell to quit while reading shell environment') + p.kill() + elif ret == 0: + while True: try: - ret: int | None = p.wait(0.01) - except subprocess.TimeoutExpired: - ret = None - with suppress(Exception): - raw += stdout.read() - if ret is not None: + x = stdout.read() + except Exception: break - if cast(Optional[int], p.returncode) is None: - log_error('Timed out waiting for shell to quit while reading shell environment') - p.kill() - elif p.returncode == 0: - while True: - try: - x = stdout.read() - except Exception: - break - if not x: - break - raw += x - draw = raw.decode('utf-8', 'replace') - for line in draw.splitlines(): - k, v = line.partition('=')[::2] - if k and v: - ans[k] = v - else: - log_error('Failed to run shell to read its environment') + if not x: + break + raw += x + draw = raw.decode('utf-8', 'replace') + env = {} + for line in draw.split('\0'): + k, sep, v = line.partition('=') + if k and v and sep: + env[k] = v + ans = MappingProxyType(env) + else: + log_error('Failed to run shell to read its environment') return ans +def read_shell_environment(opts: Options | None = None) -> MappingProxyType[str, str]: + shell = resolved_shell(opts) + return read_resolved_shell_environment(tuple(shell)) + + def parse_uri_list(text: str) -> Generator[str, None, None]: ' Get paths from file:// URLs ' from urllib.parse import unquote, urlparse