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

Merge branch 'master' of github.com:nvbn/thefuck into slow

This commit is contained in:
mcarton 2015-09-01 14:19:53 +02:00
commit 8b62959fe3
50 changed files with 509 additions and 224 deletions

View File

@ -181,6 +181,8 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `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; * `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;
* `mvn_no_command` – adds `clean package` to `mvn`;
* `mvn_unknown_lifecycle_phase` – fixes miss spelt lifecycle phases with `mvn`;
* `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;
* `open` – prepends `http` to address passed to `open`; * `open` – prepends `http` to address passed to `open`;

View File

@ -22,7 +22,7 @@ elif (3, 0) < version < (3, 3):
VERSION = '2.8' VERSION = '2.8'
install_requires = ['psutil', 'colorama', 'six'] install_requires = ['psutil', 'colorama', 'six', 'decorator']
extras_require = {':python_version<"3.4"': ['pathlib']} extras_require = {':python_version<"3.4"': ['pathlib']}
setup(name='thefuck', setup(name='thefuck',

View File

@ -1,6 +1,12 @@
import pytest import pytest
from mock import Mock
@pytest.fixture @pytest.fixture
def no_memoize(monkeypatch): def no_memoize(monkeypatch):
monkeypatch.setattr('thefuck.utils.memoize.disabled', True) monkeypatch.setattr('thefuck.utils.memoize.disabled', True)
@pytest.fixture
def settings():
return Mock(debug=False, no_colors=True)

View File

@ -1,6 +1,6 @@
import pytest import pytest
from mock import Mock
from thefuck.rules.lein_not_task import match, get_new_command from thefuck.rules.lein_not_task import match, get_new_command
from tests.utils import Command
@pytest.fixture @pytest.fixture
@ -14,10 +14,10 @@ Did you mean this?
def test_match(is_not_task): def test_match(is_not_task):
assert match(Mock(script='lein rpl', stderr=is_not_task), None) assert match(Command(script='lein rpl', stderr=is_not_task), None)
assert not match(Mock(script='ls', stderr=is_not_task), None) assert not match(Command(script='ls', stderr=is_not_task), None)
def test_get_new_command(is_not_task): def test_get_new_command(is_not_task):
assert get_new_command(Mock(script='lein rpl --help', stderr=is_not_task), assert get_new_command(Command(script='lein rpl --help', stderr=is_not_task),
None) == ['lein repl --help', 'lein jar --help'] None) == ['lein repl --help', 'lein jar --help']

View File

@ -1,16 +1,16 @@
from mock import patch, Mock
from thefuck.rules.ls_lah import match, get_new_command from thefuck.rules.ls_lah import match, get_new_command
from tests.utils import Command
def test_match(): def test_match():
assert match(Mock(script='ls'), None) assert match(Command(script='ls'), None)
assert match(Mock(script='ls file.py'), None) assert match(Command(script='ls file.py'), None)
assert match(Mock(script='ls /opt'), None) assert match(Command(script='ls /opt'), None)
assert not match(Mock(script='ls -lah /opt'), None) assert not match(Command(script='ls -lah /opt'), None)
assert not match(Mock(script='pacman -S binutils'), None) assert not match(Command(script='pacman -S binutils'), None)
assert not match(Mock(script='lsof'), None) assert not match(Command(script='lsof'), None)
def test_get_new_command(): def test_get_new_command():
assert get_new_command(Mock(script='ls file.py'), None) == 'ls -lah file.py' assert get_new_command(Command(script='ls file.py'), None) == 'ls -lah file.py'
assert get_new_command(Mock(script='ls'), None) == 'ls -lah' assert get_new_command(Command(script='ls'), None) == 'ls -lah'

View File

@ -0,0 +1,40 @@
import pytest
from thefuck.rules.mvn_no_command import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='mvn clean', stdout="""
[INFO] Scanning for projects...[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building test 0.2
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ test ---
[INFO] Deleting /home/mlk/code/test/target
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.477s
[INFO] Finished at: Wed Aug 26 13:05:47 BST 2015
[INFO] Final Memory: 6M/240M
[INFO] ------------------------------------------------------------------------
"""),
Command(script='mvn --help'),
Command(script='mvn -v')
])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package', 'mvn clean install']),
(Command(script='mvn -N', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn -N clean package', 'mvn -N clean install'])])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@ -0,0 +1,40 @@
import pytest
from thefuck.rules.mvn_unknown_lifecycle_phase import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='mvn clean', stdout="""
[INFO] Scanning for projects...[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building test 0.2
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ test ---
[INFO] Deleting /home/mlk/code/test/target
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.477s
[INFO] Finished at: Wed Aug 26 13:05:47 BST 2015
[INFO] Final Memory: 6M/240M
[INFO] ------------------------------------------------------------------------
"""),
Command(script='mvn --help'),
Command(script='mvn -v')
])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean', 'mvn compile']),
(Command(script='mvn claen package', stdout='[ERROR] Unknown lifecycle phase "claen". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package'])])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@ -13,6 +13,8 @@ from tests.utils import Command
(False, 'sudo ls', 'ls', False), (False, 'sudo ls', 'ls', False),
(False, 'ls', 'ls', False)]) (False, 'ls', 'ls', False)])
def test_sudo_support(return_value, command, called, result): def test_sudo_support(return_value, command, called, result):
fn = Mock(return_value=return_value, __name__='') def fn(command, settings):
assert command == Command(called)
return return_value
assert sudo_support(fn)(Command(command), None) == result assert sudo_support(fn)(Command(command), None) == result
fn.assert_called_once_with(Command(called), None)

View File

@ -3,7 +3,7 @@ from pathlib import PosixPath, Path
from mock import Mock from mock import Mock
from thefuck import corrector, conf, types from thefuck import corrector, conf, types
from tests.utils import Rule, Command, CorrectedCommand from tests.utils import Rule, Command, CorrectedCommand
from thefuck.corrector import make_corrected_commands, get_corrected_commands, remove_duplicates from thefuck.corrector import make_corrected_commands, get_corrected_commands
def test_load_rule(mocker): def test_load_rule(mocker):
@ -75,15 +75,6 @@ class TestGetCorrectedCommands(object):
== [CorrectedCommand(script='test!', priority=100)] == [CorrectedCommand(script='test!', priority=100)]
def test_remove_duplicates():
side_effect = lambda *_: None
assert set(remove_duplicates([CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', priority=200),
CorrectedCommand('ls', side_effect, 300)])) \
== {CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', side_effect, 300)}
def test_get_corrected_commands(mocker): def test_get_corrected_commands(mocker):
command = Command('test', 'test', 'test') command = Command('test', 'test', 'test')
rules = [Rule(match=lambda *_: False), rules = [Rule(match=lambda *_: False),
@ -94,4 +85,4 @@ def test_get_corrected_commands(mocker):
priority=60)] priority=60)]
mocker.patch('thefuck.corrector.get_rules', return_value=rules) mocker.patch('thefuck.corrector.get_rules', return_value=rules)
assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \ assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \
== ['test@', 'test!', 'test;'] == ['test!', 'test@', 'test;']

View File

@ -1,5 +1,6 @@
from thefuck.types import RulesNamesList, Settings from thefuck.types import RulesNamesList, Settings, \
from tests.utils import Rule SortedCorrectedCommandsSequence
from tests.utils import Rule, CorrectedCommand
def test_rules_names_list(): def test_rules_names_list():
@ -15,3 +16,44 @@ def test_update_settings():
assert new_settings.key == 'val' assert new_settings.key == 'val'
assert new_settings.unset == 'unset-value' assert new_settings.unset == 'unset-value'
assert settings.key == 'val' assert settings.key == 'val'
class TestSortedCorrectedCommandsSequence(object):
def test_realises_generator_only_on_demand(self, settings):
should_realise = False
def gen():
nonlocal should_realise
yield CorrectedCommand('git commit')
yield CorrectedCommand('git branch', priority=200)
assert should_realise
yield CorrectedCommand('git checkout', priority=100)
commands = SortedCorrectedCommandsSequence(gen(), settings)
assert commands[0] == CorrectedCommand('git commit')
should_realise = True
assert commands[1] == CorrectedCommand('git checkout', priority=100)
assert commands[2] == CorrectedCommand('git branch', priority=200)
def test_remove_duplicates(self, settings):
side_effect = lambda *_: None
seq = SortedCorrectedCommandsSequence(
iter([CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', priority=200),
CorrectedCommand('ls', side_effect, 300)]),
settings)
assert set(seq) == {CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', side_effect, 300)}
class TestCorrectedCommand(object):
def test_equality(self):
assert CorrectedCommand('ls', None, 100) == \
CorrectedCommand('ls', None, 200)
assert CorrectedCommand('ls', None, 100) != \
CorrectedCommand('ls', lambda *_: _, 100)
def test_hashable(self):
assert {CorrectedCommand('ls', None, 100),
CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')}

View File

@ -4,7 +4,7 @@ from mock import Mock
import pytest import pytest
from itertools import islice from itertools import islice
from thefuck import ui from thefuck import ui
from thefuck.types import CorrectedCommand from thefuck.types import CorrectedCommand, SortedCorrectedCommandsSequence
@pytest.fixture @pytest.fixture
@ -58,14 +58,18 @@ def test_command_selector():
class TestSelectCommand(object): class TestSelectCommand(object):
@pytest.fixture @pytest.fixture
def commands_with_side_effect(self): def commands_with_side_effect(self, settings):
return [CorrectedCommand('ls', lambda *_: None, 100), return SortedCorrectedCommandsSequence(
CorrectedCommand('cd', lambda *_: None, 100)] iter([CorrectedCommand('ls', lambda *_: None, 100),
CorrectedCommand('cd', lambda *_: None, 100)]),
settings)
@pytest.fixture @pytest.fixture
def commands(self): def commands(self, settings):
return [CorrectedCommand('ls', None, 100), return SortedCorrectedCommandsSequence(
CorrectedCommand('cd', None, 100)] iter([CorrectedCommand('ls', None, 100),
CorrectedCommand('cd', None, 100)]),
settings)
def test_without_commands(self, capsys): def test_without_commands(self, capsys):
assert ui.select_command([], Mock(debug=False, no_color=True)) is None assert ui.select_command([], Mock(debug=False, no_color=True)) is None
@ -92,13 +96,6 @@ class TestSelectCommand(object):
require_confirmation=True)) == commands[0] require_confirmation=True)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_one_match(self, capsys, patch_getch, commands):
patch_getch(['\n'])
assert ui.select_command((commands[0],),
Mock(debug=False, no_color=True,
require_confirmation=True)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/ctrl+c]\n')
def test_with_confirmation_abort(self, capsys, patch_getch, commands): def test_with_confirmation_abort(self, capsys, patch_getch, commands):
patch_getch([KeyboardInterrupt]) patch_getch([KeyboardInterrupt])
assert ui.select_command(commands, assert ui.select_command(commands,

View File

@ -2,8 +2,9 @@ import pytest
from mock import Mock from mock import Mock
from thefuck.utils import wrap_settings,\ from thefuck.utils import wrap_settings,\
memoize, get_closest, get_all_executables, replace_argument, \ memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands get_all_matched_commands, is_app, for_app
from thefuck.types import Settings from thefuck.types import Settings
from tests.utils import Command
@pytest.mark.parametrize('override, old, new', [ @pytest.mark.parametrize('override, old, new', [
@ -93,3 +94,25 @@ def test_replace_argument(args, result):
'service-status', 'service-unbind'])]) 'service-status', 'service-unbind'])])
def test_get_all_matched_commands(stderr, result): def test_get_all_matched_commands(stderr, result):
assert list(get_all_matched_commands(stderr)) == result assert list(get_all_matched_commands(stderr)) == result
@pytest.mark.usefixtures('no_memoize')
@pytest.mark.parametrize('script, names, result', [
('git diff', ['git', 'hub'], True),
('hub diff', ['git', 'hub'], True),
('hg diff', ['git', 'hub'], False)])
def test_is_app(script, names, result):
assert is_app(Command(script), *names) == result
@pytest.mark.usefixtures('no_memoize')
@pytest.mark.parametrize('script, names, result', [
('git diff', ['git', 'hub'], True),
('hub diff', ['git', 'hub'], True),
('hg diff', ['git', 'hub'], False)])
def test_for_app(script, names, result):
@for_app(*names)
def match(command, settings):
return True
assert match(Command(script), None) == result

View File

@ -1,5 +1,4 @@
from . import conf, logs from . import conf, types, logs
from .utils import eager
from imp import load_source from imp import load_source
from pathlib import Path from pathlib import Path
from thefuck.types import CorrectedCommand, Rule from thefuck.types import CorrectedCommand, Rule
@ -29,17 +28,16 @@ def get_loaded_rules(rules, settings):
yield loaded_rule yield loaded_rule
@eager
def get_rules(user_dir, settings): def get_rules(user_dir, settings):
"""Returns all enabled rules.""" """Returns all enabled rules."""
bundled = Path(__file__).parent \ bundled = Path(__file__).parent \
.joinpath('rules') \ .joinpath('rules') \
.glob('*.py') .glob('*.py')
user = user_dir.joinpath('rules').glob('*.py') user = user_dir.joinpath('rules').glob('*.py')
return get_loaded_rules(sorted(bundled) + sorted(user), settings) return sorted(get_loaded_rules(sorted(bundled) + sorted(user), settings),
key=lambda rule: rule.priority)
@eager
def get_matched_rules(command, rules, settings): def get_matched_rules(command, rules, settings):
"""Returns first matched rule for command.""" """Returns first matched rule for command."""
script_only = command.stdout is None and command.stderr is None script_only = command.stdout is None and command.stderr is None
@ -68,22 +66,8 @@ def make_corrected_commands(command, rules, settings):
priority=(n + 1) * rule.priority) priority=(n + 1) * rule.priority)
def remove_duplicates(corrected_commands):
commands = {(command.script, command.side_effect): command
for command in sorted(corrected_commands,
key=lambda command: -command.priority)}
return commands.values()
def get_corrected_commands(command, user_dir, settings): def get_corrected_commands(command, user_dir, settings):
rules = get_rules(user_dir, settings) rules = get_rules(user_dir, settings)
logs.debug(
u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
settings)
matched = get_matched_rules(command, rules, settings) matched = get_matched_rules(command, rules, settings)
logs.debug(
u'Matched rules: {}'.format(', '.join(rule.name for rule in matched)),
settings)
corrected_commands = make_corrected_commands(command, matched, settings) corrected_commands = make_corrected_commands(command, matched, settings)
return sorted(remove_duplicates(corrected_commands), return types.SortedCorrectedCommandsSequence(corrected_commands, settings)
key=lambda corrected_command: corrected_command.priority)

View File

@ -45,15 +45,11 @@ def show_corrected_command(corrected_command, settings):
reset=color(colorama.Style.RESET_ALL, settings))) reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_text(corrected_command, multiple_cmds, settings): def confirm_text(corrected_command, settings):
if multiple_cmds:
arrows = '{blue}{reset}/{blue}{reset}/'
else:
arrows = ''
sys.stderr.write( sys.stderr.write(
('{clear}{bold}{script}{reset}{side_effect} ' ('{clear}{bold}{script}{reset}{side_effect} '
'[{green}enter{reset}/' + arrows + '{red}ctrl+c{reset}]').format( '[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
'/{red}ctrl+c{reset}]').format(
script=corrected_command.script, script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '', side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r', clear='\033[1K\r',

View File

@ -1,6 +1,8 @@
import re import re
from thefuck.utils import for_app
@for_app('apt-get')
def match(command, settings): def match(command, settings):
return command.script.startswith('apt-get search') return command.script.startswith('apt-get search')

View File

@ -1,10 +1,10 @@
import re import re
from thefuck.utils import replace_argument from thefuck.utils import replace_argument, for_app
@for_app('cargo')
def match(command, settings): def match(command, settings):
return ('cargo' in command.script return ('No such subcommand' in command.stderr
and 'No such subcommand' in command.stderr
and 'Did you mean' in command.stderr) and 'Did you mean' in command.stderr)

View File

@ -4,6 +4,7 @@ import os
from difflib import get_close_matches from difflib import get_close_matches
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir from thefuck.rules import cd_mkdir
from thefuck.utils import for_app
__author__ = "mmussomele" __author__ = "mmussomele"
@ -16,6 +17,7 @@ def _get_sub_dirs(parent):
@sudo_support @sudo_support
@for_app('cd')
def match(command, settings): def match(command, settings):
"""Match function copied from cd_mkdir.py""" """Match function copied from cd_mkdir.py"""
return (command.script.startswith('cd ') return (command.script.startswith('cd ')

View File

@ -1,13 +1,14 @@
import re import re
from thefuck import shells from thefuck import shells
from thefuck.utils import for_app
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support
@for_app('cd')
def match(command, settings): def match(command, settings):
return (command.script.startswith('cd ') return (('no such file or directory' in command.stderr.lower()
and ('no such file or directory' in command.stderr.lower() or 'cd: can\'t cd to' in command.stderr.lower()))
or 'cd: can\'t cd to' in command.stderr.lower()))
@sudo_support @sudo_support

View File

@ -1,11 +1,11 @@
import re import re
from thefuck.utils import replace_argument from thefuck.utils import replace_argument, for_app
@for_app('composer')
def match(command, settings): def match(command, settings):
return ('composer' in command.script return (('did you mean this?' in command.stderr.lower()
and ('did you mean this?' in command.stderr.lower() or 'did you mean one of these?' in command.stderr.lower()))
or 'did you mean one of these?' in command.stderr.lower()))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,12 +1,13 @@
import re import re
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app
@sudo_support @sudo_support
@for_app('cp')
def match(command, settings): def match(command, settings):
stderr = command.stderr.lower() stderr = command.stderr.lower()
return command.script.startswith('cp ') \ return 'omitting directory' in stderr or 'is a directory' in stderr
and ('omitting directory' in stderr or 'is a directory' in stderr)
@sudo_support @sudo_support

View File

@ -1,8 +1,11 @@
from thefuck.utils import for_app
@for_app(['g++', 'clang++'])
def match(command, settings): def match(command, settings):
return (('g++' in command.script or 'clang++' in command.script) and return ('This file requires compiler and library support for the '
('This file requires compiler and library support for the ' 'ISO C++ 2011 standard.' in command.stderr or
'ISO C++ 2011 standard.' in command.stderr or '-Wc++11-extensions' in command.stderr)
'-Wc++11-extensions' in command.stderr))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,6 +1,7 @@
from thefuck import shells
import os import os
import tarfile import tarfile
from thefuck import shells
from thefuck.utils import for_app
def _is_tar_extract(cmd): def _is_tar_extract(cmd):
@ -20,19 +21,19 @@ def _tar_file(cmd):
for c in cmd.split(): for c in cmd.split():
for ext in tar_extensions: for ext in tar_extensions:
if c.endswith(ext): if c.endswith(ext):
return (c, c[0:len(c)-len(ext)]) return (c, c[0:len(c) - len(ext)])
@for_app('tar')
def match(command, settings): def match(command, settings):
return (command.script.startswith('tar') return ('-C' not in command.script
and '-C' not in command.script
and _is_tar_extract(command.script) and _is_tar_extract(command.script)
and _tar_file(command.script) is not None) and _tar_file(command.script) is not None)
def get_new_command(command, settings): def get_new_command(command, settings):
return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \ return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \
.format(dir=_tar_file(command.script)[1], cmd=command.script) .format(dir=_tar_file(command.script)[1], cmd=command.script)
def side_effect(old_cmd, command, settings): def side_effect(old_cmd, command, settings):

View File

@ -1,5 +1,6 @@
import os import os
import zipfile import zipfile
from thefuck.utils import for_app
def _is_bad_zip(file): def _is_bad_zip(file):
@ -20,9 +21,9 @@ def _zip_file(command):
return '{}.zip'.format(c) return '{}.zip'.format(c)
@for_app('unzip')
def match(command, settings): def match(command, settings):
return (command.script.startswith('unzip') return ('-d' not in command.script
and '-d' not in command.script
and _is_bad_zip(_zip_file(command))) and _is_bad_zip(_zip_file(command)))

View File

@ -1,14 +1,14 @@
from itertools import dropwhile, takewhile, islice from itertools import dropwhile, takewhile, islice
import re import re
import subprocess import subprocess
from thefuck.utils import replace_command from thefuck.utils import replace_command, for_app
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support
@for_app('docker')
def match(command, settings): def match(command, settings):
return command.script.startswith('docker') \ return 'is not a docker command' in command.stderr
and 'is not a docker command' in command.stderr
def get_docker_commands(): def get_docker_commands():

View File

@ -1,3 +1,4 @@
from thefuck.utils import for_app
# Appends .go when compiling go files # Appends .go when compiling go files
# #
# Example: # Example:
@ -5,6 +6,7 @@
# error: go run: no go files listed # error: go run: no go files listed
@for_app('go')
def match(command, settings): def match(command, settings):
return (command.script.startswith('go run ') return (command.script.startswith('go run ')
and not command.script.endswith('.go')) and not command.script.endswith('.go'))

View File

@ -1,6 +1,9 @@
from thefuck.utils import for_app
@for_app('grep')
def match(command, settings): def match(command, settings):
return (command.script.startswith('grep') return 'is a directory' in command.stderr.lower()
and 'is a directory' in command.stderr.lower())
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,11 +1,11 @@
import re import re
import subprocess import subprocess
from thefuck.utils import replace_command from thefuck.utils import replace_command, for_app
@for_app('gulp')
def match(command, script): def match(command, script):
return command.script.startswith('gulp')\ return 'is not in your gulpfile' in command.stdout
and 'is not in your gulpfile' in command.stdout
def get_gulp_tasks(): def get_gulp_tasks():

View File

@ -1,10 +1,10 @@
import re import re
from thefuck.utils import replace_command from thefuck.utils import replace_command, for_app
@for_app('heroku')
def match(command, settings): def match(command, settings):
return command.script.startswith('heroku') and \ return 'is not a heroku command' in command.stderr and \
'is not a heroku command' in command.stderr and \
'Perhaps you meant' in command.stderr 'Perhaps you meant' in command.stderr

View File

@ -1,13 +1,16 @@
# Fixes common java command mistake """Fixes common java command mistake
#
# Example: Example:
# > java foo.java > java foo.java
# Error: Could not find or load main class foo.java Error: Could not find or load main class foo.java
"""
from thefuck.utils import for_app
@for_app('java')
def match(command, settings): def match(command, settings):
return (command.script.startswith('java ') return command.script.endswith('.java')
and command.script.endswith('.java'))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,14 +1,17 @@
# Appends .java when compiling java files """Appends .java when compiling java files
#
# Example: Example:
# > javac foo > javac foo
# error: Class names, 'foo', are only accepted if annotation error: Class names, 'foo', are only accepted if annotation
# processing is explicitly requested processing is explicitly requested
"""
from thefuck.utils import for_app
@for_app('javac')
def match(command, settings): def match(command, settings):
return (command.script.startswith('javac ') return not command.script.endswith('.java')
and not command.script.endswith('.java'))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,9 +1,10 @@
import re import re
from thefuck.utils import replace_command, get_all_matched_commands from thefuck.utils import replace_command, get_all_matched_commands, for_app
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support
@for_app('lein')
def match(command, settings): def match(command, settings):
return (command.script.startswith('lein') return (command.script.startswith('lein')
and "is not a task. See 'lein help'" in command.stderr and "is not a task. See 'lein help'" in command.stderr

View File

@ -1,7 +1,9 @@
from thefuck.utils import for_app
@for_app('ls')
def match(command, settings): def match(command, settings):
return (command.script == 'ls' return 'ls -' not in command.script
or command.script.startswith('ls ')
and 'ls -' not in command.script)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,5 +1,5 @@
import re import re
from thefuck.utils import get_closest from thefuck.utils import get_closest, for_app
def extract_possibilities(command): def extract_possibilities(command):
@ -12,14 +12,12 @@ def extract_possibilities(command):
return possib return possib
@for_app('hg')
def match(command, settings): def match(command, settings):
return (command.script.startswith('hg ') return ('hg: unknown command' in command.stderr
and ('hg: unknown command' in command.stderr and '(did you mean one of ' in command.stderr
and '(did you mean one of ' in command.stderr or "hg: command '" in command.stderr
or "hg: command '" in command.stderr and "' is ambiguous:" in command.stderr)
and "' is ambiguous:" in command.stderr
)
)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -0,0 +1,11 @@
from thefuck.utils import for_app
@for_app('mvn')
def match(command, settings):
return 'No goals have been specified for this build' in command.stdout
def get_new_command(command, settings):
return [command.script + ' clean package',
command.script + ' clean install']

View File

@ -0,0 +1,32 @@
from thefuck.utils import replace_command, for_app
from difflib import get_close_matches
import re
def _get_failed_lifecycle(command):
return re.search(r'\[ERROR\] Unknown lifecycle phase "(.+)"',
command.stdout)
def _getavailable_lifecycles(command):
return re.search(
r'Available lifecycle phases are: (.+) -> \[Help 1\]', command.stdout)
@for_app('mvn')
def match(command, settings):
failed_lifecycle = _get_failed_lifecycle(command)
available_lifecycles = _getavailable_lifecycles(command)
return available_lifecycles and failed_lifecycle
def get_new_command(command, settings):
failed_lifecycle = _get_failed_lifecycle(command)
available_lifecycles = _getavailable_lifecycles(command)
if available_lifecycles and failed_lifecycle:
selected_lifecycle = get_close_matches(
failed_lifecycle.group(1), available_lifecycles.group(1).split(", "),
3, 0.6)
return replace_command(command, failed_lifecycle.group(1), selected_lifecycle)
else:
return []

View File

@ -5,21 +5,21 @@
# The file ~/github.com does not exist. # The file ~/github.com does not exist.
# Perhaps you meant 'http://github.com'? # Perhaps you meant 'http://github.com'?
# #
from thefuck.utils import for_app
@for_app('open', 'xdg-open', 'gnome-open', 'kde-open')
def match(command, settings): def match(command, settings):
return (command.script.startswith(('open', 'xdg-open', 'gnome-open', 'kde-open')) return ('.com' in command.script
and ( or '.net' in command.script
'.com' in command.script or '.org' in command.script
or '.net' in command.script or '.ly' in command.script
or '.org' in command.script or '.io' in command.script
or '.ly' in command.script or '.se' in command.script
or '.io' in command.script or '.edu' in command.script
or '.se' in command.script or '.info' in command.script
or '.edu' in command.script or '.me' in command.script
or '.info' in command.script or 'www.' in command.script)
or '.me' in command.script
or 'www.' in command.script))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,7 +1,10 @@
import re import re
from thefuck.utils import replace_argument from thefuck.utils import replace_argument, for_app
from thefuck.specific.sudo import sudo_support
@sudo_support
@for_app('pip')
def match(command, settings): def match(command, settings):
return ('pip' in command.script and return ('pip' in command.script and
'unknown command' in command.stderr and 'unknown command' in command.stderr and

View File

@ -3,11 +3,12 @@
# Example: # Example:
# > python foo # > python foo
# error: python: can't open file 'foo': [Errno 2] No such file or directory # error: python: can't open file 'foo': [Errno 2] No such file or directory
from thefuck.utils import for_app
@for_app('python')
def match(command, settings): def match(command, settings):
return (command.script.startswith('python ') return not command.script.endswith('.py')
and not command.script.endswith('.py'))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,10 +1,10 @@
import shlex import shlex
from thefuck.utils import quote from thefuck.utils import quote, for_app
@for_app('sed')
def match(command, settings): def match(command, settings):
return ('sed' in command.script return "unterminated `s' command" in command.stderr
and "unterminated `s' command" in command.stderr)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,10 +1,14 @@
import re import re
from thefuck.utils import for_app
commands = ('ssh', 'scp')
@for_app(*commands)
def match(command, settings): def match(command, settings):
if not command.script: if not command.script:
return False return False
if not command.script.startswith(('ssh', 'scp')): if not command.script.startswith(commands):
return False return False
patterns = ( patterns = (

View File

@ -2,15 +2,16 @@
The confusion in systemctl's param order is massive. The confusion in systemctl's param order is massive.
""" """
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app
@sudo_support @sudo_support
@for_app('systemctl')
def match(command, settings): def match(command, settings):
# Catches 'Unknown operation 'service'.' when executing systemctl with # Catches 'Unknown operation 'service'.' when executing systemctl with
# misordered arguments # misordered arguments
cmd = command.script.split() cmd = command.script.split()
return ('systemctl' in command.script and return ('Unknown operation \'' in command.stderr and
'Unknown operation \'' in command.stderr and
len(cmd) - cmd.index('systemctl') == 3) len(cmd) - cmd.index('systemctl') == 3)

View File

@ -1,10 +1,10 @@
from thefuck.utils import replace_command
import re import re
from thefuck.utils import replace_command, for_app
@for_app('tmux')
def match(command, settings): def match(command, settings):
return ('tmux' in command.script return ('ambiguous command:' in command.stderr
and 'ambiguous command:' in command.stderr
and 'could be:' in command.stderr) and 'could be:' in command.stderr)

View File

@ -1,9 +1,10 @@
from thefuck import shells from thefuck import shells
from thefuck.utils import for_app
@for_app('tsuru')
def match(command, settings): def match(command, settings):
return (command.script.startswith('tsuru') return ('not authenticated' in command.stderr
and 'not authenticated' in command.stderr
and 'session has expired' in command.stderr) and 'session has expired' in command.stderr)

View File

@ -1,10 +1,10 @@
import re import re
from thefuck.utils import get_all_matched_commands, replace_command from thefuck.utils import get_all_matched_commands, replace_command, for_app
@for_app('tsuru')
def match(command, settings): def match(command, settings):
return (command.script.startswith('tsuru ') return (' is not a tsuru command. See "tsuru help".' in command.stderr
and ' is not a tsuru command. See "tsuru help".' in command.stderr
and '\nDid you mean?\n\t' in command.stderr) and '\nDid you mean?\n\t' in command.stderr)

View File

@ -1,8 +1,10 @@
from thefuck import shells from thefuck import shells
from thefuck.utils import for_app
@for_app('vagrant')
def match(command, settings): def match(command, settings):
return command.script.startswith('vagrant ') and 'run `vagrant up`' in command.stderr.lower() return 'run `vagrant up`' in command.stderr.lower()
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@ -1,37 +1,32 @@
from functools import wraps
import re import re
from shlex import split from shlex import split
from decorator import decorator
from ..types import Command from ..types import Command
from ..utils import quote from ..utils import quote, is_app
def git_support(fn): @decorator
def git_support(fn, command, settings):
"""Resolves git aliases and supports testing for both git and hub.""" """Resolves git aliases and supports testing for both git and hub."""
@wraps(fn) # supports GitHub's `hub` command
def wrapper(command, settings): # which is recommended to be used with `alias git=hub`
# supports GitHub's `hub` command # but at this point, shell aliases have already been resolved
# which is recommended to be used with `alias git=hub` if not is_app(command, 'git', 'hub'):
# but at this point, shell aliases have already been resolved return False
is_git_cmd = command.script.startswith(('git', 'hub'))
if not is_git_cmd: # perform git aliases expansion
return False if 'trace: alias expansion:' in command.stderr:
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)",
command.stderr)
alias = search.group(1)
# perform git aliases expansion # by default git quotes everything, for example:
if 'trace: alias expansion:' in command.stderr: # 'commit' '--amend'
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)", # which is surprising and does not allow to easily test for
command.stderr) # eg. 'git commit'
alias = search.group(1) expansion = ' '.join(map(quote, split(search.group(2))))
new_script = command.script.replace(alias, expansion)
# by default git quotes everything, for example: command = Command._replace(command, script=new_script)
# 'commit' '--amend'
# which is surprising and does not allow to easily test for
# eg. 'git commit'
expansion = ' '.join(map(quote, split(search.group(2))))
new_script = command.script.replace(alias, expansion)
command = Command._replace(command, script=new_script) return fn(command, settings)
return fn(command, settings)
return wrapper

View File

@ -1,24 +1,22 @@
from functools import wraps
import six import six
from decorator import decorator
from ..types import Command from ..types import Command
def sudo_support(fn): @decorator
def sudo_support(fn, command, settings):
"""Removes sudo before calling fn and adds it after.""" """Removes sudo before calling fn and adds it after."""
@wraps(fn) if not command.script.startswith('sudo '):
def wrapper(command, settings): return fn(command, settings)
if not command.script.startswith('sudo '):
return fn(command, settings)
result = fn(Command(command.script[5:], result = fn(Command(command.script[5:],
command.stdout, command.stdout,
command.stderr), command.stderr),
settings) settings)
if result and isinstance(result, six.string_types): if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result) return u'sudo {}'.format(result)
elif isinstance(result, list): elif isinstance(result, list):
return [u'sudo {}'.format(x) for x in result] return [u'sudo {}'.format(x) for x in result]
else: else:
return result return result
return wrapper

View File

@ -1,14 +1,34 @@
from collections import namedtuple from collections import namedtuple
from traceback import format_stack
from .logs import debug
Command = namedtuple('Command', ('script', 'stdout', 'stderr')) Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
CorrectedCommand = namedtuple('CorrectedCommand', ('script', 'side_effect', 'priority'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default', 'side_effect', 'enabled_by_default', 'side_effect',
'priority', 'requires_output')) 'priority', 'requires_output'))
class CorrectedCommand(object):
def __init__(self, script, side_effect, priority):
self.script = script
self.side_effect = side_effect
self.priority = priority
def __eq__(self, other):
"""Ignores `priority` field."""
if isinstance(other, CorrectedCommand):
return (other.script, other.side_effect) ==\
(self.script, self.side_effect)
else:
return False
def __hash__(self):
return (self.script, self.side_effect).__hash__()
def __repr__(self):
return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
class RulesNamesList(list): class RulesNamesList(list):
"""Wrapper a top of list for storing rules names.""" """Wrapper a top of list for storing rules names."""
@ -18,7 +38,6 @@ class RulesNamesList(list):
class Settings(dict): class Settings(dict):
def __getattr__(self, item): def __getattr__(self, item):
return self.get(item) return self.get(item)
@ -29,3 +48,60 @@ class Settings(dict):
conf = dict(kwargs) conf = dict(kwargs)
conf.update(self) conf.update(self)
return Settings(conf) return Settings(conf)
class SortedCorrectedCommandsSequence(object):
"""List-like collection/wrapper around generator, that:
- immediately gives access to the first commands through [];
- realises generator and sorts commands on first access to other
commands through [], or when len called.
"""
def __init__(self, commands, settings):
self._settings = settings
self._commands = commands
self._cached = self._realise_first()
self._realised = False
def _realise_first(self):
try:
return [next(self._commands)]
except StopIteration:
return []
def _remove_duplicates(self, corrected_commands):
"""Removes low-priority duplicates."""
commands = {command
for command in sorted(corrected_commands,
key=lambda command: -command.priority)
if command.script != self._cached[0]}
return commands
def _realise(self):
"""Realises generator, removes duplicates and sorts commands."""
commands = self._remove_duplicates(self._commands)
self._cached = [self._cached[0]] + sorted(
commands, key=lambda corrected_command: corrected_command.priority)
self._realised = True
debug('SortedCommandsSequence was realised with: {}, after: {}'.format(
self._cached, '\n'.join(format_stack())), self._settings)
def __getitem__(self, item):
if item != 0 and not self._realised:
self._realise()
return self._cached[item]
def __bool__(self):
return bool(self._cached)
def __len__(self):
if not self._realised:
self._realise()
return len(self._cached)
def __iter__(self):
if not self._realised:
self._realise()
return iter(self._cached)

View File

@ -88,9 +88,7 @@ def select_command(corrected_commands, settings):
logs.show_corrected_command(selector.value, settings) logs.show_corrected_command(selector.value, settings)
return selector.value return selector.value
multiple_cmds = len(corrected_commands) > 1 selector.on_change(lambda val: logs.confirm_text(val, settings))
selector.on_change(lambda val: logs.confirm_text(val, multiple_cmds, settings))
for action in read_actions(): for action in read_actions():
if action == SELECT: if action == SELECT:
sys.stderr.write('\n') sys.stderr.write('\n')

View File

@ -1,5 +1,6 @@
from difflib import get_close_matches from difflib import get_close_matches
from functools import wraps from functools import wraps
from decorator import decorator
import os import os
import pickle import pickle
@ -64,12 +65,9 @@ def wrap_settings(params):
print(settings.apt) print(settings.apt)
""" """
def decorator(fn): def _wrap_settings(fn, command, settings):
@wraps(fn) return fn(command, settings.update(**params))
def wrapper(command, settings): return decorator(_wrap_settings)
return fn(command, settings.update(**params))
return wrapper
return decorator
def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True): def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True):
@ -111,11 +109,9 @@ def replace_argument(script, from_, to):
u' {} '.format(from_), u' {} '.format(to), 1) u' {} '.format(from_), u' {} '.format(to), 1)
def eager(fn): @decorator
@wraps(fn) def eager(fn, *args, **kwargs):
def wrapper(*args, **kwargs): return list(fn(*args, **kwargs))
return list(fn(*args, **kwargs))
return wrapper
@eager @eager
@ -133,3 +129,24 @@ def replace_command(command, broken, matched):
new_cmds = get_close_matches(broken, matched, cutoff=0.1) new_cmds = get_close_matches(broken, matched, cutoff=0.1)
return [replace_argument(command.script, broken, new_cmd.strip()) return [replace_argument(command.script, broken, new_cmd.strip())
for new_cmd in new_cmds] for new_cmd in new_cmds]
@memoize
def is_app(command, *app_names):
"""Returns `True` if command is call to one of passed app names."""
for name in app_names:
if command.script == name \
or command.script.startswith(u'{} '.format(name)):
return True
return False
def for_app(*app_names):
"""Specifies that matching script is for on of app names."""
def _for_app(fn, command, settings):
if is_app(command, *app_names):
return fn(command, settings)
else:
return False
return decorator(_for_app)