From fcc2a1a40a6e07a76cc653352c052e810df0546f Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 3 May 2015 12:46:01 +0200 Subject: [PATCH] #128 #69 add support of shell specific actions, add alias expansion for bash and zsh --- tests/test_main.py | 5 +++ tests/test_shells.py | 53 +++++++++++++++++++++++++++++ thefuck/main.py | 5 +-- thefuck/shells.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 tests/test_shells.py create mode 100644 thefuck/shells.py diff --git a/tests/test_main.py b/tests/test_main.py index 68912cfe..a871c76a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -50,6 +50,11 @@ class TestGetCommand(object): monkeypatch.setattr('thefuck.main.os.environ', {}) monkeypatch.setattr('thefuck.main.wait_output', lambda *_: True) + @pytest.fixture(autouse=True) + def generic_shell(self, monkeypatch): + monkeypatch.setattr('thefuck.shells.from_shell', lambda x: x) + monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x) + def test_get_command_calls(self, Popen): assert main.get_command(Mock(), Mock(), ['thefuck', 'apt-get', 'search', 'vim']) \ diff --git a/tests/test_shells.py b/tests/test_shells.py new file mode 100644 index 00000000..c24112b3 --- /dev/null +++ b/tests/test_shells.py @@ -0,0 +1,53 @@ +import pytest +from mock import Mock +from thefuck import shells + + +class TestGeneric(object): + def test_from_shell(self): + assert shells.Generic().from_shell('pwd') == 'pwd' + + def test_to_shell(self): + assert shells.Bash().to_shell('pwd') == 'pwd' + + +class TestBash(object): + @pytest.fixture(autouse=True) + def Popen(self, monkeypatch): + mock = Mock() + monkeypatch.setattr('thefuck.shells.Popen', mock) + mock.return_value.stdout.read.return_value = ( + b'alias l=\'ls -CF\'\n' + b'alias la=\'ls -A\'\n' + b'alias ll=\'ls -alF\'') + return mock + + @pytest.mark.parametrize('before, after', [ + ('pwd', 'pwd'), + ('ll', 'ls -alF')]) + def test_from_shell(self, before, after): + assert shells.Bash().from_shell(before) == after + + def test_to_shell(self): + assert shells.Bash().to_shell('pwd') == 'pwd' + + +class TestZsh(object): + @pytest.fixture(autouse=True) + def Popen(self, monkeypatch): + mock = Mock() + monkeypatch.setattr('thefuck.shells.Popen', mock) + mock.return_value.stdout.read.return_value = ( + b'l=\'ls -CF\'\n' + b'la=\'ls -A\'\n' + b'll=\'ls -alF\'') + return mock + + @pytest.mark.parametrize('before, after', [ + ('pwd', 'pwd'), + ('ll', 'ls -alF')]) + def test_from_shell(self, before, after): + assert shells.Zsh().from_shell(before) == after + + def test_to_shell(self): + assert shells.Zsh().to_shell('pwd') == 'pwd' \ No newline at end of file diff --git a/thefuck/main.py b/thefuck/main.py index de1c425b..8a2fd3a1 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -7,7 +7,7 @@ import sys from psutil import Process, TimeoutExpired import colorama from .history import History -from . import logs, conf, types +from . import logs, conf, types, shells def setup_user_dir(): @@ -73,6 +73,7 @@ def get_command(settings, history, args): if not script: return + script = shells.from_shell(script) history.update(last_command=script, last_fixed_command=None) result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, @@ -109,7 +110,7 @@ def confirm(new_command, side_effect, settings): def run_rule(rule, command, history, settings): """Runs command from rule for passed command.""" - new_command = rule.get_new_command(command, settings) + new_command = shells.to_shell(rule.get_new_command(command, settings)) if confirm(new_command, rule.side_effect, settings): if rule.side_effect: rule.side_effect(command, settings) diff --git a/thefuck/shells.py b/thefuck/shells.py new file mode 100644 index 00000000..b0fdc7ac --- /dev/null +++ b/thefuck/shells.py @@ -0,0 +1,80 @@ +"""Module with shell specific actions, each shell class should +implement `from_shell` and `to_shell` methods. + +""" +from collections import defaultdict +from subprocess import Popen, PIPE +import os +from psutil import Process + + +FNULL = open(os.devnull, 'w') + + +class Generic(object): + def _get_aliases(self): + return {} + + def _expand_aliases(self, command_script): + aliases = self._get_aliases() + binary = command_script.split(' ')[0] + if binary in aliases: + return command_script.replace(binary, aliases[binary], 1) + else: + return command_script + + def from_shell(self, command_script): + """Prepares command before running in app.""" + return self._expand_aliases(command_script) + + def to_shell(self, command_script): + """Prepares command for running in shell.""" + return command_script + + +class Bash(Generic): + def _parse_alias(self, alias): + name, value = alias.replace('alias ', '', 1).split('=', 1) + if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": + value = value[1:-1] + return name, value + + def _get_aliases(self): + proc = Popen('bash -ic alias', stdout=PIPE, stderr=FNULL, shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias) + + +class Zsh(Generic): + def _parse_alias(self, alias): + name, value = alias.split('=', 1) + if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": + value = value[1:-1] + return name, value + + def _get_aliases(self): + proc = Popen('zsh -ic alias', stdout=PIPE, stderr=FNULL, shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias) + + +shells = defaultdict(lambda: Generic(), { + 'bash': Bash(), + 'zsh': Zsh()}) + + +def _get_shell(): + shell = Process(os.getpid()).parent().cmdline()[0] + return shells[shell] + + +def from_shell(command): + return _get_shell().from_shell(command) + + +def to_shell(command): + return _get_shell().to_shell(command)