diff --git a/tests/shells/__init__.py b/tests/shells/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/shells/conftest.py b/tests/shells/conftest.py new file mode 100644 index 0000000..8fcbced --- /dev/null +++ b/tests/shells/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.fixture +def builtins_open(mocker): + return mocker.patch('six.moves.builtins.open') + + +@pytest.fixture +def isfile(mocker): + return mocker.patch('os.path.isfile', return_value=True) + + +@pytest.fixture +@pytest.mark.usefixtures('isfile') +def history_lines(mocker): + def aux(lines): + mock = mocker.patch('io.open') + mock.return_value.__enter__ \ + .return_value.readlines.return_value = lines + return aux diff --git a/tests/shells/test_bash.py b/tests/shells/test_bash.py new file mode 100644 index 0000000..d4b57c7 --- /dev/null +++ b/tests/shells/test_bash.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +import pytest +from thefuck.shells import Bash + + +@pytest.mark.usefixtures('isfile') +class TestBash(object): + @pytest.fixture + def shell(self): + return Bash() + + @pytest.fixture(autouse=True) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.bash.Popen') + mock.return_value.stdout.read.return_value = ( + b'alias fuck=\'eval $(thefuck $(fc -ln -1))\'\n' + 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'), + ('fuck', 'eval $(thefuck $(fc -ln -1))'), + ('awk', 'awk'), + ('ll', 'ls -alF')]) + def test_from_shell(self, before, after, shell): + assert shell.from_shell(before) == after + + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' + + @pytest.mark.parametrize('entry, entry_utf8', [ + ('ls', 'ls\n'), + (u'echo café', 'echo café\n')]) + def test_put_to_history(self, entry, entry_utf8, builtins_open, shell): + shell.put_to_history(entry) + builtins_open.return_value.__enter__.return_value. \ + write.assert_called_once_with(entry_utf8) + + def test_and_(self, shell): + assert shell.and_('ls', 'cd') == 'ls && cd' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))', + 'l': 'ls -CF', + 'la': 'ls -A', + 'll': 'ls -alF'} + + def test_app_alias(self, shell): + assert 'alias fuck' in shell.app_alias('fuck') + assert 'alias FUCK' in shell.app_alias('FUCK') + assert 'thefuck' in shell.app_alias('fuck') + assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') + assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') + + def test_get_history(self, history_lines, shell): + history_lines(['ls', 'rm']) + assert list(shell.get_history()) == ['ls', 'rm'] diff --git a/tests/shells/test_fish.py b/tests/shells/test_fish.py new file mode 100644 index 0000000..2e1a296 --- /dev/null +++ b/tests/shells/test_fish.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +import pytest +from thefuck.shells import Fish + + +@pytest.mark.usefixtures('isfile') +class TestFish(object): + @pytest.fixture + def shell(self): + return Fish() + + @pytest.fixture(autouse=True) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.fish.Popen') + mock.return_value.stdout.read.return_value = ( + b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n' + b'man\nmath\npopd\npushd\nruby') + return mock + + @pytest.fixture + def environ(self, monkeypatch): + data = {'TF_OVERRIDDEN_ALIASES': 'cd, ls, man, open'} + monkeypatch.setattr('thefuck.shells.fish.os.environ', data) + return data + + @pytest.mark.usefixture('environ') + def test_get_overridden_aliases(self, shell, environ): + assert shell._get_overridden_aliases() == ['cd', 'ls', 'man', 'open'] + + @pytest.mark.parametrize('before, after', [ + ('cd', 'cd'), + ('pwd', 'pwd'), + ('fuck', 'fish -ic "fuck"'), + ('find', 'find'), + ('funced', 'fish -ic "funced"'), + ('grep', 'grep'), + ('awk', 'awk'), + ('math "2 + 2"', r'fish -ic "math \"2 + 2\""'), + ('man', 'man'), + ('open', 'open'), + ('vim', 'vim'), + ('ll', 'fish -ic "ll"'), + ('ls', 'ls')]) # Fish has no aliases but functions + def test_from_shell(self, before, after, shell): + assert shell.from_shell(before) == after + + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' + + @pytest.mark.parametrize('entry, entry_utf8', [ + ('ls', '- cmd: ls\n when: 1430707243\n'), + (u'echo café', '- cmd: echo café\n when: 1430707243\n')]) + def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell): + mocker.patch('thefuck.shells.fish.time', + return_value=1430707243.3517463) + shell.put_to_history(entry) + builtins_open.return_value.__enter__.return_value. \ + write.assert_called_once_with(entry_utf8) + + def test_and_(self, shell): + assert shell.and_('foo', 'bar') == 'foo; and bar' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == {'fish_config': 'fish_config', + 'fuck': 'fuck', + 'funced': 'funced', + 'funcsave': 'funcsave', + 'history': 'history', + 'll': 'll', + 'math': 'math', + 'popd': 'popd', + 'pushd': 'pushd', + 'ruby': 'ruby'} + + def test_app_alias(self, shell): + assert 'function fuck' in shell.app_alias('fuck') + assert 'function FUCK' in shell.app_alias('FUCK') + assert 'thefuck' in shell.app_alias('fuck') + assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') + assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') + + def test_get_history(self, history_lines, shell): + history_lines(['- cmd: ls', ' when: 1432613911', + '- cmd: rm', ' when: 1432613916']) + assert list(shell.get_history()) == ['ls', 'rm'] diff --git a/tests/shells/test_generic.py b/tests/shells/test_generic.py new file mode 100644 index 0000000..a135ff8 --- /dev/null +++ b/tests/shells/test_generic.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +import pytest +from thefuck.shells import Generic + + +class TestGeneric(object): + @pytest.fixture + def shell(self): + return Generic() + + def test_from_shell(self, shell): + assert shell.from_shell('pwd') == 'pwd' + + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' + + def test_put_to_history(self, builtins_open, shell): + assert shell.put_to_history('ls') is None + assert shell.put_to_history(u'echo café') is None + assert builtins_open.call_count == 0 + + def test_and_(self, shell): + assert shell.and_('ls', 'cd') == 'ls && cd' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == {} + + def test_app_alias(self, shell): + assert 'alias fuck' in shell.app_alias('fuck') + assert 'alias FUCK' in shell.app_alias('FUCK') + assert 'thefuck' in shell.app_alias('fuck') + assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') + assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') + + def test_get_history(self, history_lines, shell): + history_lines(['ls', 'rm']) + # We don't know what to do in generic shell with history lines, + # so just ignore them: + assert list(shell.get_history()) == [] + + def test_split_command(self, shell): + assert shell.split_command('ls') == ['ls'] + assert shell.split_command(u'echo café') == [u'echo', u'café'] diff --git a/tests/shells/test_zsh.py b/tests/shells/test_zsh.py new file mode 100644 index 0000000..fd327ee --- /dev/null +++ b/tests/shells/test_zsh.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import pytest +from thefuck.shells.zsh import Zsh + + +@pytest.mark.usefixtures('isfile') +class TestZsh(object): + @pytest.fixture + def shell(self): + return Zsh() + + @pytest.fixture(autouse=True) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.zsh.Popen') + mock.return_value.stdout.read.return_value = ( + b'fuck=\'eval $(thefuck $(fc -ln -1 | tail -n 1))\'\n' + b'l=\'ls -CF\'\n' + b'la=\'ls -A\'\n' + b'll=\'ls -alF\'') + return mock + + @pytest.mark.parametrize('before, after', [ + ('fuck', 'eval $(thefuck $(fc -ln -1 | tail -n 1))'), + ('pwd', 'pwd'), + ('ll', 'ls -alF')]) + def test_from_shell(self, before, after, shell): + assert shell.from_shell(before) == after + + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' + + @pytest.mark.parametrize('entry, entry_utf8', [ + ('ls', ': 1430707243:0;ls\n'), + (u'echo café', ': 1430707243:0;echo café\n')]) + def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell): + mocker.patch('thefuck.shells.zsh.time', + return_value=1430707243.3517463) + shell.put_to_history(entry) + builtins_open.return_value.__enter__.return_value. \ + write.assert_called_once_with(entry_utf8) + + def test_and_(self, shell): + assert shell.and_('ls', 'cd') == 'ls && cd' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == { + 'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))', + 'l': 'ls -CF', + 'la': 'ls -A', + 'll': 'ls -alF'} + + def test_app_alias(self, shell): + assert 'alias fuck' in shell.app_alias('fuck') + assert 'alias FUCK' in shell.app_alias('FUCK') + assert 'thefuck' in shell.app_alias('fuck') + assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') + assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') + + def test_get_history(self, history_lines, shell): + history_lines([': 1432613911:0;ls', ': 1432613916:0;rm']) + assert list(shell.get_history()) == ['ls', 'rm'] diff --git a/tests/test_shells.py b/tests/test_shells.py deleted file mode 100644 index 1cea2fb..0000000 --- a/tests/test_shells.py +++ /dev/null @@ -1,260 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest -from thefuck import shells - - -@pytest.fixture -def builtins_open(mocker): - return mocker.patch('six.moves.builtins.open') - - -@pytest.fixture -def isfile(mocker): - return mocker.patch('os.path.isfile', return_value=True) - - -@pytest.fixture -@pytest.mark.usefixtures('isfile') -def history_lines(mocker): - def aux(lines): - mock = mocker.patch('io.open') - mock.return_value.__enter__\ - .return_value.readlines.return_value = lines - return aux - - -class TestGeneric(object): - @pytest.fixture - def shell(self): - return shells.Generic() - - def test_from_shell(self, shell): - assert shell.from_shell('pwd') == 'pwd' - - def test_to_shell(self, shell): - assert shell.to_shell('pwd') == 'pwd' - - def test_put_to_history(self, builtins_open, shell): - assert shell.put_to_history('ls') is None - assert shell.put_to_history(u'echo café') is None - assert builtins_open.call_count == 0 - - def test_and_(self, shell): - assert shell.and_('ls', 'cd') == 'ls && cd' - - def test_get_aliases(self, shell): - assert shell.get_aliases() == {} - - def test_app_alias(self, shell): - assert 'alias fuck' in shell.app_alias('fuck') - assert 'alias FUCK' in shell.app_alias('FUCK') - assert 'thefuck' in shell.app_alias('fuck') - assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') - assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') - - def test_get_history(self, history_lines, shell): - history_lines(['ls', 'rm']) - # We don't know what to do in generic shell with history lines, - # so just ignore them: - assert list(shell.get_history()) == [] - - def test_split_command(self, shell): - assert shell.split_command('ls') == ['ls'] - assert shell.split_command(u'echo café') == [u'echo', u'café'] - - -@pytest.mark.usefixtures('isfile') -class TestBash(object): - @pytest.fixture - def shell(self): - return shells.Bash() - - @pytest.fixture(autouse=True) - def Popen(self, mocker): - mock = mocker.patch('thefuck.shells.Popen') - mock.return_value.stdout.read.return_value = ( - b'alias fuck=\'eval $(thefuck $(fc -ln -1))\'\n' - 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'), - ('fuck', 'eval $(thefuck $(fc -ln -1))'), - ('awk', 'awk'), - ('ll', 'ls -alF')]) - def test_from_shell(self, before, after, shell): - assert shell.from_shell(before) == after - - def test_to_shell(self, shell): - assert shell.to_shell('pwd') == 'pwd' - - @pytest.mark.parametrize('entry, entry_utf8', [ - ('ls', 'ls\n'), - (u'echo café', 'echo café\n')]) - def test_put_to_history(self, entry, entry_utf8, builtins_open, shell): - shell.put_to_history(entry) - builtins_open.return_value.__enter__.return_value. \ - write.assert_called_once_with(entry_utf8) - - def test_and_(self, shell): - assert shell.and_('ls', 'cd') == 'ls && cd' - - def test_get_aliases(self, shell): - assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))', - 'l': 'ls -CF', - 'la': 'ls -A', - 'll': 'ls -alF'} - - def test_app_alias(self, shell): - assert 'alias fuck' in shell.app_alias('fuck') - assert 'alias FUCK' in shell.app_alias('FUCK') - assert 'thefuck' in shell.app_alias('fuck') - assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') - assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') - - def test_get_history(self, history_lines, shell): - history_lines(['ls', 'rm']) - assert list(shell.get_history()) == ['ls', 'rm'] - - -@pytest.mark.usefixtures('isfile') -class TestFish(object): - @pytest.fixture - def shell(self): - return shells.Fish() - - @pytest.fixture(autouse=True) - def Popen(self, mocker): - mock = mocker.patch('thefuck.shells.Popen') - mock.return_value.stdout.read.return_value = ( - b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n' - b'man\nmath\npopd\npushd\nruby') - return mock - - @pytest.fixture - def environ(self, monkeypatch): - data = {'TF_OVERRIDDEN_ALIASES': 'cd, ls, man, open'} - monkeypatch.setattr('thefuck.shells.os.environ', data) - return data - - @pytest.mark.usefixture('environ') - def test_get_overridden_aliases(self, shell, environ): - assert shell._get_overridden_aliases() == ['cd', 'ls', 'man', 'open'] - - @pytest.mark.parametrize('before, after', [ - ('cd', 'cd'), - ('pwd', 'pwd'), - ('fuck', 'fish -ic "fuck"'), - ('find', 'find'), - ('funced', 'fish -ic "funced"'), - ('grep', 'grep'), - ('awk', 'awk'), - ('math "2 + 2"', r'fish -ic "math \"2 + 2\""'), - ('man', 'man'), - ('open', 'open'), - ('vim', 'vim'), - ('ll', 'fish -ic "ll"'), - ('ls', 'ls')]) # Fish has no aliases but functions - def test_from_shell(self, before, after, shell): - assert shell.from_shell(before) == after - - def test_to_shell(self, shell): - assert shell.to_shell('pwd') == 'pwd' - - @pytest.mark.parametrize('entry, entry_utf8', [ - ('ls', '- cmd: ls\n when: 1430707243\n'), - (u'echo café', '- cmd: echo café\n when: 1430707243\n')]) - def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell): - mocker.patch('thefuck.shells.time', - return_value=1430707243.3517463) - shell.put_to_history(entry) - builtins_open.return_value.__enter__.return_value. \ - write.assert_called_once_with(entry_utf8) - - def test_and_(self, shell): - assert shell.and_('foo', 'bar') == 'foo; and bar' - - def test_get_aliases(self, shell): - assert shell.get_aliases() == {'fish_config': 'fish_config', - 'fuck': 'fuck', - 'funced': 'funced', - 'funcsave': 'funcsave', - 'history': 'history', - 'll': 'll', - 'math': 'math', - 'popd': 'popd', - 'pushd': 'pushd', - 'ruby': 'ruby'} - - def test_app_alias(self, shell): - assert 'function fuck' in shell.app_alias('fuck') - assert 'function FUCK' in shell.app_alias('FUCK') - assert 'thefuck' in shell.app_alias('fuck') - assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') - assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') - - def test_get_history(self, history_lines, shell): - history_lines(['- cmd: ls', ' when: 1432613911', - '- cmd: rm', ' when: 1432613916']) - assert list(shell.get_history()) == ['ls', 'rm'] - - -@pytest.mark.usefixtures('isfile') -class TestZsh(object): - @pytest.fixture - def shell(self): - return shells.Zsh() - - @pytest.fixture(autouse=True) - def Popen(self, mocker): - mock = mocker.patch('thefuck.shells.Popen') - mock.return_value.stdout.read.return_value = ( - b'fuck=\'eval $(thefuck $(fc -ln -1 | tail -n 1))\'\n' - b'l=\'ls -CF\'\n' - b'la=\'ls -A\'\n' - b'll=\'ls -alF\'') - return mock - - @pytest.mark.parametrize('before, after', [ - ('fuck', 'eval $(thefuck $(fc -ln -1 | tail -n 1))'), - ('pwd', 'pwd'), - ('ll', 'ls -alF')]) - def test_from_shell(self, before, after, shell): - assert shell.from_shell(before) == after - - def test_to_shell(self, shell): - assert shell.to_shell('pwd') == 'pwd' - - @pytest.mark.parametrize('entry, entry_utf8', [ - ('ls', ': 1430707243:0;ls\n'), - (u'echo café', ': 1430707243:0;echo café\n')]) - def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell): - mocker.patch('thefuck.shells.time', - return_value=1430707243.3517463) - shell.put_to_history(entry) - builtins_open.return_value.__enter__.return_value. \ - write.assert_called_once_with(entry_utf8) - - def test_and_(self, shell): - assert shell.and_('ls', 'cd') == 'ls && cd' - - def test_get_aliases(self, shell): - assert shell.get_aliases() == { - 'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))', - 'l': 'ls -CF', - 'la': 'ls -A', - 'll': 'ls -alF'} - - def test_app_alias(self, shell): - assert 'alias fuck' in shell.app_alias('fuck') - assert 'alias FUCK' in shell.app_alias('FUCK') - assert 'thefuck' in shell.app_alias('fuck') - assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') - assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') - - def test_get_history(self, history_lines, shell): - history_lines([': 1432613911:0;ls', ': 1432613916:0;rm']) - assert list(shell.get_history()) == ['ls', 'rm'] diff --git a/thefuck/shells.py b/thefuck/shells.py deleted file mode 100644 index d14a2d7..0000000 --- a/thefuck/shells.py +++ /dev/null @@ -1,334 +0,0 @@ -"""Module with shell specific actions, each shell class should -implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and -`get_aliases` methods. - -""" -from collections import defaultdict -from psutil import Process -from subprocess import Popen, PIPE -from time import time -import io -import os -import shlex -import sys -import six -from .utils import DEVNULL, memoize, cache -from .conf import settings -from . import logs - - -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 - - def app_alias(self, fuck): - return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \ - "thefuck $(fc -ln -1))'".format(fuck) - - def _get_history_file_name(self): - return '' - - def _get_history_line(self, command_script): - return '' - - def put_to_history(self, command_script): - """Puts command script to shell history.""" - history_file_name = self._get_history_file_name() - if os.path.isfile(history_file_name): - with open(history_file_name, 'a') as history: - entry = self._get_history_line(command_script) - if six.PY2: - history.write(entry.encode('utf-8')) - else: - history.write(entry) - - def get_history(self): - """Returns list of history entries.""" - history_file_name = self._get_history_file_name() - if os.path.isfile(history_file_name): - with io.open(history_file_name, 'r', - encoding='utf-8', errors='ignore') as history_file: - - lines = history_file.readlines() - if settings.history_limit: - lines = lines[-settings.history_limit:] - - for line in lines: - prepared = self._script_from_history(line) \ - .strip() - if prepared: - yield prepared - - def and_(self, *commands): - return u' && '.join(commands) - - def how_to_configure(self): - return - - def split_command(self, command): - """Split the command using shell-like syntax.""" - if six.PY2: - return [s.decode('utf8') for s in shlex.split(command.encode('utf8'))] - return shlex.split(command) - - def quote(self, s): - """Return a shell-escaped version of the string s.""" - - if six.PY2: - from pipes import quote - else: - from shlex import quote - - return quote(s) - - def _script_from_history(self, line): - return line - - -class Bash(Generic): - def app_alias(self, fuck): - return "alias {0}='eval " \ - "$(TF_ALIAS={0} PYTHONIOENCODING=utf-8 thefuck $(fc -ln -1));" \ - " history -r'".format(fuck) - - 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 - - @memoize - @cache('.bashrc', '.bash_profile') - def get_aliases(self): - proc = Popen(['bash', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) - return dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '=' in alias) - - def _get_history_file_name(self): - return os.environ.get("HISTFILE", - os.path.expanduser('~/.bash_history')) - - def _get_history_line(self, command_script): - return u'{}\n'.format(command_script) - - def how_to_configure(self): - if os.path.join(os.path.expanduser('~'), '.bashrc'): - config = '~/.bashrc' - elif os.path.join(os.path.expanduser('~'), '.bash_profile'): - config = '~/.bashrc' - else: - config = 'bash config' - return 'eval $(thefuck --alias)', config - - -class Fish(Generic): - def _get_overridden_aliases(self): - overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip() - if overridden_aliases: - return [alias.strip() for alias in overridden_aliases.split(',')] - else: - return ['cd', 'grep', 'ls', 'man', 'open'] - - def app_alias(self, fuck): - return ('function {0} -d "Correct your previous console command"\n' - ' set -l exit_code $status\n' - ' set -l fucked_up_command $history[1]\n' - ' env TF_ALIAS={0} PYTHONIOENCODING=utf-8' - ' thefuck $fucked_up_command | read -l unfucked_command\n' - ' if [ "$unfucked_command" != "" ]\n' - ' eval $unfucked_command\n' - ' if test $exit_code -ne 0\n' - ' history --delete $fucked_up_command\n' - ' history --merge ^ /dev/null\n' - ' return 0\n' - ' end\n' - ' end\n' - 'end').format(fuck) - - @memoize - @cache('.config/fish/config.fish', '.config/fish/functions') - def get_aliases(self): - overridden = self._get_overridden_aliases() - proc = Popen(['fish', '-ic', 'functions'], stdout=PIPE, stderr=DEVNULL) - functions = proc.stdout.read().decode('utf-8').strip().split('\n') - return {func: func for func in functions if func not in overridden} - - def _expand_aliases(self, command_script): - aliases = self.get_aliases() - binary = command_script.split(' ')[0] - if binary in aliases: - return u'fish -ic "{}"'.format(command_script.replace('"', r'\"')) - else: - return command_script - - def from_shell(self, command_script): - """Prepares command before running in app.""" - return self._expand_aliases(command_script) - - def _get_history_file_name(self): - return os.path.expanduser('~/.config/fish/fish_history') - - def _get_history_line(self, command_script): - return u'- cmd: {}\n when: {}\n'.format(command_script, int(time())) - - def _script_from_history(self, line): - if '- cmd: ' in line: - return line.split('- cmd: ', 1)[1] - else: - return '' - - def and_(self, *commands): - return u'; and '.join(commands) - - def how_to_configure(self): - return 'eval thefuck --alias', '~/.config/fish/config.fish' - - -class Zsh(Generic): - def app_alias(self, fuck): - return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8" \ - " thefuck $(fc -ln -1 | tail -n 1));" \ - " fc -R'".format(fuck) - - 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 - - @memoize - @cache('.zshrc') - def get_aliases(self): - proc = Popen(['zsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) - return dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '=' in alias) - - def _get_history_file_name(self): - return os.environ.get("HISTFILE", - os.path.expanduser('~/.zsh_history')) - - def _get_history_line(self, command_script): - return u': {}:0;{}\n'.format(int(time()), command_script) - - def _script_from_history(self, line): - if ';' in line: - return line.split(';', 1)[1] - else: - return '' - - def how_to_configure(self): - return 'eval $(thefuck --alias)', '~/.zshrc' - - -class Tcsh(Generic): - def app_alias(self, fuck): - return ("alias {0} 'setenv TF_ALIAS {0} && " - "set fucked_cmd=`history -h 2 | head -n 1` && " - "eval `thefuck ${{fucked_cmd}}`'").format(fuck) - - def _parse_alias(self, alias): - name, value = alias.split("\t", 1) - return name, value - - @memoize - def get_aliases(self): - proc = Popen(['tcsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) - return dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '\t' in alias) - - def _get_history_file_name(self): - return os.environ.get("HISTFILE", - os.path.expanduser('~/.history')) - - def _get_history_line(self, command_script): - return u'#+{}\n{}\n'.format(int(time()), command_script) - - def how_to_configure(self): - return 'eval `thefuck --alias`', '~/.tcshrc' - - -shells = defaultdict(Generic, { - 'bash': Bash(), - 'fish': Fish(), - 'zsh': Zsh(), - 'csh': Tcsh(), - 'tcsh': Tcsh()}) - - -@memoize -def _get_shell(): - try: - shell = Process(os.getpid()).parent().name() - except TypeError: - shell = Process(os.getpid()).parent.name - return shells[shell] - - -def from_shell(command): - return _get_shell().from_shell(command) - - -def to_shell(command): - return _get_shell().to_shell(command) - - -def app_alias(alias): - return _get_shell().app_alias(alias) - - -def thefuck_alias(): - return os.environ.get('TF_ALIAS', 'fuck') - - -def put_to_history(command): - try: - return _get_shell().put_to_history(command) - except IOError: - logs.exception("Can't update history", sys.exc_info()) - - -def and_(*commands): - return _get_shell().and_(*commands) - - -def get_aliases(): - return list(_get_shell().get_aliases().keys()) - - -def split_command(command): - return _get_shell().split_command(command) - - -def quote(s): - return _get_shell().quote(s) - - -@memoize -def get_history(): - return list(_get_shell().get_history()) - - -def how_to_configure(): - return _get_shell().how_to_configure() diff --git a/thefuck/shells/__init__.py b/thefuck/shells/__init__.py new file mode 100644 index 0000000..af9142b --- /dev/null +++ b/thefuck/shells/__init__.py @@ -0,0 +1,75 @@ +from collections import defaultdict +from psutil import Process +import os +import sys +from ..utils import memoize +from .. import logs +from .bash import Bash +from .fish import Fish +from .generic import Generic +from .tcsh import Tcsh +from .zsh import Zsh + +shells = defaultdict(Generic, + bash=Bash(), + fish=Fish(), + zsh=Zsh(), + csh=Tcsh(), + tcsh=Tcsh()) + + +@memoize +def _get_shell(): + try: + shell = Process(os.getpid()).parent().name() + except TypeError: + shell = Process(os.getpid()).parent.name + return shells[shell] + + +def from_shell(command): + return _get_shell().from_shell(command) + + +def to_shell(command): + return _get_shell().to_shell(command) + + +def app_alias(alias): + return _get_shell().app_alias(alias) + + +def thefuck_alias(): + return os.environ.get('TF_ALIAS', 'fuck') + + +def put_to_history(command): + try: + return _get_shell().put_to_history(command) + except IOError: + logs.exception("Can't update history", sys.exc_info()) + + +def and_(*commands): + return _get_shell().and_(*commands) + + +def get_aliases(): + return list(_get_shell().get_aliases().keys()) + + +def split_command(command): + return _get_shell().split_command(command) + + +def quote(s): + return _get_shell().quote(s) + + +@memoize +def get_history(): + return list(_get_shell().get_history()) + + +def how_to_configure(): + return _get_shell().how_to_configure() diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py new file mode 100644 index 0000000..9e7a1c4 --- /dev/null +++ b/thefuck/shells/bash.py @@ -0,0 +1,42 @@ +from subprocess import Popen, PIPE +import os +from ..utils import DEVNULL, memoize, cache +from .generic import Generic + + +class Bash(Generic): + def app_alias(self, fuck): + return "alias {0}='eval " \ + "$(TF_ALIAS={0} PYTHONIOENCODING=utf-8 thefuck $(fc -ln -1));" \ + " history -r'".format(fuck) + + 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 + + @memoize + @cache('.bashrc', '.bash_profile') + def get_aliases(self): + proc = Popen(['bash', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '=' in alias) + + def _get_history_file_name(self): + return os.environ.get("HISTFILE", + os.path.expanduser('~/.bash_history')) + + def _get_history_line(self, command_script): + return u'{}\n'.format(command_script) + + def how_to_configure(self): + if os.path.join(os.path.expanduser('~'), '.bashrc'): + config = '~/.bashrc' + elif os.path.join(os.path.expanduser('~'), '.bash_profile'): + config = '~/.bashrc' + else: + config = 'bash config' + return 'eval $(thefuck --alias)', config diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py new file mode 100644 index 0000000..b617439 --- /dev/null +++ b/thefuck/shells/fish.py @@ -0,0 +1,68 @@ +from subprocess import Popen, PIPE +from time import time +import os +from ..utils import DEVNULL, memoize, cache +from .generic import Generic + + +class Fish(Generic): + def _get_overridden_aliases(self): + overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip() + if overridden_aliases: + return [alias.strip() for alias in overridden_aliases.split(',')] + else: + return ['cd', 'grep', 'ls', 'man', 'open'] + + def app_alias(self, fuck): + return ('function {0} -d "Correct your previous console command"\n' + ' set -l exit_code $status\n' + ' set -l fucked_up_command $history[1]\n' + ' env TF_ALIAS={0} PYTHONIOENCODING=utf-8' + ' thefuck $fucked_up_command | read -l unfucked_command\n' + ' if [ "$unfucked_command" != "" ]\n' + ' eval $unfucked_command\n' + ' if test $exit_code -ne 0\n' + ' history --delete $fucked_up_command\n' + ' history --merge ^ /dev/null\n' + ' return 0\n' + ' end\n' + ' end\n' + 'end').format(fuck) + + @memoize + @cache('.config/fish/config.fish', '.config/fish/functions') + def get_aliases(self): + overridden = self._get_overridden_aliases() + proc = Popen(['fish', '-ic', 'functions'], stdout=PIPE, stderr=DEVNULL) + functions = proc.stdout.read().decode('utf-8').strip().split('\n') + return {func: func for func in functions if func not in overridden} + + def _expand_aliases(self, command_script): + aliases = self.get_aliases() + binary = command_script.split(' ')[0] + if binary in aliases: + return u'fish -ic "{}"'.format(command_script.replace('"', r'\"')) + else: + return command_script + + def from_shell(self, command_script): + """Prepares command before running in app.""" + return self._expand_aliases(command_script) + + def _get_history_file_name(self): + return os.path.expanduser('~/.config/fish/fish_history') + + def _get_history_line(self, command_script): + return u'- cmd: {}\n when: {}\n'.format(command_script, int(time())) + + def _script_from_history(self, line): + if '- cmd: ' in line: + return line.split('- cmd: ', 1)[1] + else: + return '' + + def and_(self, *commands): + return u'; and '.join(commands) + + def how_to_configure(self): + return 'eval thefuck --alias', '~/.config/fish/config.fish' diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py new file mode 100644 index 0000000..896792c --- /dev/null +++ b/thefuck/shells/generic.py @@ -0,0 +1,89 @@ +import io +import os +import shlex +import six +from ..conf import settings + + +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 + + def app_alias(self, fuck): + return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \ + "thefuck $(fc -ln -1))'".format(fuck) + + def _get_history_file_name(self): + return '' + + def _get_history_line(self, command_script): + return '' + + def put_to_history(self, command_script): + """Puts command script to shell history.""" + history_file_name = self._get_history_file_name() + if os.path.isfile(history_file_name): + with open(history_file_name, 'a') as history: + entry = self._get_history_line(command_script) + if six.PY2: + history.write(entry.encode('utf-8')) + else: + history.write(entry) + + def get_history(self): + """Returns list of history entries.""" + history_file_name = self._get_history_file_name() + if os.path.isfile(history_file_name): + with io.open(history_file_name, 'r', + encoding='utf-8', errors='ignore') as history_file: + + lines = history_file.readlines() + if settings.history_limit: + lines = lines[-settings.history_limit:] + + for line in lines: + prepared = self._script_from_history(line) \ + .strip() + if prepared: + yield prepared + + def and_(self, *commands): + return u' && '.join(commands) + + def how_to_configure(self): + return + + def split_command(self, command): + """Split the command using shell-like syntax.""" + if six.PY2: + return [s.decode('utf8') for s in shlex.split(command.encode('utf8'))] + return shlex.split(command) + + def quote(self, s): + """Return a shell-escaped version of the string s.""" + + if six.PY2: + from pipes import quote + else: + from shlex import quote + + return quote(s) + + def _script_from_history(self, line): + return line diff --git a/thefuck/shells/tcsh.py b/thefuck/shells/tcsh.py new file mode 100644 index 0000000..6c355ca --- /dev/null +++ b/thefuck/shells/tcsh.py @@ -0,0 +1,34 @@ +from subprocess import Popen, PIPE +from time import time +import os +from ..utils import DEVNULL, memoize +from .generic import Generic + + +class Tcsh(Generic): + def app_alias(self, fuck): + return ("alias {0} 'setenv TF_ALIAS {0} && " + "set fucked_cmd=`history -h 2 | head -n 1` && " + "eval `thefuck ${{fucked_cmd}}`'").format(fuck) + + def _parse_alias(self, alias): + name, value = alias.split("\t", 1) + return name, value + + @memoize + def get_aliases(self): + proc = Popen(['tcsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '\t' in alias) + + def _get_history_file_name(self): + return os.environ.get("HISTFILE", + os.path.expanduser('~/.history')) + + def _get_history_line(self, command_script): + return u'#+{}\n{}\n'.format(int(time()), command_script) + + def how_to_configure(self): + return 'eval `thefuck --alias`', '~/.tcshrc' diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py new file mode 100644 index 0000000..058ca36 --- /dev/null +++ b/thefuck/shells/zsh.py @@ -0,0 +1,43 @@ +from subprocess import Popen, PIPE +from time import time +import os +from ..utils import DEVNULL, memoize, cache +from .generic import Generic + + +class Zsh(Generic): + def app_alias(self, fuck): + return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8" \ + " thefuck $(fc -ln -1 | tail -n 1));" \ + " fc -R'".format(fuck) + + 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 + + @memoize + @cache('.zshrc') + def get_aliases(self): + proc = Popen(['zsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '=' in alias) + + def _get_history_file_name(self): + return os.environ.get("HISTFILE", + os.path.expanduser('~/.zsh_history')) + + def _get_history_line(self, command_script): + return u': {}:0;{}\n'.format(int(time()), command_script) + + def _script_from_history(self, line): + if ';' in line: + return line.split(';', 1)[1] + else: + return '' + + def how_to_configure(self): + return 'eval $(thefuck --alias)', '~/.zshrc'