diff --git a/click/_compat.py b/click/_compat.py index 30573277f..3d1355e0b 100644 --- a/click/_compat.py +++ b/click/_compat.py @@ -9,6 +9,9 @@ PY2 = sys.version_info[0] == 2 +_ansi_re = re.compile('\033\[((?:\d|;)*)([a-zA-Z])') + + def _make_text_stream(stream, encoding, errors): if encoding is None: encoding = get_best_encoding(stream) @@ -427,7 +430,7 @@ def __repr__(self): return repr(self._f) -_auto_wrap_for_ansi = lambda x: x +auto_wrap_for_ansi = None try: @@ -438,14 +441,11 @@ def __repr__(self): from weakref import WeakKeyDictionary _ansi_stream_wrappers = WeakKeyDictionary() - def _auto_wrap_for_ansi(stream): + def auto_wrap_for_ansi(stream): cached = _ansi_stream_wrappers.get(stream) if cached is not None: return cached - try: - strip = not stream.isatty() - except Exception: - strip = True + strip = not isatty(stream) rv = colorama.AnsiToWin32(stream, strip=strip).stream try: _ansi_stream_wrappers[stream] = rv @@ -454,6 +454,17 @@ def _auto_wrap_for_ansi(stream): return rv +def strip_ansi(value): + return _ansi_re.sub('', value) + + +def isatty(stream): + try: + return stream.isatty() + except Exception: + return False + + binary_streams = { 'stdin': get_binary_stdin, 'stdout': get_binary_stdout, diff --git a/click/_termui_impl.py b/click/_termui_impl.py index 0bf54a8f3..074966316 100644 --- a/click/_termui_impl.py +++ b/click/_termui_impl.py @@ -10,9 +10,11 @@ :license: BSD, see LICENSE for more details. """ import os +import sys import time import math -from ._compat import get_text_stdout, range_type, PY2 +from ._compat import get_text_stdout, range_type, PY2, isatty, open_stream, \ + strip_ansi from .utils import echo @@ -224,3 +226,60 @@ def next(self): if not PY2: __next__ = next del next + + +def pager(text): + """Decide what method to use for paging through text.""" + stdout = get_text_stdout() + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, text) + if 'PAGER' in os.environ: + if sys.platform == 'win32': + return _tempfilepager(strip_ansi(text), os.environ['PAGER']) + elif os.environ.get('TERM') in ('dumb', 'emacs'): + return _pipepager(strip_ansi(text), os.environ['PAGER']) + else: + return _pipepager(text, os.environ['PAGER']) + if os.environ.get('TERM') in ('dumb', 'emacs'): + return _nullpager(stdout, text) + if sys.platform == 'win32' or sys.platform.startswith('os2'): + return _tempfilepager(strip_ansi(text), 'more <') + if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: + return _pipepager(text, 'less') + + import tempfile + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: + return _pipepager(text, 'more') + return _nullpager(stdout, text) + finally: + os.unlink(filename) + + +def _pipepager(text, cmd): + """Page through text by feeding it to another program.""" + pipe = os.popen(cmd, 'w') + try: + pipe.write(text) + pipe.close() + except IOError: + pass + + +def _tempfilepager(text, cmd): + """Page through text by invoking a program on a temporary file.""" + import tempfile + filename = tempfile.mktemp() + with open_stream(filename, 'w')[1] as f: + f.write(text) + try: + os.system(cmd + ' "' + filename + '"') + finally: + os.unlink(filename) + + +def _nullpager(stream, text): + """Simply print unformatted text. This is the ultimate fallback.""" + stream.write(strip_ansi(text)) diff --git a/click/termui.py b/click/termui.py index a3ce92c31..f57611c51 100644 --- a/click/termui.py +++ b/click/termui.py @@ -175,12 +175,8 @@ def echo_via_pager(text): encoding = get_best_encoding(sys.stdout) text = text.encode(encoding, 'replace') - # Pydoc's pager is badly broken with LANG=C on Python 3 to the point - # where it will corrupt the terminal. http://bugs.python.org/issue21398 - # I don't feel like reimplementing it given that it works on Python 2 - # and seems reasonably stable otherwise. - import pydoc - pydoc.pager(text) + from ._termui_impl import pager + return pager(text + '\n') def progressbar(iterable=None, length=None, label=None, show_eta=True, diff --git a/click/utils.py b/click/utils.py index 2ff45c827..f13e2baae 100644 --- a/click/utils.py +++ b/click/utils.py @@ -4,7 +4,7 @@ from ._compat import text_type, open_stream, get_streerror, string_types, \ PY2, get_best_encoding, binary_streams, text_streams, filename_to_ui, \ - _auto_wrap_for_ansi + auto_wrap_for_ansi, strip_ansi, isatty if not PY2: from ._compat import _find_binary_writer @@ -223,7 +223,6 @@ def echo(message=None, file=None, nl=True): """ if file is None: file = sys.stdout - file = _auto_wrap_for_ansi(file) if message is not None and not isinstance(message, echo_native_types): message = text_type(message) @@ -242,6 +241,16 @@ def echo(message=None, file=None, nl=True): binary_file.write(b'\n') binary_file.flush() return + + # If we have colorama support we wrap the stream to handle colors + # for us. In case colorama is not supported and our output stream + # is not a terminal, we strip the ansi codes ourselves. + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file) + elif message and not isatty(file): + message = strip_ansi(message) + + if message: file.write(message) if nl: file.write('\n') diff --git a/tests/test_utils.py b/tests/test_utils.py index dd3d786a0..438de0015 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,8 +7,9 @@ def test_echo(runner): click.echo(b'\x44\x44') click.echo(42, nl=False) click.echo(b'a', nl=False) + click.echo('\x1b[31mx\x1b[39m', nl=False) bytes = out.getvalue() - assert bytes == b'\xe2\x98\x83\nDD\n42a' + assert bytes == b'\xe2\x98\x83\nDD\n42ax' def test_filename_formatting():