mirror of
https://github.com/python/cpython.git
synced 2024-11-24 00:38:00 +01:00
426569eb8c
Debian (and derivatives) provide a /usr/bin/pager binary, managed by the alternatives system, that always points to an available pager utility. Allow _pyrepl to use it, to follow system policy. This is a very trivial change, from a patch that Debian has been carrying since 2.7 era. Seems appropriate to upstream. https://bugs.debian.org/799555
176 lines
5.7 KiB
Python
176 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
|
|
# types
|
|
if False:
|
|
from typing import Protocol
|
|
class Pager(Protocol):
|
|
def __call__(self, text: str, title: str = "") -> None:
|
|
...
|
|
|
|
|
|
def get_pager() -> Pager:
|
|
"""Decide what method to use for paging through text."""
|
|
if not hasattr(sys.stdin, "isatty"):
|
|
return plain_pager
|
|
if not hasattr(sys.stdout, "isatty"):
|
|
return plain_pager
|
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
return plain_pager
|
|
if sys.platform == "emscripten":
|
|
return plain_pager
|
|
use_pager = os.environ.get('MANPAGER') or os.environ.get('PAGER')
|
|
if use_pager:
|
|
if sys.platform == 'win32': # pipes completely broken in Windows
|
|
return lambda text, title='': tempfile_pager(plain(text), use_pager)
|
|
elif os.environ.get('TERM') in ('dumb', 'emacs'):
|
|
return lambda text, title='': pipe_pager(plain(text), use_pager, title)
|
|
else:
|
|
return lambda text, title='': pipe_pager(text, use_pager, title)
|
|
if os.environ.get('TERM') in ('dumb', 'emacs'):
|
|
return plain_pager
|
|
if sys.platform == 'win32':
|
|
return lambda text, title='': tempfile_pager(plain(text), 'more <')
|
|
if hasattr(os, 'system') and os.system('(pager) 2>/dev/null') == 0:
|
|
return lambda text, title='': pipe_pager(text, 'pager', title)
|
|
if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0:
|
|
return lambda text, title='': pipe_pager(text, 'less', title)
|
|
|
|
import tempfile
|
|
(fd, filename) = tempfile.mkstemp()
|
|
os.close(fd)
|
|
try:
|
|
if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0:
|
|
return lambda text, title='': pipe_pager(text, 'more', title)
|
|
else:
|
|
return tty_pager
|
|
finally:
|
|
os.unlink(filename)
|
|
|
|
|
|
def escape_stdout(text: str) -> str:
|
|
# Escape non-encodable characters to avoid encoding errors later
|
|
encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8'
|
|
return text.encode(encoding, 'backslashreplace').decode(encoding)
|
|
|
|
|
|
def escape_less(s: str) -> str:
|
|
return re.sub(r'([?:.%\\])', r'\\\1', s)
|
|
|
|
|
|
def plain(text: str) -> str:
|
|
"""Remove boldface formatting from text."""
|
|
return re.sub('.\b', '', text)
|
|
|
|
|
|
def tty_pager(text: str, title: str = '') -> None:
|
|
"""Page through text on a text terminal."""
|
|
lines = plain(escape_stdout(text)).split('\n')
|
|
has_tty = False
|
|
try:
|
|
import tty
|
|
import termios
|
|
fd = sys.stdin.fileno()
|
|
old = termios.tcgetattr(fd)
|
|
tty.setcbreak(fd)
|
|
has_tty = True
|
|
|
|
def getchar() -> str:
|
|
return sys.stdin.read(1)
|
|
|
|
except (ImportError, AttributeError, io.UnsupportedOperation):
|
|
def getchar() -> str:
|
|
return sys.stdin.readline()[:-1][:1]
|
|
|
|
try:
|
|
try:
|
|
h = int(os.environ.get('LINES', 0))
|
|
except ValueError:
|
|
h = 0
|
|
if h <= 1:
|
|
h = 25
|
|
r = inc = h - 1
|
|
sys.stdout.write('\n'.join(lines[:inc]) + '\n')
|
|
while lines[r:]:
|
|
sys.stdout.write('-- more --')
|
|
sys.stdout.flush()
|
|
c = getchar()
|
|
|
|
if c in ('q', 'Q'):
|
|
sys.stdout.write('\r \r')
|
|
break
|
|
elif c in ('\r', '\n'):
|
|
sys.stdout.write('\r \r' + lines[r] + '\n')
|
|
r = r + 1
|
|
continue
|
|
if c in ('b', 'B', '\x1b'):
|
|
r = r - inc - inc
|
|
if r < 0: r = 0
|
|
sys.stdout.write('\n' + '\n'.join(lines[r:r+inc]) + '\n')
|
|
r = r + inc
|
|
|
|
finally:
|
|
if has_tty:
|
|
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
|
|
|
|
|
|
def plain_pager(text: str, title: str = '') -> None:
|
|
"""Simply print unformatted text. This is the ultimate fallback."""
|
|
sys.stdout.write(plain(escape_stdout(text)))
|
|
|
|
|
|
def pipe_pager(text: str, cmd: str, title: str = '') -> None:
|
|
"""Page through text by feeding it to another program."""
|
|
import subprocess
|
|
env = os.environ.copy()
|
|
if title:
|
|
title += ' '
|
|
esc_title = escape_less(title)
|
|
prompt_string = (
|
|
f' {esc_title}' +
|
|
'?ltline %lt?L/%L.'
|
|
':byte %bB?s/%s.'
|
|
'.'
|
|
'?e (END):?pB %pB\\%..'
|
|
' (press h for help or q to quit)')
|
|
env['LESS'] = '-RmPm{0}$PM{0}$'.format(prompt_string)
|
|
proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
|
|
errors='backslashreplace', env=env)
|
|
assert proc.stdin is not None
|
|
try:
|
|
with proc.stdin as pipe:
|
|
try:
|
|
pipe.write(text)
|
|
except KeyboardInterrupt:
|
|
# We've hereby abandoned whatever text hasn't been written,
|
|
# but the pager is still in control of the terminal.
|
|
pass
|
|
except OSError:
|
|
pass # Ignore broken pipes caused by quitting the pager program.
|
|
while True:
|
|
try:
|
|
proc.wait()
|
|
break
|
|
except KeyboardInterrupt:
|
|
# Ignore ctl-c like the pager itself does. Otherwise the pager is
|
|
# left running and the terminal is in raw mode and unusable.
|
|
pass
|
|
|
|
|
|
def tempfile_pager(text: str, cmd: str, title: str = '') -> None:
|
|
"""Page through text by invoking a program on a temporary file."""
|
|
import tempfile
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
filename = os.path.join(tempdir, 'pydoc.out')
|
|
with open(filename, 'w', errors='backslashreplace',
|
|
encoding=os.device_encoding(0) if
|
|
sys.platform == 'win32' else None
|
|
) as file:
|
|
file.write(text)
|
|
os.system(cmd + ' "' + filename + '"')
|