1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-02-21 20:38:54 +00:00

#89 #152 Use shell history

This commit is contained in:
nvbn 2015-05-04 04:44:16 +02:00
parent 72ac9650f9
commit 3f2fe0d275
7 changed files with 94 additions and 114 deletions

View File

@ -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) [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 ```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: # You can use whatever you want as an alias, like for Mondays:
alias FUCK='fuck' 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`: Alternatively, you can redirect the output of `thefuck-alias`:
```bash ```bash

View File

@ -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

View File

@ -56,7 +56,7 @@ class TestGetCommand(object):
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x) monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_get_command_calls(self, Popen): def test_get_command_calls(self, Popen):
assert main.get_command(Mock(), Mock(), assert main.get_command(Mock(),
['thefuck', 'apt-get', 'search', 'vim']) \ ['thefuck', 'apt-get', 'search', 'vim']) \
== Command('apt-get search vim', 'stdout', 'stderr') == Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim', Popen.assert_called_once_with('apt-get search vim',
@ -64,22 +64,14 @@ class TestGetCommand(object):
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
env={'LANG': 'C'}) env={'LANG': 'C'})
@pytest.mark.parametrize('args, result', [
@pytest.mark.parametrize('history, args, result', [ (['thefuck', 'ls', '-la'], 'ls -la'),
(Mock(), [''], None), (['thefuck', 'ls'], 'ls')])
(Mock(last_command='ls', last_fixed_command='ls -la'), def test_get_command_script(self, args, result):
['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):
if result: if result:
assert main.get_command(Mock(), history, args).script == result assert main.get_command(Mock(), args).script == result
else: else:
assert main.get_command(Mock(), history, args) is None assert main.get_command(Mock(), args) is None
class TestGetMatchedRule(object): class TestGetMatchedRule(object):
@ -109,7 +101,7 @@ class TestRunRule(object):
def test_run_rule(self, capsys): def test_run_rule(self, capsys):
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
Command(), Mock(), None) Command(), None)
assert capsys.readouterr() == ('new-command\n', '') assert capsys.readouterr() == ('new-command\n', '')
def test_run_rule_with_side_effect(self, capsys): def test_run_rule_with_side_effect(self, capsys):
@ -118,21 +110,14 @@ class TestRunRule(object):
command = Command() command = Command()
main.run_rule(Rule(get_new_command=lambda *_: 'new-command', main.run_rule(Rule(get_new_command=lambda *_: 'new-command',
side_effect=side_effect), side_effect=side_effect),
command, Mock(), settings) command, settings)
assert capsys.readouterr() == ('new-command\n', '') assert capsys.readouterr() == ('new-command\n', '')
side_effect.assert_called_once_with(command, settings) 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): def test_when_not_comfirmed(self, capsys, confirm):
confirm.return_value = False confirm.return_value = False
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
Command(), Mock(), None) Command(), None)
assert capsys.readouterr() == ('', '') assert capsys.readouterr() == ('', '')

View File

@ -1,16 +1,35 @@
import pytest import pytest
from mock import Mock from mock import Mock, MagicMock
from thefuck import shells 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): class TestGeneric(object):
def test_from_shell(self): def test_from_shell(self):
assert shells.Generic().from_shell('pwd') == 'pwd' assert shells.Generic().from_shell('pwd') == 'pwd'
def test_to_shell(self): 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): class TestBash(object):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def Popen(self, monkeypatch): def Popen(self, monkeypatch):
@ -31,7 +50,13 @@ class TestBash(object):
def test_to_shell(self): def test_to_shell(self):
assert shells.Bash().to_shell('pwd') == 'pwd' 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): class TestZsh(object):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def Popen(self, monkeypatch): def Popen(self, monkeypatch):
@ -50,4 +75,11 @@ class TestZsh(object):
assert shells.Zsh().from_shell(before) == after assert shells.Zsh().from_shell(before) == after
def test_to_shell(self): def test_to_shell(self):
assert shells.Zsh().to_shell('pwd') == 'pwd' 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')

View File

@ -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))

View File

@ -6,7 +6,7 @@ import os
import sys import sys
from psutil import Process, TimeoutExpired from psutil import Process, TimeoutExpired
import colorama import colorama
from .history import History import six
from . import logs, conf, types, shells from . import logs, conf, types, shells
@ -60,22 +60,17 @@ def wait_output(settings, popen):
return False return False
def get_command(settings, history, args): def get_command(settings, args):
"""Creates command from `args` and executes it.""" """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:]) script = ' '.join(arg.decode('utf-8') for arg in args[1:])
else: else:
script = ' '.join(args[1:]) script = ' '.join(args[1:])
if script == 'fuck' or script == history.last_command:
script = history.last_fixed_command or history.last_command
if not script: if not script:
return return
script = shells.from_shell(script) script = shells.from_shell(script)
history.update(last_command=script,
last_fixed_command=None)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C')) env=dict(os.environ, LANG='C'))
if wait_output(settings, result): if wait_output(settings, result):
@ -108,14 +103,13 @@ def confirm(new_command, side_effect, settings):
return False return False
def run_rule(rule, command, history, settings): def run_rule(rule, command, settings):
"""Runs command from rule for passed command.""" """Runs command from rule for passed command."""
new_command = shells.to_shell(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 confirm(new_command, rule.side_effect, settings):
if rule.side_effect: if rule.side_effect:
rule.side_effect(command, settings) rule.side_effect(command, settings)
history.update(last_command=command.script, shells.put_to_history(new_command)
last_fixed_command=new_command)
print(new_command) print(new_command)
@ -127,14 +121,13 @@ def main():
colorama.init() colorama.init()
user_dir = setup_user_dir() user_dir = setup_user_dir()
settings = conf.get_settings(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: if command:
rules = get_rules(user_dir, settings) rules = get_rules(user_dir, settings)
matched_rule = get_matched_rule(command, rules, settings) matched_rule = get_matched_rule(command, rules, settings)
if matched_rule: if matched_rule:
run_rule(matched_rule, command, history, settings) run_rule(matched_rule, command, settings)
return return
logs.failed('No fuck given', settings) logs.failed('No fuck given', settings)

View File

@ -1,9 +1,11 @@
"""Module with shell specific actions, each shell class should """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 collections import defaultdict
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from time import time
import os import os
from psutil import Process from psutil import Process
@ -34,6 +36,19 @@ class Generic(object):
def app_alias(self): def app_alias(self):
return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" 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): class Bash(Generic):
def _parse_alias(self, alias): def _parse_alias(self, alias):
@ -49,6 +64,13 @@ class Bash(Generic):
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias) 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): class Zsh(Generic):
def _parse_alias(self, alias): def _parse_alias(self, alias):
@ -64,6 +86,13 @@ class Zsh(Generic):
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias) 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(), { shells = defaultdict(lambda: Generic(), {
'bash': Bash(), 'bash': Bash(),
@ -85,3 +114,7 @@ def to_shell(command):
def app_alias(): def app_alias():
return _get_shell().app_alias() return _get_shell().app_alias()
def put_to_history(command):
return _get_shell().put_to_history(command)