1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-10 20:06:02 +00:00

Compare commits

...

18 Commits
1.48 ... 1.49.1

Author SHA1 Message Date
nvbn
fbfb4b5e41 Merge branch 'petr-tichy-master' 2015-07-18 17:19:57 +03:00
Petr Tichý
51c37bc5ab Fix wheel dependencies for Python 2 2015-07-17 18:51:35 +02:00
nvbn
78769e4fbc Bump to 1.49 2015-07-15 07:49:18 +03:00
nvbn
3e4c043ccc #280: Add debug output 2015-07-15 07:47:54 +03:00
nvbn
934099fe9e #289: Add is a directory pattern to cp_omitting_directory rule 2015-07-15 07:12:07 +03:00
Vladimir Iakovlev
464f86eccf Merge pull request #288 from scorphus/overridden-aliases
fix(fish.get_aliases): do not include overridden aliases
2015-07-15 06:58:32 +03:00
Pablo Santiago Blum de Aguiar
891fbe7ed1 fix(fish.get_aliases): do not include overridden aliases
Fish Shell overrides some shell commands, such as `cd` and `ls` and
therefore some rules fail to match. The following aliases are excluded
by default:

 * cd
 * grep
 * ls
 * man
 * open

To change them, one can use the `TF_OVERRIDDEN_ALIASES` environment
variable such as:

```
set TF_OVERRIDDEN_ALIASES 'cd,grep,ls'
```

Fix #262
2015-07-13 22:53:15 -03:00
nvbn
5abab8bd1e Merge branch 'master' of github.com:nvbn/thefuck 2015-07-10 17:58:53 +03:00
nvbn
7ebc8a38af #N/A Add history rule 2015-07-10 17:58:41 +03:00
nvbn
f40b63f44b #N/A Add ability to disable memoization in tests 2015-07-10 17:06:05 +03:00
nvbn
4b4e7acc0f #N/A Add ability to get shell history 2015-07-10 16:42:21 +03:00
Vladimir Iakovlev
a8587d3871 Merge pull request #285 from mcarton/tmux
Use `get_closest` in the tmux rule
2015-07-10 15:54:14 +03:00
mcarton
370c58e679 Use get_closest in the tmux rule 2015-07-10 09:49:49 +02:00
Vladimir Iakovlev
328e65179e Merge pull request #283 from mcarton/mercurial
Some fixes in REAME.md
2015-07-09 20:13:38 +03:00
Vladimir Iakovlev
63bb4da8e1 Merge pull request #282 from mcarton/sudo
Add systemd's kind of error for the sudo rule
2015-07-09 20:13:17 +03:00
mcarton
0b5a7a8e2d Fix rule name in README 2015-07-09 18:35:33 +02:00
mcarton
5693bd49f7 #281 Add the mercurial rule to README.md 2015-07-09 18:01:44 +02:00
mcarton
12f8d017b9 Add systemd's kind of error for the sudo rule
A complete error would be:

```
% systemctl daemon-reload
==== AUTHENTICATING FOR org.freedesktop.systemd1.reload-daemon ===
Authentication is required to reload the systemd state.
Authenticating as: martin
Password:
```
2015-07-09 17:24:45 +02:00
23 changed files with 281 additions and 62 deletions

View File

@@ -4,6 +4,6 @@ python:
- "3.3" - "3.3"
- "2.7" - "2.7"
install: install:
- python setup.py develop
- pip install -r requirements.txt - pip install -r requirements.txt
- python setup.py develop
script: py.test -v script: py.test -v

View File

@@ -169,12 +169,14 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `go_run` – appends `.go` extension when compiling/running Go programs * `go_run` – appends `.go` extension when compiling/running Go programs
* `grep_recursive` – adds `-r` when you trying to grep directory; * `grep_recursive` – adds `-r` when you trying to grep directory;
* `has_exists_script` – prepends `./` when script/binary exists; * `has_exists_script` – prepends `./` when script/binary exists;
* `history` – tries to replace command with most similar command from history;
* `java` – removes `.java` extension when running Java programs; * `java` – removes `.java` extension when running Java programs;
* `javac` – appends missing `.java` when compiling Java files; * `javac` – appends missing `.java` when compiling Java files;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `ls_lah` – adds -lah to ls; * `ls_lah` – adds -lah to ls;
* `man` – change manual section; * `man` – change manual section;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `man_no_space` – fixes man commands without spaces, for example `mandiff`;
* `mercurial` – fixes wrong `hg` commands;
* `mkdir_p` – adds `-p` when you trying to create directory without parent; * `mkdir_p` – adds `-p` when you trying to create directory without parent;
* `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_command` – fixes wrong console commands, for example `vom/vim`;
* `no_such_file` – creates missing directories with `mv` and `cp` commands; * `no_such_file` – creates missing directories with `mv` and `cp` commands;
@@ -184,7 +186,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `python_execute` – appends missing `.py` when executing Python files; * `python_execute` – appends missing `.py` when executing Python files;
* `quotation_marks` – fixes uneven usage of `'` and `"` when containing args' * `quotation_marks` – fixes uneven usage of `'` and `"` when containing args'
* `rm_dir` – adds `-rf` when you trying to remove directory; * `rm_dir` – adds `-rf` when you trying to remove directory;
* `sed` – adds missing '/' to `sed`'s `s` commands; * `sed_unterminated_s` – adds missing '/' to `sed`'s `s` commands;
* `sl_ls` – changes `sl` to `ls`; * `sl_ls` – changes `sl` to `ls`;
* `ssh_known_hosts` – removes host from `known_hosts` on warning; * `ssh_known_hosts` – removes host from `known_hosts` on warning;
* `sudo` – prepends `sudo` to previous command if it failed because of permissions; * `sudo` – prepends `sudo` to previous command if it failed because of permissions;
@@ -251,7 +253,8 @@ The Fuck has a few settings parameters which can be changed in `~/.thefuck/setti
* `require_confirmation` – requires confirmation before running new command, by default `False`; * `require_confirmation` – requires confirmation before running new command, by default `False`;
* `wait_command` – max amount of time in seconds for getting previous command output; * `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output; * `no_colors` – disable colored output;
* `priority` – dict with rules priorities, rule with lower `priority` will be matched first. * `priority` – dict with rules priorities, rule with lower `priority` will be matched first;
* `debug` – enabled debug output, by default `False`;
Example of `settings.py`: Example of `settings.py`:
@@ -261,6 +264,7 @@ require_confirmation = True
wait_command = 10 wait_command = 10
no_colors = False no_colors = False
priority = {'sudo': 100, 'no_command': 9999} priority = {'sudo': 100, 'no_command': 9999}
debug = False
``` ```
Or via environment variables: Or via environment variables:
@@ -270,7 +274,8 @@ Or via environment variables:
* `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output; * `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`; * `THEFUCK_NO_COLORS` – disable colored output, `true/false`;
* `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`, * `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`,
rule with lower `priority` will be matched first. rule with lower `priority` will be matched first;
* `THEFUCK_DEBUG` – enables debug output, `true/false`.
For example: For example:

View File

@@ -2,3 +2,4 @@ pytest
mock mock
pytest-mock pytest-mock
wheel wheel
setuptools>=17.1

View File

@@ -11,12 +11,10 @@ elif (3, 0) < sys.version_info < (3, 3):
' ({}.{} detected).'.format(*sys.version_info[:2])) ' ({}.{} detected).'.format(*sys.version_info[:2]))
sys.exit(-1) sys.exit(-1)
VERSION = '1.48' VERSION = '1.49.1'
install_requires = ['psutil', 'colorama', 'six'] install_requires = ['psutil', 'colorama', 'six']
extras_require = {':python_version<"3.4"': ['pathlib']}
if sys.version_info < (3, 4):
install_requires.append('pathlib')
setup(name='thefuck', setup(name='thefuck',
version=VERSION, version=VERSION,
@@ -30,6 +28,7 @@ setup(name='thefuck',
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=install_requires, install_requires=install_requires,
extras_require=extras_require,
entry_points={'console_scripts': [ entry_points={'console_scripts': [
'thefuck = thefuck.main:main', 'thefuck = thefuck.main:main',
'thefuck-alias = thefuck.shells:app_alias']}) 'thefuck-alias = thefuck.shells:app_alias']})

6
tests/conftest.py Normal file
View File

@@ -0,0 +1,6 @@
import pytest
@pytest.fixture
def no_memoize(monkeypatch):
monkeypatch.setattr('thefuck.utils.memoize.disabled', True)

View File

@@ -1,14 +1,22 @@
from mock import Mock import pytest
from thefuck.rules.cp_omitting_directory import match, get_new_command from thefuck.rules.cp_omitting_directory import match, get_new_command
from tests.utils import Command
def test_match(): @pytest.mark.parametrize('script, stderr', [
assert match(Mock(script='cp dir', stderr="cp: omitting directory 'dir'"), ('cp dir', 'cp: dor: is a directory'),
None) ('cp dir', "cp: omitting directory 'dir'")])
assert not match(Mock(script='some dir', def test_match(script, stderr):
stderr="cp: omitting directory 'dir'"), None) assert match(Command(script, stderr=stderr), None)
assert not match(Mock(script='cp dir', stderr=""), None)
@pytest.mark.parametrize('script, stderr', [
('some dir', 'cp: dor: is a directory'),
('some dir', "cp: omitting directory 'dir'"),
('cp dir', '')])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr), None)
def test_get_new_command(): def test_get_new_command():
assert get_new_command(Mock(script='cp dir'), None) == 'cp -a dir' assert get_new_command(Command(script='cp dir'), None) == 'cp -a dir'

View File

@@ -0,0 +1,35 @@
import pytest
from thefuck.rules.history import match, get_new_command
from tests.utils import Command
@pytest.fixture
def history(mocker):
return mocker.patch('thefuck.rules.history.get_history',
return_value=['ls cat', 'diff x', 'nocommand x'])
@pytest.fixture
def callables(mocker):
return mocker.patch('thefuck.rules.history.get_all_callables',
return_value=['diff', 'ls'])
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.parametrize('script', ['ls cet', 'daff x'])
def test_match(script):
assert match(Command(script=script), None)
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.parametrize('script', ['apt-get', 'nocommand y'])
def test_not_match(script):
assert not match(Command(script=script), None)
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.parametrize('script, result', [
('ls cet', 'ls cat'),
('daff x', 'diff x')])
def test_get_new_command(script, result):
assert get_new_command(Command(script), None) == result

View File

@@ -1,36 +1,42 @@
from mock import patch, Mock import pytest
from thefuck.rules.no_command import match, get_new_command, _get_all_callables from thefuck.rules.no_command import match, get_new_command, get_all_callables
from tests.utils import Command
@patch('thefuck.rules.no_command._safe', return_value=[]) @pytest.fixture(autouse=True)
@patch('thefuck.rules.no_command.get_aliases', def _safe(mocker):
mocker.patch('thefuck.rules.no_command._safe', return_value=[])
@pytest.fixture(autouse=True)
def get_aliases(mocker):
mocker.patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck']) return_value=['vim', 'apt-get', 'fsck', 'fuck'])
@pytest.mark.usefixtures('no_memoize')
def test_get_all_callables(*args): def test_get_all_callables(*args):
all_callables = _get_all_callables() all_callables = get_all_callables()
assert 'vim' in all_callables assert 'vim' in all_callables
assert 'fsck' in all_callables assert 'fsck' in all_callables
assert 'fuck' not in all_callables assert 'fuck' not in all_callables
@patch('thefuck.rules.no_command._safe', return_value=[]) @pytest.mark.usefixtures('no_memoize')
@patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
def test_match(*args): def test_match(*args):
assert match(Mock(stderr='vom: not found', script='vom file.py'), None) assert match(Command(stderr='vom: not found', script='vom file.py'), None)
assert match(Mock(stderr='fucck: not found', script='fucck'), None) assert match(Command(stderr='fucck: not found', script='fucck'), None)
assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None) assert not match(Command(stderr='qweqwe: not found', script='qweqwe'), None)
assert not match(Mock(stderr='some text', script='vom file.py'), None) assert not match(Command(stderr='some text', script='vom file.py'), None)
@patch('thefuck.rules.no_command._safe', return_value=[]) @pytest.mark.usefixtures('no_memoize')
@patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
def test_get_new_command(*args): def test_get_new_command(*args):
assert get_new_command( assert get_new_command(
Mock(stderr='vom: not found', Command(stderr='vom: not found',
script='vom file.py'), script='vom file.py'),
None) == 'vim file.py' None) == 'vim file.py'
assert get_new_command( assert get_new_command(
Mock(stderr='fucck: not found', Command(stderr='fucck: not found',
script='fucck'), script='fucck'),
None) == 'fsck' Command) == 'fsck'

View File

@@ -16,4 +16,4 @@ def test_match(tmux_ambiguous):
def test_get_new_command(tmux_ambiguous): def test_get_new_command(tmux_ambiguous):
assert get_new_command(Command('tmux list', stderr=tmux_ambiguous), None)\ assert get_new_command(Command('tmux list', stderr=tmux_ambiguous), None)\
== 'tmux list-buffers' == 'tmux list-keys'

View File

@@ -5,3 +5,10 @@ from thefuck import logs
def test_color(): def test_color():
assert logs.color('red', Mock(no_colors=False)) == 'red' assert logs.color('red', Mock(no_colors=False)) == 'red'
assert logs.color('red', Mock(no_colors=True)) == '' assert logs.color('red', Mock(no_colors=True)) == ''
def test_debug(capsys):
logs.debug('test', Mock(no_colors=True, debug=True))
assert capsys.readouterr() == ('', 'DEBUG: test\n')
logs.debug('test', Mock(no_colors=True, debug=False))
assert capsys.readouterr() == ('', '')

View File

@@ -110,7 +110,7 @@ class TestGetMatchedRule(object):
def test_when_rule_failed(self, capsys): def test_when_rule_failed(self, capsys):
main.get_matched_rule( main.get_matched_rule(
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))], Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))],
Mock(no_colors=True)) Mock(no_colors=True, debug=False))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:' assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
@@ -126,7 +126,7 @@ class TestRunRule(object):
def test_run_rule_with_side_effect(self, capsys): def test_run_rule_with_side_effect(self, capsys):
side_effect = Mock() side_effect = Mock()
settings = Mock() settings = Mock(debug=False)
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),

View File

@@ -12,6 +12,16 @@ def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True) 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.__iter__.return_value = lines
return aux
class TestGeneric(object): class TestGeneric(object):
@pytest.fixture @pytest.fixture
def shell(self): def shell(self):
@@ -38,6 +48,12 @@ class TestGeneric(object):
assert 'thefuck' in shell.app_alias() assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias() assert 'TF_ALIAS' in shell.app_alias()
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()) == []
@pytest.mark.usefixtures('isfile') @pytest.mark.usefixtures('isfile')
class TestBash(object): class TestBash(object):
@@ -85,6 +101,10 @@ class TestBash(object):
assert 'thefuck' in shell.app_alias() assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias() assert 'TF_ALIAS' in shell.app_alias()
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.usefixtures('isfile') @pytest.mark.usefixtures('isfile')
class TestFish(object): class TestFish(object):
@@ -96,18 +116,34 @@ class TestFish(object):
def Popen(self, mocker): def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen') mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = ( mock.return_value.stdout.read.return_value = (
b'fish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nmath') b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n'
b'man\nmath\npopd\npushd\nruby')
return mock 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', [ @pytest.mark.parametrize('before, after', [
('cd', 'cd'),
('pwd', 'pwd'), ('pwd', 'pwd'),
('fuck', 'fish -ic "fuck"'), ('fuck', 'fish -ic "fuck"'),
('find', 'find'), ('find', 'find'),
('funced', 'fish -ic "funced"'), ('funced', 'fish -ic "funced"'),
('grep', 'grep'),
('awk', 'awk'), ('awk', 'awk'),
('math "2 + 2"', r'fish -ic "math \"2 + 2\""'), ('math "2 + 2"', r'fish -ic "math \"2 + 2\""'),
('man', 'man'),
('open', 'open'),
('vim', 'vim'), ('vim', 'vim'),
('ll', 'fish -ic "ll"')]) # Fish has no aliases but functions ('ll', 'fish -ic "ll"'),
('ls', 'ls')]) # Fish has no aliases but functions
def test_from_shell(self, before, after, shell): def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after assert shell.from_shell(before) == after
@@ -129,10 +165,12 @@ class TestFish(object):
'fuck': 'fuck', 'fuck': 'fuck',
'funced': 'funced', 'funced': 'funced',
'funcsave': 'funcsave', 'funcsave': 'funcsave',
'grep': 'grep',
'history': 'history', 'history': 'history',
'll': 'll', 'll': 'll',
'math': 'math'} 'math': 'math',
'popd': 'popd',
'pushd': 'pushd',
'ruby': 'ruby'}
def test_app_alias(self, shell): def test_app_alias(self, shell):
assert 'function fuck' in shell.app_alias() assert 'function fuck' in shell.app_alias()
@@ -187,3 +225,7 @@ class TestZsh(object):
assert 'alias fuck' in shell.app_alias() assert 'alias fuck' in shell.app_alias()
assert 'thefuck' in shell.app_alias() assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias() assert 'TF_ALIAS' in shell.app_alias()
def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])
assert list(shell.get_history()) == ['ls', 'rm']

View File

@@ -34,6 +34,15 @@ def test_memoize():
fn.assert_called_once_with() fn.assert_called_once_with()
@pytest.mark.usefixtures('no_memoize')
def test_no_memoize():
fn = Mock(__name__='fn')
memoized = memoize(fn)
memoized()
memoized()
assert fn.call_count == 2
class TestGetClosest(object): class TestGetClosest(object):
def test_when_can_match(self): def test_when_can_match(self):

View File

@@ -29,13 +29,15 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3, 'wait_command': 3,
'require_confirmation': False, 'require_confirmation': False,
'no_colors': False, 'no_colors': False,
'debug': False,
'priority': {}} 'priority': {}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command', 'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation', 'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors', 'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_PRIORITY': 'priority'} 'THEFUCK_PRIORITY': 'priority',
'THEFUCK_DEBUG': 'debug'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
@@ -87,7 +89,7 @@ def _val_from_env(env, attr):
return dict(_priority_from_env(val)) return dict(_priority_from_env(val))
elif attr == 'wait_command': elif attr == 'wait_command':
return int(val) return int(val)
elif attr in ('require_confirmation', 'no_colors'): elif attr in ('require_confirmation', 'no_colors', 'debug'):
return val.lower() == 'true' return val.lower() == 'true'
else: else:
return val return val

View File

@@ -1,3 +1,4 @@
from pprint import pformat
import sys import sys
from traceback import format_exception from traceback import format_exception
import colorama import colorama
@@ -52,3 +53,12 @@ def failed(msg, settings):
msg=msg, msg=msg,
red=color(colorama.Fore.RED, settings), red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings))) reset=color(colorama.Style.RESET_ALL, settings)))
def debug(msg, settings):
if settings.debug:
sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format(
msg=msg,
reset=color(colorama.Style.RESET_ALL, settings),
blue=color(colorama.Fore.BLUE, settings),
bold=color(colorama.Style.BRIGHT, settings)))

View File

@@ -1,6 +1,7 @@
from imp import load_source from imp import load_source
from pathlib import Path from pathlib import Path
from os.path import expanduser from os.path import expanduser
from pprint import pformat
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import os import os
import sys import sys
@@ -79,6 +80,7 @@ def get_command(settings, args):
return return
script = shells.from_shell(script) script = shells.from_shell(script)
logs.debug('Call: {}'.format(script), settings)
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):
@@ -90,6 +92,7 @@ def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command.""" """Returns first matched rule for command."""
for rule in rules: for rule in rules:
try: try:
logs.debug(u'Trying rule: {}'.format(rule.name), settings)
if rule.match(command, settings): if rule.match(command, settings):
return rule return rule
except Exception: except Exception:
@@ -125,12 +128,21 @@ 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)
logs.debug('Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv) command = get_command(settings, sys.argv)
if command: if command:
logs.debug('Received stdout: {}'.format(command.stdout), settings)
logs.debug('Received stderr: {}'.format(command.stderr), settings)
rules = get_rules(user_dir, settings) rules = get_rules(user_dir, settings)
logs.debug(
'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
settings)
matched_rule = get_matched_rule(command, rules, settings) matched_rule = get_matched_rule(command, rules, settings)
if matched_rule: if matched_rule:
logs.debug('Matched rule: {}'.format(matched_rule.name), settings)
run_rule(matched_rule, command, settings) run_rule(matched_rule, command, settings)
return return

View File

@@ -4,8 +4,9 @@ from thefuck.utils import sudo_support
@sudo_support @sudo_support
def match(command, settings): def match(command, settings):
stderr = command.stderr.lower()
return command.script.startswith('cp ') \ return command.script.startswith('cp ') \
and 'cp: omitting directory' in command.stderr.lower() and ('omitting directory' in stderr or 'is a directory' in stderr)
@sudo_support @sudo_support

24
thefuck/rules/history.py Normal file
View File

@@ -0,0 +1,24 @@
from difflib import get_close_matches
from thefuck.shells import get_history
from thefuck.utils import get_closest, memoize
from thefuck.rules.no_command import get_all_callables
@memoize
def _history_of_exists_without_current(command):
callables = get_all_callables()
return [line for line in get_history()
if line != command.script
and line.split(' ')[0] in callables]
def match(command, settings):
return len(get_close_matches(command.script,
_history_of_exists_without_current(command)))
def get_new_command(command, settings):
return get_closest(command.script,
_history_of_exists_without_current(command))
priority = 9999

View File

@@ -1,7 +1,7 @@
from difflib import get_close_matches from difflib import get_close_matches
import os import os
from pathlib import Path from pathlib import Path
from thefuck.utils import sudo_support from thefuck.utils import sudo_support, memoize
from thefuck.shells import thefuck_alias, get_aliases from thefuck.shells import thefuck_alias, get_aliases
@@ -12,7 +12,8 @@ def _safe(fn, fallback):
return fallback return fallback
def _get_all_callables(): @memoize
def get_all_callables():
tf_alias = thefuck_alias() tf_alias = thefuck_alias()
return [exe.name return [exe.name
for path in os.environ.get('PATH', '').split(':') for path in os.environ.get('PATH', '').split(':')
@@ -25,14 +26,14 @@ def _get_all_callables():
def match(command, settings): def match(command, settings):
return 'not found' in command.stderr and \ return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0], bool(get_close_matches(command.script.split(' ')[0],
_get_all_callables())) get_all_callables()))
@sudo_support @sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
old_command = command.script.split(' ')[0] old_command = command.script.split(' ')[0]
new_command = get_close_matches(old_command, new_command = get_close_matches(old_command,
_get_all_callables())[0] get_all_callables())[0]
return ' '.join([new_command] + command.script.split(' ')[1:]) return ' '.join([new_command] + command.script.split(' ')[1:])

View File

@@ -13,7 +13,8 @@ patterns = ['permission denied',
'must be root', 'must be root',
'need to be root', 'need to be root',
'need root', 'need root',
'only root can do that'] 'only root can do that',
'authentication is required']
def match(command, settings): def match(command, settings):

View File

@@ -1,3 +1,4 @@
from thefuck.utils import get_closest
import re import re
@@ -8,7 +9,12 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
cmd = re.match(r"ambiguous command: (.*), could be: ([^, \n]*)", cmd = re.match(r"ambiguous command: (.*), could be: (.*)",
command.stderr) command.stderr)
return command.script.replace(cmd.group(1), cmd.group(2)) old_cmd = cmd.group(1)
suggestions = [cmd.strip() for cmd in cmd.group(2).split(',')]
new_cmd = get_closest(old_cmd, suggestions)
return command.script.replace(old_cmd, new_cmd)

View File

@@ -7,7 +7,9 @@ from collections import defaultdict
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from time import time from time import time
import os import os
import io
from psutil import Process from psutil import Process
import six
from .utils import DEVNULL, memoize from .utils import DEVNULL, memoize
@@ -48,6 +50,26 @@ class Generic(object):
with open(history_file_name, 'a') as history: with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script)) history.write(self._get_history_line(command_script))
def _script_from_history(self, line):
"""Returns prepared history line.
Should return a blank line if history line is corrupted or empty.
"""
return ''
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:
for line in history:
prepared = self._script_from_history(line)\
.strip()
if prepared:
yield prepared
def and_(self, *commands): def and_(self, *commands):
return u' && '.join(commands) return u' && '.join(commands)
@@ -63,7 +85,6 @@ class Bash(Generic):
value = value[1:-1] value = value[1:-1]
return name, value return name, value
@memoize
def get_aliases(self): def get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True) shell=True)
@@ -79,12 +100,24 @@ class Bash(Generic):
def _get_history_line(self, command_script): def _get_history_line(self, command_script):
return u'{}\n'.format(command_script) return u'{}\n'.format(command_script)
def _script_from_history(self, line):
print(line)
return line
class Fish(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): def app_alias(self):
return ("function fuck -d 'Correct your previous console command'\n" return ("set TF_ALIAS fuck\n"
"function fuck -d 'Correct your previous console command'\n"
" set -l exit_code $status\n" " set -l exit_code $status\n"
" set -l TF_ALIAS fuck\n"
" set -l eval_script" " set -l eval_script"
" (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n" " (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n"
" set -l fucked_up_commandd $history[1]\n" " set -l fucked_up_commandd $history[1]\n"
@@ -96,12 +129,12 @@ class Fish(Generic):
" end\n" " end\n"
"end") "end")
@memoize
def get_aliases(self): def get_aliases(self):
overridden = self._get_overridden_aliases()
proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL, proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL,
shell=True) shell=True)
functions = proc.stdout.read().decode('utf-8').strip().split('\n') functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {function: function for function in functions} return {func: func for func in functions if func not in overridden}
def _expand_aliases(self, command_script): def _expand_aliases(self, command_script):
aliases = self.get_aliases() aliases = self.get_aliases()
@@ -137,7 +170,6 @@ class Zsh(Generic):
value = value[1:-1] value = value[1:-1]
return name, value return name, value
@memoize
def get_aliases(self): def get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True) shell=True)
@@ -153,6 +185,12 @@ class Zsh(Generic):
def _get_history_line(self, command_script): def _get_history_line(self, command_script):
return u': {}:0;{}\n'.format(int(time()), 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 ''
class Tcsh(Generic): class Tcsh(Generic):
def app_alias(self): def app_alias(self):
@@ -162,7 +200,6 @@ class Tcsh(Generic):
name, value = alias.split("\t", 1) name, value = alias.split("\t", 1)
return name, value return name, value
@memoize
def get_aliases(self): def get_aliases(self):
proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True) shell=True)
@@ -219,5 +256,11 @@ def and_(*commands):
return _get_shell().and_(*commands) return _get_shell().and_(*commands)
@memoize
def get_aliases(): def get_aliases():
return list(_get_shell().get_aliases().keys()) return list(_get_shell().get_aliases().keys())
@memoize
def get_history():
return list(_get_shell().get_history())

View File

@@ -80,12 +80,13 @@ def memoize(fn):
@wraps(fn) @wraps(fn)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs)) key = pickle.dumps((args, kwargs))
if key not in memo: if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs) memo[key] = fn(*args, **kwargs)
return memo[key] return memo[key]
return wrapper return wrapper
memoize.disabled = False
def get_closest(word, possibilities, n=3, cutoff=0.6): def get_closest(word, possibilities, n=3, cutoff=0.6):