diff --git a/README.md b/README.md index 63abba4a..a2630bf2 100644 --- a/README.md +++ b/README.md @@ -102,14 +102,20 @@ sudo pip install thefuck [Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation) -And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX): +And add to `.bashrc` or `.bash_profile`(for OSX): ```bash -alias fuck='eval $(thefuck $(fc -ln -1))' +alias fuck='eval $(thefuck $(fc -ln -1)); history -r' # You can use whatever you want as an alias, like for Mondays: alias FUCK='fuck' ``` +Or in your `.zshrc`: + +```bash +alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R' +``` + Alternatively, you can redirect the output of `thefuck-alias`: ```bash diff --git a/tests/test_history.py b/tests/test_history.py deleted file mode 100644 index 12af8906..00000000 --- a/tests/test_history.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from mock import Mock -from thefuck.history import History - - -class TestHistory(object): - @pytest.fixture(autouse=True) - def process(self, monkeypatch): - Process = Mock() - Process.return_value.parent.return_value.pid = 1 - monkeypatch.setattr('thefuck.history.Process', Process) - return Process - - @pytest.fixture(autouse=True) - def db(self, monkeypatch): - class DBMock(dict): - def __init__(self): - super(DBMock, self).__init__() - self.sync = Mock() - - def __call__(self, *args, **kwargs): - return self - - db = DBMock() - monkeypatch.setattr('thefuck.history.shelve.open', db) - return db - - def test_set(self, db): - history = History() - history.update(last_command='ls', - last_fixed_command=None) - assert db == {'1-last_command': 'ls', - '1-last_fixed_command': None} - - def test_get(self, db): - history = History() - db['1-last_command'] = 'cd ..' - assert history.last_command == 'cd ..' - - def test_get_without_value(self): - history = History() - assert history.last_command is None diff --git a/tests/test_main.py b/tests/test_main.py index a871c76a..681b6164 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -56,7 +56,7 @@ class TestGetCommand(object): monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x) def test_get_command_calls(self, Popen): - assert main.get_command(Mock(), Mock(), + assert main.get_command(Mock(), ['thefuck', 'apt-get', 'search', 'vim']) \ == Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', @@ -64,22 +64,14 @@ class TestGetCommand(object): stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) - - @pytest.mark.parametrize('history, args, result', [ - (Mock(), [''], None), - (Mock(last_command='ls', last_fixed_command='ls -la'), - ['thefuck', 'fuck'], 'ls -la'), - (Mock(last_command='ls', last_fixed_command='ls -la'), - ['thefuck', 'ls'], 'ls -la'), - (Mock(last_command='ls', last_fixed_command=''), - ['thefuck', 'ls'], 'ls'), - (Mock(last_command='ls', last_fixed_command=''), - ['thefuck', 'fuck'], 'ls')]) - def test_get_command_script(self, history, args, result): + @pytest.mark.parametrize('args, result', [ + (['thefuck', 'ls', '-la'], 'ls -la'), + (['thefuck', 'ls'], 'ls')]) + def test_get_command_script(self, args, result): if result: - assert main.get_command(Mock(), history, args).script == result + assert main.get_command(Mock(), args).script == result else: - assert main.get_command(Mock(), history, args) is None + assert main.get_command(Mock(), args) is None class TestGetMatchedRule(object): @@ -109,7 +101,7 @@ class TestRunRule(object): def test_run_rule(self, capsys): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - Command(), Mock(), None) + Command(), None) assert capsys.readouterr() == ('new-command\n', '') def test_run_rule_with_side_effect(self, capsys): @@ -118,21 +110,14 @@ class TestRunRule(object): command = Command() main.run_rule(Rule(get_new_command=lambda *_: 'new-command', side_effect=side_effect), - command, Mock(), settings) + command, settings) assert capsys.readouterr() == ('new-command\n', '') side_effect.assert_called_once_with(command, settings) - def test_hisotry_updated(self): - history = Mock() - main.run_rule(Rule(get_new_command=lambda *_: 'ls -lah'), - Command('ls'), history, None) - history.update.assert_called_once_with(last_command='ls', - last_fixed_command='ls -lah') - def test_when_not_comfirmed(self, capsys, confirm): confirm.return_value = False main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - Command(), Mock(), None) + Command(), None) assert capsys.readouterr() == ('', '') diff --git a/tests/test_shells.py b/tests/test_shells.py index c24112b3..5b2748e5 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -1,16 +1,35 @@ import pytest -from mock import Mock +from mock import Mock, MagicMock from thefuck import shells +@pytest.fixture +def builtins_open(monkeypatch): + mock = MagicMock() + monkeypatch.setattr('six.moves.builtins.open', mock) + return mock + + +@pytest.fixture +def isfile(monkeypatch): + mock = Mock(return_value=True) + monkeypatch.setattr('os.path.isfile', mock) + return mock + + 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' + assert shells.Generic().to_shell('pwd') == 'pwd' + + def test_put_to_history(self, builtins_open): + assert shells.Generic().put_to_history('ls') is None + assert builtins_open.call_count == 0 +@pytest.mark.usefixtures('isfile') class TestBash(object): @pytest.fixture(autouse=True) def Popen(self, monkeypatch): @@ -31,7 +50,13 @@ class TestBash(object): def test_to_shell(self): assert shells.Bash().to_shell('pwd') == 'pwd' + def test_put_to_history(self, builtins_open): + shells.Bash().put_to_history('ls') + builtins_open.return_value.__enter__.return_value.\ + write.assert_called_once_with('ls\n') + +@pytest.mark.usefixtures('isfile') class TestZsh(object): @pytest.fixture(autouse=True) def Popen(self, monkeypatch): @@ -50,4 +75,11 @@ class TestZsh(object): 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 + assert shells.Zsh().to_shell('pwd') == 'pwd' + + def test_put_to_history(self, builtins_open, monkeypatch): + monkeypatch.setattr('thefuck.shells.time', + lambda: 1430707243.3517463) + shells.Zsh().put_to_history('ls') + builtins_open.return_value.__enter__.return_value. \ + write.assert_called_once_with(': 1430707243:0;ls\n') \ No newline at end of file diff --git a/thefuck/history.py b/thefuck/history.py deleted file mode 100644 index 86a24413..00000000 --- a/thefuck/history.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import shelve -from tempfile import gettempdir -from psutil import Process - - -class History(object): - """Temporary history of commands/fixed-commands dependent on - current shell instance. - - """ - - def __init__(self): - self._path = os.path.join(gettempdir(), '.thefuck_history') - self._pid = Process(os.getpid()).parent().pid - self._db = shelve.open(self._path) - - def _prepare_key(self, key): - return '{}-{}'.format(self._pid, key) - - def update(self, **kwargs): - self._db.update({self._prepare_key(k): v for k,v in kwargs.items()}) - self._db.sync() - return self - - def __getattr__(self, item): - return self._db.get(self._prepare_key(item)) diff --git a/thefuck/main.py b/thefuck/main.py index 0a62d457..56e28d1d 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -6,7 +6,7 @@ import os import sys from psutil import Process, TimeoutExpired import colorama -from .history import History +import six from . import logs, conf, types, shells @@ -60,22 +60,17 @@ def wait_output(settings, popen): return False -def get_command(settings, history, args): +def get_command(settings, args): """Creates command from `args` and executes it.""" - if sys.version_info[0] < 3: + if six.PY2: script = ' '.join(arg.decode('utf-8') for arg in args[1:]) else: script = ' '.join(args[1:]) - if script == 'fuck' or script == history.last_command: - script = history.last_fixed_command or history.last_command - 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, env=dict(os.environ, LANG='C')) if wait_output(settings, result): @@ -108,14 +103,13 @@ def confirm(new_command, side_effect, settings): return False -def run_rule(rule, command, history, settings): +def run_rule(rule, command, settings): """Runs command from rule for passed command.""" 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) - history.update(last_command=command.script, - last_fixed_command=new_command) + shells.put_to_history(new_command) print(new_command) @@ -127,14 +121,13 @@ def main(): colorama.init() user_dir = setup_user_dir() settings = conf.get_settings(user_dir) - history = History() - command = get_command(settings, history, sys.argv) + command = get_command(settings, sys.argv) if command: rules = get_rules(user_dir, settings) matched_rule = get_matched_rule(command, rules, settings) if matched_rule: - run_rule(matched_rule, command, history, settings) + run_rule(matched_rule, command, settings) return logs.failed('No fuck given', settings) diff --git a/thefuck/shells.py b/thefuck/shells.py index a9bc7cb5..c0bb0894 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -1,9 +1,11 @@ """Module with shell specific actions, each shell class should -implement `from_shell` and `to_shell` methods. +implement `from_shell`, `to_shell`, `app_alias` and `put_to_history` +methods. """ from collections import defaultdict from subprocess import Popen, PIPE +from time import time import os from psutil import Process @@ -34,6 +36,19 @@ class Generic(object): def app_alias(self): return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" + 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: + history.write(self._get_history_line(command_script)) + class Bash(Generic): def _parse_alias(self, alias): @@ -49,6 +64,13 @@ class Bash(Generic): for alias in proc.stdout.read().decode('utf-8').split('\n') if 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) + class Zsh(Generic): def _parse_alias(self, alias): @@ -64,6 +86,13 @@ class Zsh(Generic): for alias in proc.stdout.read().decode('utf-8').split('\n') if 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) + shells = defaultdict(lambda: Generic(), { 'bash': Bash(), @@ -85,3 +114,7 @@ def to_shell(command): def app_alias(): return _get_shell().app_alias() + + +def put_to_history(command): + return _get_shell().put_to_history(command)