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`;
* `mercurial` – fixes wrong `hg` commands;
* `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_such_file` – creates missing directories with `mv` and `cp` commands;
* `open` – prepends `http` to address passed to `open`;

View File

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

View File

@ -1,6 +1,12 @@
import pytest
from mock import Mock
@pytest.fixture
def no_memoize(monkeypatch):
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
from mock import Mock
from thefuck.rules.lein_not_task import match, get_new_command
from tests.utils import Command
@pytest.fixture
@ -14,10 +14,10 @@ Did you mean this?
def test_match(is_not_task):
assert match(Mock(script='lein rpl', stderr=is_not_task), None)
assert not match(Mock(script='ls', stderr=is_not_task), None)
assert match(Command(script='lein rpl', stderr=is_not_task), None)
assert not match(Command(script='ls', stderr=is_not_task), None)
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']

View File

@ -1,16 +1,16 @@
from mock import patch, Mock
from thefuck.rules.ls_lah import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Mock(script='ls'), None)
assert match(Mock(script='ls file.py'), None)
assert match(Mock(script='ls /opt'), None)
assert not match(Mock(script='ls -lah /opt'), None)
assert not match(Mock(script='pacman -S binutils'), None)
assert not match(Mock(script='lsof'), None)
assert match(Command(script='ls'), None)
assert match(Command(script='ls file.py'), None)
assert match(Command(script='ls /opt'), None)
assert not match(Command(script='ls -lah /opt'), None)
assert not match(Command(script='pacman -S binutils'), None)
assert not match(Command(script='lsof'), None)
def test_get_new_command():
assert get_new_command(Mock(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 file.py'), None) == 'ls -lah file.py'
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, 'ls', 'ls', False)])
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
fn.assert_called_once_with(Command(called), None)

View File

@ -3,7 +3,7 @@ from pathlib import PosixPath, Path
from mock import Mock
from thefuck import corrector, conf, types
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):
@ -75,15 +75,6 @@ class TestGetCorrectedCommands(object):
== [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):
command = Command('test', 'test', 'test')
rules = [Rule(match=lambda *_: False),
@ -94,4 +85,4 @@ def test_get_corrected_commands(mocker):
priority=60)]
mocker.patch('thefuck.corrector.get_rules', return_value=rules)
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 tests.utils import Rule
from thefuck.types import RulesNamesList, Settings, \
SortedCorrectedCommandsSequence
from tests.utils import Rule, CorrectedCommand
def test_rules_names_list():
@ -15,3 +16,44 @@ def test_update_settings():
assert new_settings.key == 'val'
assert new_settings.unset == 'unset-value'
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
from itertools import islice
from thefuck import ui
from thefuck.types import CorrectedCommand
from thefuck.types import CorrectedCommand, SortedCorrectedCommandsSequence
@pytest.fixture
@ -58,14 +58,18 @@ def test_command_selector():
class TestSelectCommand(object):
@pytest.fixture
def commands_with_side_effect(self):
return [CorrectedCommand('ls', lambda *_: None, 100),
CorrectedCommand('cd', lambda *_: None, 100)]
def commands_with_side_effect(self, settings):
return SortedCorrectedCommandsSequence(
iter([CorrectedCommand('ls', lambda *_: None, 100),
CorrectedCommand('cd', lambda *_: None, 100)]),
settings)
@pytest.fixture
def commands(self):
return [CorrectedCommand('ls', None, 100),
CorrectedCommand('cd', None, 100)]
def commands(self, settings):
return SortedCorrectedCommandsSequence(
iter([CorrectedCommand('ls', None, 100),
CorrectedCommand('cd', None, 100)]),
settings)
def test_without_commands(self, capsys):
assert ui.select_command([], Mock(debug=False, no_color=True)) is None
@ -92,13 +96,6 @@ class TestSelectCommand(object):
require_confirmation=True)) == commands[0]
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):
patch_getch([KeyboardInterrupt])
assert ui.select_command(commands,

View File

@ -2,8 +2,9 @@ import pytest
from mock import Mock
from thefuck.utils import wrap_settings,\
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 tests.utils import Command
@pytest.mark.parametrize('override, old, new', [
@ -93,3 +94,25 @@ def test_replace_argument(args, result):
'service-status', 'service-unbind'])])
def test_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 .utils import eager
from . import conf, types, logs
from imp import load_source
from pathlib import Path
from thefuck.types import CorrectedCommand, Rule
@ -29,17 +28,16 @@ def get_loaded_rules(rules, settings):
yield loaded_rule
@eager
def get_rules(user_dir, settings):
"""Returns all enabled rules."""
bundled = Path(__file__).parent \
.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):
"""Returns first matched rule for command."""
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)
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):
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)
logs.debug(
u'Matched rules: {}'.format(', '.join(rule.name for rule in matched)),
settings)
corrected_commands = make_corrected_commands(command, matched, settings)
return sorted(remove_duplicates(corrected_commands),
key=lambda corrected_command: corrected_command.priority)
return types.SortedCorrectedCommandsSequence(corrected_commands, settings)

View File

@ -45,15 +45,11 @@ def show_corrected_command(corrected_command, settings):
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_text(corrected_command, multiple_cmds, settings):
if multiple_cmds:
arrows = '{blue}{reset}/{blue}{reset}/'
else:
arrows = ''
def confirm_text(corrected_command, settings):
sys.stderr.write(
('{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,
side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r',

View File

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

View File

@ -1,10 +1,10 @@
import re
from thefuck.utils import replace_argument
from thefuck.utils import replace_argument, for_app
@for_app('cargo')
def match(command, settings):
return ('cargo' in command.script
and 'No such subcommand' in command.stderr
return ('No such subcommand' 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 thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir
from thefuck.utils import for_app
__author__ = "mmussomele"
@ -16,6 +17,7 @@ def _get_sub_dirs(parent):
@sudo_support
@for_app('cd')
def match(command, settings):
"""Match function copied from cd_mkdir.py"""
return (command.script.startswith('cd ')

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
from thefuck.utils import for_app
@for_app(['g++', 'clang++'])
def match(command, settings):
return (('g++' in command.script or 'clang++' in command.script) and
('This file requires compiler and library support for the '
return ('This file requires compiler and library support for the '
'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):

View File

@ -1,6 +1,7 @@
from thefuck import shells
import os
import tarfile
from thefuck import shells
from thefuck.utils import for_app
def _is_tar_extract(cmd):
@ -20,12 +21,12 @@ def _tar_file(cmd):
for c in cmd.split():
for ext in tar_extensions:
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):
return (command.script.startswith('tar')
and '-C' not in command.script
return ('-C' not in command.script
and _is_tar_extract(command.script)
and _tar_file(command.script) is not None)

View File

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

View File

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

View File

@ -1,3 +1,4 @@
from thefuck.utils import for_app
# Appends .go when compiling go files
#
# Example:
@ -5,6 +6,7 @@
# error: go run: no go files listed
@for_app('go')
def match(command, settings):
return (command.script.startswith('go run ')
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):
return (command.script.startswith('grep')
and 'is a directory' in command.stderr.lower())
return 'is a directory' in command.stderr.lower()
def get_new_command(command, settings):

View File

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

View File

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

View File

@ -1,13 +1,16 @@
# Fixes common java command mistake
#
# Example:
# > java foo.java
# Error: Could not find or load main class foo.java
"""Fixes common java command mistake
Example:
> java 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):
return (command.script.startswith('java ')
and command.script.endswith('.java'))
return command.script.endswith('.java')
def get_new_command(command, settings):

View File

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

View File

@ -1,9 +1,10 @@
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
@sudo_support
@for_app('lein')
def match(command, settings):
return (command.script.startswith('lein')
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):
return (command.script == 'ls'
or command.script.startswith('ls ')
and 'ls -' not in command.script)
return 'ls -' not in command.script
def get_new_command(command, settings):

View File

@ -1,5 +1,5 @@
import re
from thefuck.utils import get_closest
from thefuck.utils import get_closest, for_app
def extract_possibilities(command):
@ -12,14 +12,12 @@ def extract_possibilities(command):
return possib
@for_app('hg')
def match(command, settings):
return (command.script.startswith('hg ')
and ('hg: unknown command' in command.stderr
return ('hg: unknown command' in command.stderr
and '(did you mean one of ' 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):

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,12 +5,12 @@
# The file ~/github.com does not exist.
# 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):
return (command.script.startswith(('open', 'xdg-open', 'gnome-open', 'kde-open'))
and (
'.com' in command.script
return ('.com' in command.script
or '.net' in command.script
or '.org' in command.script
or '.ly' in command.script
@ -19,7 +19,7 @@ def match(command, settings):
or '.edu' in command.script
or '.info' in command.script
or '.me' in command.script
or 'www.' in command.script))
or 'www.' in command.script)
def get_new_command(command, settings):

View File

@ -1,7 +1,10 @@
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):
return ('pip' in command.script and
'unknown command' in command.stderr and

View File

@ -3,11 +3,12 @@
# Example:
# > python foo
# 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):
return (command.script.startswith('python ')
and not command.script.endswith('.py'))
return not command.script.endswith('.py')
def get_new_command(command, settings):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
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):
return (command.script.startswith('tsuru ')
and ' is not a tsuru command. See "tsuru help".' in command.stderr
return (' is not a tsuru command. See "tsuru help".' in command.stderr
and '\nDid you mean?\n\t' in command.stderr)

View File

@ -1,8 +1,10 @@
from thefuck import shells
from thefuck.utils import for_app
@for_app('vagrant')
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):

View File

@ -1,20 +1,17 @@
from functools import wraps
import re
from shlex import split
from decorator import decorator
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."""
@wraps(fn)
def wrapper(command, settings):
# supports GitHub's `hub` command
# which is recommended to be used with `alias git=hub`
# but at this point, shell aliases have already been resolved
is_git_cmd = command.script.startswith(('git', 'hub'))
if not is_git_cmd:
if not is_app(command, 'git', 'hub'):
return False
# perform git aliases expansion
@ -33,5 +30,3 @@ def git_support(fn):
command = Command._replace(command, script=new_script)
return fn(command, settings)
return wrapper

View File

@ -1,12 +1,11 @@
from functools import wraps
import six
from decorator import decorator
from ..types import Command
def sudo_support(fn):
@decorator
def sudo_support(fn, command, settings):
"""Removes sudo before calling fn and adds it after."""
@wraps(fn)
def wrapper(command, settings):
if not command.script.startswith('sudo '):
return fn(command, settings)
@ -21,4 +20,3 @@ def sudo_support(fn):
return [u'sudo {}'.format(x) for x in result]
else:
return result
return wrapper

View File

@ -1,14 +1,34 @@
from collections import namedtuple
from traceback import format_stack
from .logs import debug
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
CorrectedCommand = namedtuple('CorrectedCommand', ('script', 'side_effect', 'priority'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default', 'side_effect',
'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):
"""Wrapper a top of list for storing rules names."""
@ -18,7 +38,6 @@ class RulesNamesList(list):
class Settings(dict):
def __getattr__(self, item):
return self.get(item)
@ -29,3 +48,60 @@ class Settings(dict):
conf = dict(kwargs)
conf.update(self)
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)
return selector.value
multiple_cmds = len(corrected_commands) > 1
selector.on_change(lambda val: logs.confirm_text(val, multiple_cmds, settings))
selector.on_change(lambda val: logs.confirm_text(val, settings))
for action in read_actions():
if action == SELECT:
sys.stderr.write('\n')

View File

@ -1,5 +1,6 @@
from difflib import get_close_matches
from functools import wraps
from decorator import decorator
import os
import pickle
@ -64,12 +65,9 @@ def wrap_settings(params):
print(settings.apt)
"""
def decorator(fn):
@wraps(fn)
def wrapper(command, settings):
def _wrap_settings(fn, command, settings):
return fn(command, settings.update(**params))
return wrapper
return decorator
return decorator(_wrap_settings)
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)
def eager(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
@decorator
def eager(fn, *args, **kwargs):
return list(fn(*args, **kwargs))
return wrapper
@eager
@ -133,3 +129,24 @@ def replace_command(command, broken, matched):
new_cmds = get_close_matches(broken, matched, cutoff=0.1)
return [replace_argument(command.script, broken, new_cmd.strip())
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)