diff --git a/src/octoprint/util/commandline.py b/src/octoprint/util/commandline.py index 1b38de89..a074d466 100644 --- a/src/octoprint/util/commandline.py +++ b/src/octoprint/util/commandline.py @@ -8,6 +8,39 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms import sarge import logging +import re + + +# These regexes are based on the colorama package +# Author: Jonathan Hartley +# License: BSD-3 (https://github.com/tartley/colorama/blob/master/LICENSE.txt) +# Website: https://github.com/tartley/colorama/ +_ANSI_CSI_PATTERN = "\001?\033\[(\??(?:\d|;)*)([a-zA-Z])\002?" # Control Sequence Introducer +_ANSI_OSC_PATTERN = "\001?\033\]((?:.|;)*?)(\x07)\002?" # Operating System Command +_ANSI_REGEX = re.compile("|".join([_ANSI_CSI_PATTERN, + _ANSI_OSC_PATTERN])) + + +def clean_ansi(line): + """ + Removes ANSI control codes from ``line``. + + Parameters: + line (str or unicode): the line to process + + Returns: + (str or unicode) The line without any ANSI control codes + + Example:: + + >>> text = "Some text with some \x1b[31mred words\x1b[39m in it" + >>> clean_ansi(text) + 'Some text with some red words in it' + >>> text = "We \x1b[?25lhide the cursor here and then \x1b[?25hshow it again here" + >>> clean_ansi(text) + 'We hide the cursor here and then show it again here' + """ + return _ANSI_REGEX.sub("", line) class CommandlineError(Exception): @@ -53,11 +86,13 @@ class CommandlineCaller(object): while p.returncode is None: line = p.stderr.readline(timeout=0.5) if line: + line = self._preprocess_lines(line) self._log_stderr(line) all_stderr.append(line) line = p.stdout.readline(timeout=0.5) if line: + line = self._preprocess_lines(line) self._log_stdout(line) all_stdout.append(line) @@ -68,13 +103,14 @@ class CommandlineCaller(object): stderr = p.stderr.text if stderr: - split_lines = stderr.split("\n") + split_lines = self._preprocess_lines( + stderr.split("\n")) self._log_stderr(*split_lines) all_stderr += split_lines stdout = p.stdout.text if stdout: - split_lines = stdout.split("\n") + split_lines = self._preprocess_lines(stdout.split("\n")) self._log_stdout(*split_lines) all_stdout += split_lines @@ -85,3 +121,6 @@ class CommandlineCaller(object): def _log_stderr(self, *lines): self.on_log_stderr(*lines) + + def _preprocess_lines(self, lines): + return lines diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py index b27e5fce..083741d1 100644 --- a/src/octoprint/util/pip.py +++ b/src/octoprint/util/pip.py @@ -9,12 +9,12 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms import sarge import sys import logging -import re import site import pkg_resources -from .commandline import CommandlineCaller +from .commandline import CommandlineCaller, clean_ansi +from octoprint.util import to_unicode _cache = dict(version=dict(), setup=dict()) @@ -258,7 +258,7 @@ class PipCaller(CommandlineCaller): self._logger.warn("Error while trying to run pip --version: {}".format(p.stderr.text)) return None, None - output = p.stdout.text + output = PipCaller._preprocess(p.stdout.text) # output should look something like this: # # pip from () @@ -363,6 +363,28 @@ class PipCaller(CommandlineCaller): self._logger.debug("Could not detect desired output from testballoon install, got this instead: {!r}".format(data)) return False, False, False, None + def _preprocess_lines(self, lines): + return map(self._preprocess, lines) + + @staticmethod + def _preprocess(text): + """ + Strips ANSI and VT100 cursor control characters from line and makes sure it's a unicode. + + Parameters: + text (str or unicode): The text to process + + Returns: + (unicode) The processed text as a unicode, stripped of ANSI and VT100 cursor show/hide codes + + Example:: + + >>> text = b'some text with some\x1b[?25h ANSI codes for \x1b[31mred words\x1b[39m and\x1b[?25l also some cursor control codes' + >>> PipCaller._preprocess(text) + u'some text with some ANSI codes for red words and also some cursor control codes' + """ + return to_unicode(clean_ansi(text)) + class LocalPipCaller(PipCaller): """ The LocalPipCaller always uses the pip instance associated with