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

This commit is contained in:
mcarton
2015-09-01 14:19:53 +02:00
50 changed files with 509 additions and 224 deletions
+4 -20
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)
+3 -7
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',
+2
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')
+3 -3
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)
+2
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 ')
+4 -3
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
+4 -4
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):
+3 -2
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
+7 -4
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):
+6 -5
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):
+3 -2
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)))
+3 -3
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():
+2
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'))
+5 -2
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):
+3 -3
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():
+3 -3
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
+10 -7
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):
+11 -8
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):
+2 -1
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
+5 -3
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):
+6 -8
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):
+11
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']
@@ -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 []
+12 -12
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):
+4 -1
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
+3 -2
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):
+3 -3
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):
+5 -1
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 = (
+3 -2
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)
+3 -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)
+3 -2
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)
+3 -3
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)
+3 -1
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):
+22 -27
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)
+15 -17
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
+80 -4
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)
+1 -3
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')
+28 -11
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)