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,13 +1,14 @@
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()
or 'cd: can\'t cd to' in command.stderr.lower()))
return (('no such file or directory' in command.stderr.lower()
or 'cd: can\'t cd to' in command.stderr.lower()))
@sudo_support

View File

@ -1,11 +1,11 @@
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()
or 'did you mean one of these?' in command.stderr.lower()))
return (('did you mean this?' in command.stderr.lower()
or 'did you mean one of these?' in command.stderr.lower()))
def get_new_command(command, settings):

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 '
'ISO C++ 2011 standard.' in command.stderr or
'-Wc++11-extensions' in command.stderr))
return ('This file requires compiler and library support for the '
'ISO C++ 2011 standard.' in command.stderr or
'-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,19 +21,19 @@ 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)
def get_new_command(command, settings):
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):

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
and '(did you mean one of ' in command.stderr
or "hg: command '" in command.stderr
and "' is ambiguous:" 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)
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.
# 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
or '.net' in command.script
or '.org' in command.script
or '.ly' in command.script
or '.io' in command.script
or '.se' in command.script
or '.edu' in command.script
or '.info' in command.script
or '.me' in command.script
or 'www.' in command.script))
return ('.com' in command.script
or '.net' in command.script
or '.org' in command.script
or '.ly' in command.script
or '.io' in command.script
or '.se' in command.script
or '.edu' in command.script
or '.info' in command.script
or '.me' 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,37 +1,32 @@
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'))
# 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
if not is_app(command, 'git', 'hub'):
return False
if not is_git_cmd:
return False
# perform git aliases expansion
if 'trace: alias expansion:' in command.stderr:
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)",
command.stderr)
alias = search.group(1)
# perform git aliases expansion
if 'trace: alias expansion:' in command.stderr:
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)",
command.stderr)
alias = search.group(1)
# by default git quotes everything, for example:
# '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)
# by default git quotes everything, for example:
# '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)
command = Command._replace(command, script=new_script)
return fn(command, settings)
return wrapper
return fn(command, settings)

View File

@ -1,24 +1,22 @@
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)
if not command.script.startswith('sudo '):
return fn(command, settings)
result = fn(Command(command.script[5:],
command.stdout,
command.stderr),
settings)
result = fn(Command(command.script[5:],
command.stdout,
command.stderr),
settings)
if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result)
elif isinstance(result, list):
return [u'sudo {}'.format(x) for x in result]
else:
return result
return wrapper
if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result)
elif isinstance(result, list):
return [u'sudo {}'.format(x) for x in result]
else:
return result

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):
return fn(command, settings.update(**params))
return wrapper
return decorator
def _wrap_settings(fn, command, settings):
return fn(command, settings.update(**params))
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):
return list(fn(*args, **kwargs))
return wrapper
@decorator
def eager(fn, *args, **kwargs):
return list(fn(*args, **kwargs))
@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)