diff --git a/README.md b/README.md index 12f9649d..9c97084f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Magnificent app which corrects your previous console command, inspired by a [@liamosaur](https://twitter.com/liamosaur/) [tweet](https://twitter.com/liamosaur/status/506975850596536320). +The Fuck is too slow? [Try experimental instant mode!](#experimental-instant-mode) + [![gif with examples][examples-link]][examples-link] Few more examples: @@ -395,6 +397,23 @@ export THEFUCK_PRIORITY='no_command=9999:apt_get=100' export THEFUCK_HISTORY_LIMIT='2000' ``` +## Experimental instant mode + +By default The Fuck reruns a previous command and that takes time, +in instant mode The Fuck logs output with [script](https://en.wikipedia.org/wiki/Script_(Unix)) +and just reads the log. + +[![gif with instant mode][instant-mode-gif-link]][instant-mode-gif-link] + +At the moment only Python 3 with bash or zsh is supported. + +For enabling instant mode you need to add `--enable-experimental-instant-mode` +to alias initialization in your `.bashrc`, `.bash_profile` or `.zshrc` like: + +```bash +eval $(thefuck --alias --enable-experimental-instant-mode) +``` + ## Developing Install `The Fuck` for development: @@ -443,4 +462,5 @@ Project License can be found [here](LICENSE.md). [coverage-link]: https://coveralls.io/github/nvbn/thefuck [license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg [examples-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif +[instant-mode-gif-link]: https://raw.githubusercontent.com/nvbn/thefuck/682-instant-fuck-mode/example_instant_mode.gif [homebrew]: http://brew.sh/ diff --git a/example_instant_mode.gif b/example_instant_mode.gif new file mode 100644 index 00000000..43bfcc33 Binary files /dev/null and b/example_instant_mode.gif differ diff --git a/setup.py b/setup.py index 98d01f1b..8ea8288e 100755 --- a/setup.py +++ b/setup.py @@ -33,8 +33,9 @@ elif (3, 0) < version < (3, 3): VERSION = '3.21' -install_requires = ['psutil', 'colorama', 'six', 'decorator'] +install_requires = ['psutil', 'colorama', 'six', 'decorator', 'pyte'] extras_require = {':python_version<"3.4"': ['pathlib2'], + ':python_version<"3.3"': ['backports.shutil_get_terminal_size'], ":sys_platform=='win32'": ['win_unicode_console']} setup(name='thefuck', diff --git a/tests/test_argument_parser.py b/tests/test_argument_parser.py index 015449bd..d2eb7c27 100644 --- a/tests/test_argument_parser.py +++ b/tests/test_argument_parser.py @@ -6,7 +6,8 @@ from thefuck.const import ARGUMENT_PLACEHOLDER def _args(**override): args = {'alias': None, 'command': [], 'yes': False, 'help': False, 'version': False, 'debug': False, - 'force_command': None, 'repeat': False} + 'force_command': None, 'repeat': False, + 'enable_experimental_instant_mode': False} args.update(override) return args @@ -14,6 +15,8 @@ def _args(**override): @pytest.mark.parametrize('argv, result', [ (['thefuck'], _args()), (['thefuck', '-a'], _args(alias='fuck')), + (['thefuck', '--alias', '--enable-experimental-instant-mode'], + _args(alias='fuck', enable_experimental_instant_mode=True)), (['thefuck', '-a', 'fix'], _args(alias='fix')), (['thefuck', 'git', 'branch', ARGUMENT_PLACEHOLDER, '-y'], _args(command=['git', 'branch'], yes=True)), diff --git a/tests/test_types.py b/tests/test_types.py index 6b3d81ff..d9326d3e 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -110,13 +110,13 @@ class TestCommand(object): Popen = Mock() Popen.return_value.stdout.read.return_value = b'stdout' Popen.return_value.stderr.read.return_value = b'stderr' - monkeypatch.setattr('thefuck.types.Popen', Popen) + monkeypatch.setattr('thefuck.output_readers.rerun.Popen', Popen) return Popen @pytest.fixture(autouse=True) def prepare(self, monkeypatch): - monkeypatch.setattr('thefuck.types.Command._wait_output', - staticmethod(lambda *_: True)) + monkeypatch.setattr('thefuck.output_readers.rerun._wait_output', + lambda *_: True) def test_from_script_calls(self, Popen, settings, os_environ): settings.env = {} diff --git a/tests/test_ui.py b/tests/test_ui.py index 1c2a9933..1e539651 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -69,34 +69,40 @@ class TestSelectCommand(object): def test_without_confirmation(self, capsys, commands, settings): settings.require_confirmation = False assert ui.select_command(iter(commands)) == commands[0] - assert capsys.readouterr() == ('', 'ls\n') + assert capsys.readouterr() == ('', const.USER_COMMAND_MARK + 'ls\n') def test_without_confirmation_with_side_effects( self, capsys, commands_with_side_effect, settings): settings.require_confirmation = False assert (ui.select_command(iter(commands_with_side_effect)) == commands_with_side_effect[0]) - assert capsys.readouterr() == ('', 'ls (+side effect)\n') + assert capsys.readouterr() == ('', const.USER_COMMAND_MARK + 'ls (+side effect)\n') def test_with_confirmation(self, capsys, patch_get_key, commands): patch_get_key(['\n']) assert ui.select_command(iter(commands)) == commands[0] - assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') + assert capsys.readouterr() == ( + '', const.USER_COMMAND_MARK + u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') def test_with_confirmation_abort(self, capsys, patch_get_key, commands): patch_get_key([const.KEY_CTRL_C]) assert ui.select_command(iter(commands)) is None - assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n') + assert capsys.readouterr() == ( + '', const.USER_COMMAND_MARK + u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n') def test_with_confirmation_with_side_effct(self, capsys, patch_get_key, commands_with_side_effect): patch_get_key(['\n']) assert (ui.select_command(iter(commands_with_side_effect)) == commands_with_side_effect[0]) - assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n') + assert capsys.readouterr() == ( + '', const.USER_COMMAND_MARK + u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n') def test_with_confirmation_select_second(self, capsys, patch_get_key, commands): patch_get_key([const.KEY_DOWN, '\n']) assert ui.select_command(iter(commands)) == commands[1] - assert capsys.readouterr() == ( - '', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n') + stderr = ( + u'{mark}\x1b[1K\rls [enter/↑/↓/ctrl+c]' + u'{mark}\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n' + ).format(mark=const.USER_COMMAND_MARK) + assert capsys.readouterr() == ('', stderr) diff --git a/thefuck/argument_parser.py b/thefuck/argument_parser.py index 8776dd52..695111c1 100644 --- a/thefuck/argument_parser.py +++ b/thefuck/argument_parser.py @@ -25,6 +25,10 @@ class Parser(object): nargs='?', const=get_alias(), help='[custom-alias-name] prints alias for current shell') + self._parser.add_argument( + '--enable-experimental-instant-mode', + action='store_true', + help='enable experimental instant mode, use on your own risk') self._parser.add_argument( '-h', '--help', action='store_true', diff --git a/thefuck/conf.py b/thefuck/conf.py index e1ae8eb8..1f69faf8 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -98,7 +98,7 @@ class Settings(dict): elif attr in ('wait_command', 'history_limit', 'wait_slow_command'): return int(val) elif attr in ('require_confirmation', 'no_colors', 'debug', - 'alter_history'): + 'alter_history', 'instant_mode'): return val.lower() == 'true' elif attr == 'slow_commands': return val.split(':') diff --git a/thefuck/const.py b/thefuck/const.py index 9530a428..0cc0e7ef 100644 --- a/thefuck/const.py +++ b/thefuck/const.py @@ -35,6 +35,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'slow_commands': ['lein', 'react-native', 'gradle', './gradlew', 'vagrant'], 'repeat': False, + 'instant_mode': False, 'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}} ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', @@ -48,7 +49,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', 'THEFUCK_ALTER_HISTORY': 'alter_history', 'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command', 'THEFUCK_SLOW_COMMANDS': 'slow_commands', - 'THEFUCK_REPEAT': 'repeat'} + 'THEFUCK_REPEAT': 'repeat', + 'THEFUCK_INSTANT_MODE': 'instant_mode'} SETTINGS_HEADER = u"""# The Fuck settings file # @@ -65,3 +67,7 @@ SETTINGS_HEADER = u"""# The Fuck settings file ARGUMENT_PLACEHOLDER = 'THEFUCK_ARGUMENT_PLACEHOLDER' CONFIGURATION_TIMEOUT = 60 + +USER_COMMAND_MARK = u'\u200B' * 10 + +LOG_SIZE = 1000 diff --git a/thefuck/exceptions.py b/thefuck/exceptions.py index be888092..5fc5dcd6 100644 --- a/thefuck/exceptions.py +++ b/thefuck/exceptions.py @@ -4,3 +4,7 @@ class EmptyCommand(Exception): class NoRuleMatched(Exception): """Raised when no rule matched for some command.""" + + +class ScriptNotInLog(Exception): + """Script not found in log.""" diff --git a/thefuck/logs.py b/thefuck/logs.py index 42fa53c1..aab3aca2 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -6,6 +6,7 @@ import sys from traceback import format_exception import colorama from .conf import settings +from . import const def color(color_): @@ -16,6 +17,14 @@ def color(color_): return color_ +def warn(title): + sys.stderr.write(u'{warn}[WARN] {title}{reset}\n'.format( + warn=color(colorama.Back.RED + colorama.Fore.WHITE + + colorama.Style.BRIGHT), + reset=color(colorama.Style.RESET_ALL), + title=title)) + + def exception(title, exc_info): sys.stderr.write( u'{warn}[WARN] {title}:{reset}\n{trace}' @@ -39,7 +48,8 @@ def failed(msg): def show_corrected_command(corrected_command): - sys.stderr.write(u'{bold}{script}{reset}{side_effect}\n'.format( + sys.stderr.write(u'{prefix}{bold}{script}{reset}{side_effect}\n'.format( + prefix=const.USER_COMMAND_MARK, script=corrected_command.script, side_effect=u' (+side effect)' if corrected_command.side_effect else u'', bold=color(colorama.Style.BRIGHT), @@ -48,9 +58,10 @@ def show_corrected_command(corrected_command): def confirm_text(corrected_command): sys.stderr.write( - (u'{clear}{bold}{script}{reset}{side_effect} ' + (u'{prefix}{clear}{bold}{script}{reset}{side_effect} ' u'[{green}enter{reset}/{blue}↑{reset}/{blue}↓{reset}' u'/{red}ctrl+c{reset}]').format( + prefix=const.USER_COMMAND_MARK, script=corrected_command.script, side_effect=' (+side effect)' if corrected_command.side_effect else '', clear='\033[1K\r', diff --git a/thefuck/main.py b/thefuck/main.py index e02cc2ba..0416508d 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -5,6 +5,7 @@ init_output() from pprint import pformat # noqa: E402 import sys # noqa: E402 +import six # noqa: E402 from . import logs, types # noqa: E402 from .shells import shell # noqa: E402 from .conf import settings # noqa: E402 @@ -13,6 +14,7 @@ from .exceptions import EmptyCommand # noqa: E402 from .ui import select_command # noqa: E402 from .argument_parser import Parser # noqa: E402 from .utils import get_installation_info # noqa: E402 +from .logs import warn # noqa: E402 def fix_command(known_args): @@ -50,6 +52,15 @@ def main(): elif known_args.command: fix_command(known_args) elif known_args.alias: - print(shell.app_alias(known_args.alias)) + if known_args.enable_experimental_instant_mode: + if six.PY2: + warn("Instant mode not supported with Python 2") + alias = shell.app_alias(known_args.alias) + else: + alias = shell.instant_mode_alias(known_args.alias) + else: + alias = shell.app_alias(known_args.alias) + + print(alias) else: parser.print_usage() diff --git a/thefuck/output_readers/__init__.py b/thefuck/output_readers/__init__.py new file mode 100644 index 00000000..c07a8cb9 --- /dev/null +++ b/thefuck/output_readers/__init__.py @@ -0,0 +1,18 @@ +from ..conf import settings +from . import read_log, rerun + + +def get_output(script, expanded): + """Get output of the script. + + :param script: Console script. + :type script: str + :param expanded: Console script with expanded aliases. + :type expanded: str + :rtype: (str, str) + + """ + if settings.instant_mode: + return read_log.get_output(script) + else: + return rerun.get_output(script, expanded) diff --git a/thefuck/output_readers/read_log.py b/thefuck/output_readers/read_log.py new file mode 100644 index 00000000..23d80cbc --- /dev/null +++ b/thefuck/output_readers/read_log.py @@ -0,0 +1,82 @@ +import os +import shlex +try: + from shutil import get_terminal_size +except ImportError: + from backports.shutil_get_terminal_size import get_terminal_size +import six +import pyte +from ..exceptions import ScriptNotInLog +from ..logs import warn +from .. import const + + +def _group_by_calls(log): + script_line = None + lines = [] + for line in log: + try: + line = line.decode() + except UnicodeDecodeError: + continue + + if const.USER_COMMAND_MARK in line: + if script_line: + yield script_line, lines + + script_line = line + lines = [line] + elif script_line is not None: + lines.append(line) + + if script_line: + yield script_line, lines + + +def _get_script_group_lines(grouped, script): + parts = shlex.split(script) + + for script_line, lines in reversed(grouped): + if all(part in script_line for part in parts): + return lines + + raise ScriptNotInLog + + +def _get_output_lines(script, log_file): + lines = log_file.readlines()[-const.LOG_SIZE:] + grouped = list(_group_by_calls(lines)) + script_lines = _get_script_group_lines(grouped, script) + + screen = pyte.Screen(get_terminal_size().columns, len(script_lines)) + stream = pyte.Stream(screen) + stream.feed(''.join(script_lines)) + return screen.display + + +def get_output(script): + """Reads script output from log. + + :type script: str + :rtype: (str, str) + + """ + if six.PY2: + warn('Experimental instant mode is Python 3+ only') + return None, None + + if 'THEFUCK_OUTPUT_LOG' not in os.environ: + warn("Output log isn't specified") + return None, None + + try: + with open(os.environ['THEFUCK_OUTPUT_LOG'], 'rb') as log_file: + lines = _get_output_lines(script, log_file) + output = '\n'.join(lines).strip() + return output, output + except OSError: + warn("Can't read output log") + return None, None + except ScriptNotInLog: + warn("Script not found in output log") + return None, None diff --git a/thefuck/output_readers/rerun.py b/thefuck/output_readers/rerun.py new file mode 100644 index 00000000..571dd3af --- /dev/null +++ b/thefuck/output_readers/rerun.py @@ -0,0 +1,57 @@ +import os +import shlex +from subprocess import Popen, PIPE +from psutil import Process, TimeoutExpired +from .. import logs +from ..conf import settings + + +def _wait_output(popen, is_slow): + """Returns `True` if we can get output of the command in the + `settings.wait_command` time. + + Command will be killed if it wasn't finished in the time. + + :type popen: Popen + :rtype: bool + + """ + proc = Process(popen.pid) + try: + proc.wait(settings.wait_slow_command if is_slow + else settings.wait_command) + return True + except TimeoutExpired: + for child in proc.children(recursive=True): + child.kill() + proc.kill() + return False + + +def get_output(script, expanded): + """Runs the script and obtains stdin/stderr. + + :type script: str + :type expanded: str + :rtype: (str, str) + + """ + env = dict(os.environ) + env.update(settings.env) + + is_slow = shlex.split(expanded) in settings.slow_commands + with logs.debug_time(u'Call: {}; with env: {}; is slow: '.format( + script, env, is_slow)): + result = Popen(expanded, shell=True, stdin=PIPE, + stdout=PIPE, stderr=PIPE, env=env) + if _wait_output(result, is_slow): + stdout = result.stdout.read().decode('utf-8') + stderr = result.stderr.read().decode('utf-8') + + logs.debug(u'Received stdout: {}'.format(stdout)) + logs.debug(u'Received stderr: {}'.format(stderr)) + + return stdout, stderr + else: + logs.debug(u'Execution timed out!') + return None, None diff --git a/thefuck/rules/git_push.py b/thefuck/rules/git_push.py index 85f59fe1..1f8e7626 100644 --- a/thefuck/rules/git_push.py +++ b/thefuck/rules/git_push.py @@ -1,3 +1,4 @@ +import re from thefuck.utils import replace_argument from thefuck.specific.git import git_support @@ -32,5 +33,6 @@ def get_new_command(command): if len(command_parts) > upstream_option_index: command_parts.pop(upstream_option_index) - push_upstream = command.stderr.split('\n')[-3].strip().partition('git ')[2] - return replace_argument(" ".join(command_parts), 'push', push_upstream) + arguments = re.findall(r'git push (.*)', command.stderr)[0].strip() + return replace_argument(" ".join(command_parts), 'push', + 'push {}'.format(arguments)) diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index b347decc..52eb06ef 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -1,6 +1,7 @@ import os +from uuid import uuid4 from ..conf import settings -from ..const import ARGUMENT_PLACEHOLDER +from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK from ..utils import memoize from .generic import Generic @@ -27,6 +28,21 @@ class Bash(Generic): alter_history=('history -s $TF_CMD;' if settings.alter_history else '')) + def instant_mode_alias(self, alias_name): + if os.environ.get('THEFUCK_INSTANT_MODE', '').lower() == 'true': + return ''' + export PS1="{user_command_mark}$PS1"; + {app_alias} + '''.format(user_command_mark=USER_COMMAND_MARK, + app_alias=self.app_alias(alias_name)) + else: + return ''' + export THEFUCK_INSTANT_MODE=True; + export THEFUCK_OUTPUT_LOG={log}; + script -feq {log}; + exit + '''.format(log='/tmp/thefuck-script-log-{}'.format(uuid4().hex)) + def _parse_alias(self, alias): name, value = alias.replace('alias ', '', 1).split('=', 1) if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 6d777461..f0a1f4f3 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -18,7 +18,7 @@ class Fish(Generic): default.add(alias.strip()) return default - def app_alias(self, fuck): + def app_alias(self, alias_name): if settings.alter_history: alter_history = (' builtin history delete --exact' ' --case-sensitive -- $fucked_up_command\n' @@ -33,7 +33,7 @@ class Fish(Generic): ' if [ "$unfucked_command" != "" ]\n' ' eval $unfucked_command\n{1}' ' end\n' - 'end').format(fuck, alter_history) + 'end').format(alias_name, alter_history) @memoize @cache('.config/fish/config.fish', '.config/fish/functions') diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index fb190fb8..a810d8a7 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -3,6 +3,7 @@ import os import shlex import six from collections import namedtuple +from ..logs import warn from ..utils import memoize from ..conf import settings from ..system import Path @@ -32,9 +33,13 @@ class Generic(object): """Prepares command for running in shell.""" return command_script - def app_alias(self, fuck): + def app_alias(self, alias_name): return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \ - "thefuck $(fc -ln -1))'".format(fuck) + "thefuck $(fc -ln -1))'".format(alias_name) + + def instant_mode_alias(self, alias_name): + warn("Instant mode not supported by your shell") + return self.app_alias(alias_name) def _get_history_file_name(self): return '' diff --git a/thefuck/shells/powershell.py b/thefuck/shells/powershell.py index 938b3d4e..17477805 100644 --- a/thefuck/shells/powershell.py +++ b/thefuck/shells/powershell.py @@ -2,8 +2,8 @@ from .generic import Generic, ShellConfiguration class Powershell(Generic): - def app_alias(self, fuck): - return 'function ' + fuck + ' {\n' \ + def app_alias(self, alias_name): + return 'function ' + alias_name + ' {\n' \ ' $history = (Get-History -Count 1).CommandLine;\n' \ ' if (-not [string]::IsNullOrWhiteSpace($history)) {\n' \ ' $fuck = $(thefuck $history);\n' \ diff --git a/thefuck/shells/tcsh.py b/thefuck/shells/tcsh.py index b1967a9b..d8470eba 100644 --- a/thefuck/shells/tcsh.py +++ b/thefuck/shells/tcsh.py @@ -6,10 +6,10 @@ from .generic import Generic class Tcsh(Generic): - def app_alias(self, fuck): + def app_alias(self, alias_name): return ("alias {0} 'setenv TF_ALIAS {0} && " "set fucked_cmd=`history -h 2 | head -n 1` && " - "eval `thefuck ${{fucked_cmd}}`'").format(fuck) + "eval `thefuck ${{fucked_cmd}}`'").format(alias_name) def _parse_alias(self, alias): name, value = alias.split("\t", 1) diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py index f0e92085..aafbd00a 100644 --- a/thefuck/shells/zsh.py +++ b/thefuck/shells/zsh.py @@ -1,7 +1,8 @@ from time import time import os +from uuid import uuid4 from ..conf import settings -from ..const import ARGUMENT_PLACEHOLDER +from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK from ..utils import memoize from .generic import Generic @@ -26,6 +27,21 @@ class Zsh(Generic): alter_history=('test -n "$TF_CMD" && print -s $TF_CMD' if settings.alter_history else '')) + def instant_mode_alias(self, alias_name): + if os.environ.get('THEFUCK_INSTANT_MODE', '').lower() == 'true': + return ''' + export PS1="{user_command_mark}$PS1"; + {app_alias} + '''.format(user_command_mark=USER_COMMAND_MARK, + app_alias=self.app_alias(alias_name)) + else: + return ''' + export THEFUCK_INSTANT_MODE=True; + export THEFUCK_OUTPUT_LOG={log}; + script -feq {log}; + exit + '''.format(log='/tmp/thefuck-script-log-{}'.format(uuid4().hex)) + def _parse_alias(self, alias): name, value = alias.split('=', 1) if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": diff --git a/thefuck/types.py b/thefuck/types.py index 71d6e493..5fa7b3d7 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -1,15 +1,13 @@ from imp import load_source -from subprocess import Popen, PIPE import os import sys -import six -from psutil import Process, TimeoutExpired from . import logs from .shells import shell from .conf import settings from .const import DEFAULT_PRIORITY, ALL_ENABLED from .exceptions import EmptyCommand -from .utils import get_alias +from .utils import get_alias, format_raw_script +from .output_readers import get_output class Command(object): @@ -61,44 +59,6 @@ class Command(object): kwargs.setdefault('stderr', self.stderr) return Command(**kwargs) - @staticmethod - def _wait_output(popen, is_slow): - """Returns `True` if we can get output of the command in the - `settings.wait_command` time. - - Command will be killed if it wasn't finished in the time. - - :type popen: Popen - :rtype: bool - - """ - proc = Process(popen.pid) - try: - proc.wait(settings.wait_slow_command if is_slow - else settings.wait_command) - return True - except TimeoutExpired: - for child in proc.children(recursive=True): - child.kill() - proc.kill() - return False - - @staticmethod - def _prepare_script(raw_script): - """Creates single script from a list of script parts. - - :type raw_script: [basestring] - :rtype: basestring - - """ - if six.PY2: - script = ' '.join(arg.decode('utf-8') for arg in raw_script) - else: - script = ' '.join(raw_script) - - script = script.strip() - return shell.from_shell(script) - @classmethod def from_raw_script(cls, raw_script): """Creates instance of `Command` from a list of script parts. @@ -108,29 +68,13 @@ class Command(object): :raises: EmptyCommand """ - script = cls._prepare_script(raw_script) + script = format_raw_script(raw_script) if not script: raise EmptyCommand - env = dict(os.environ) - env.update(settings.env) - - is_slow = script.split(' ')[0] in settings.slow_commands - with logs.debug_time(u'Call: {}; with env: {}; is slow: '.format( - script, env, is_slow)): - result = Popen(script, shell=True, stdin=PIPE, - stdout=PIPE, stderr=PIPE, env=env) - if cls._wait_output(result, is_slow): - stdout = result.stdout.read().decode('utf-8') - stderr = result.stderr.read().decode('utf-8') - - logs.debug(u'Received stdout: {}'.format(stdout)) - logs.debug(u'Received stderr: {}'.format(stderr)) - - return cls(script, stdout, stderr) - else: - logs.debug(u'Execution timed out!') - return cls(script, None, None) + expanded = shell.from_shell(script) + stdout, stderr = get_output(script, expanded) + return cls(expanded, stdout, stderr) class Rule(object): diff --git a/thefuck/utils.py b/thefuck/utils.py index b9d4d3af..ae56b963 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -7,7 +7,7 @@ from contextlib import closing from decorator import decorator from difflib import get_close_matches from functools import wraps -from warnings import warn +from .logs import warn from .conf import settings from .system import Path @@ -282,3 +282,18 @@ def get_valid_history_without_current(command): return [line for line in _not_corrected(history, tf_alias) if not line.startswith(tf_alias) and not line == command.script and line.split(' ')[0] in executables] + + +def format_raw_script(raw_script): + """Creates single script from a list of script parts. + + :type raw_script: [basestring] + :rtype: basestring + + """ + if six.PY2: + script = ' '.join(arg.decode('utf-8') for arg in raw_script) + else: + script = ' '.join(raw_script) + + return script.strip()