1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-02 16:12:08 +00:00

Compare commits

..

16 Commits
1.26 ... 1.27

Author SHA1 Message Date
nvbn
69ddd82bae Bump to 1.27 2015-04-22 16:46:06 +02:00
nvbn
e7b78205f4 Add transparent sudo support for rules where it required 2015-04-22 16:45:38 +02:00
nvbn
7010b3a7f6 #43 Add test for rm_root 2015-04-22 16:22:10 +02:00
nvbn
3a9c2cc204 Merge branch 'SpyCheese-patch-1' 2015-04-22 16:09:09 +02:00
nvbn
fa4e4522b7 #43 Add rm_root as disabled by default rule 2015-04-22 16:08:54 +02:00
nvbn
14ef5c7d1c Merge branch 'patch-1' of git://github.com/SpyCheese/thefuck into SpyCheese-patch-1 2015-04-22 16:03:20 +02:00
nvbn
957209bdb6 Add ability to bundle disabled by default rules 2015-04-22 15:59:44 +02:00
nvbn
8376fed459 Merge branch 'master' of github.com:nvbn/thefuck 2015-04-22 06:03:34 +02:00
nvbn
5d424dad88 Use colorama for colored output 2015-04-22 06:03:06 +02:00
nvbn
126194ec2e Put errors in stderr instead of "echo ..." in stdout 2015-04-22 05:29:44 +02:00
Vladimir Iakovlev
6b54a3a072 Merge pull request #88 from Dugucloud/master
Added sudo rule for Fedora yum's output.
2015-04-22 05:15:24 +02:00
Dugucloud
79fb7c987c Added sudo rule for Fedora yum's output. 2015-04-22 09:26:45 +08:00
秋纫
d2356c570e Merge pull request #1 from nvbn/master
Synchronize with nvbn's repo.
2015-04-22 09:23:20 +08:00
SpyCheese
ceeccf1cd7 Update rm_root.py
Okay, there was an incorrect match function.
2015-04-19 10:21:46 +05:00
SpyCheese
f113bae59d Update rm_root.py 2015-04-19 09:12:19 +05:00
SpyCheese
2a79a5e413 Create rm_root.py 2015-04-19 09:03:34 +05:00
18 changed files with 222 additions and 45 deletions

View File

@@ -161,6 +161,10 @@ using matched rule and run it. Rules enabled by default:
* `sudo` – prepends `sudo` to previous command if it failed because of permissions;
* `switch_layout` – switches command from your local layout to en.
Bundled, but not enabled by default:
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules
For adding your own rule you should create `your-rule-name.py`
@@ -193,7 +197,8 @@ The Fuck has a few settings parameters, they can be changed in `~/.thefuck/setti
* `rules` – list of enabled rules, by default all;
* `require_confirmation` – require confirmation before running new command, by default `False`;
* `wait_command` – max amount of time in seconds for getting previous command output.
* `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output.
## Developing

View File

@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.26'
VERSION = '1.27'
setup(name='thefuck',
@@ -15,6 +15,6 @@ setup(name='thefuck',
'tests', 'release']),
include_package_data=True,
zip_safe=False,
install_requires=['pathlib', 'psutil'],
install_requires=['pathlib', 'psutil', 'colorama', 'six'],
entry_points={'console_scripts': [
'thefuck = thefuck.main:main']})

View File

@@ -0,0 +1,18 @@
from mock import Mock
from thefuck.rules.rm_root import match, get_new_command
def test_match():
assert match(Mock(script='rm -rf /',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='ls',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='rm --no-preserve-root /',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='rm -rf /',
stderr=''), None)
def test_get_new_command():
assert get_new_command(Mock(script='rm -rf /'), None) \
== 'rm -rf / --no-preserve-root'

7
tests/test_logs.py Normal file
View File

@@ -0,0 +1,7 @@
from mock import Mock
from thefuck import logs
def test_color():
assert logs.color('red', Mock(no_colors=False)) == 'red'
assert logs.color('red', Mock(no_colors=True)) == ''

View File

@@ -12,9 +12,14 @@ def test_get_settings():
def test_is_rule_enabled():
assert main.is_rule_enabled(Mock(rules=None), Path('bash.py'))
assert main.is_rule_enabled(Mock(rules=['bash']), Path('bash.py'))
assert not main.is_rule_enabled(Mock(rules=['bash']), Path('lisp.py'))
assert main.is_rule_enabled(Mock(rules=None),
main.Rule('bash', None, None, True))
assert not main.is_rule_enabled(Mock(rules=None),
main.Rule('bash', None, None, False))
assert main.is_rule_enabled(Mock(rules=['bash']),
main.Rule('bash', None, None, True))
assert not main.is_rule_enabled(Mock(rules=['bash']),
main.Rule('lisp', None, None, True))
def test_load_rule():
@@ -23,26 +28,29 @@ def test_load_rule():
with patch('thefuck.main.load_source',
return_value=Mock(
match=match,
get_new_command=get_new_command)) as load_source:
assert main.load_rule(Path('/rules/bash.py')) == main.Rule('bash', match, get_new_command)
get_new_command=get_new_command,
enabled_by_default=True)) as load_source:
assert main.load_rule(Path('/rules/bash.py')) \
== main.Rule('bash', match, get_new_command, True)
load_source.assert_called_once_with('bash', '/rules/bash.py')
def test_get_rules():
with patch('thefuck.main.Path.glob') as glob, \
patch('thefuck.main.load_source',
lambda x, _: Mock(match=x, get_new_command=x)):
lambda x, _: Mock(match=x, get_new_command=x,
enabled_by_default=True)):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
assert main.get_rules(
assert list(main.get_rules(
Path('~'),
Mock(rules=None)) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('lisp', 'lisp', 'lisp'),
main.Rule('bash', 'bash', 'bash'),
main.Rule('lisp', 'lisp', 'lisp')]
assert main.get_rules(
Mock(rules=None))) == [main.Rule('bash', 'bash', 'bash', True),
main.Rule('lisp', 'lisp', 'lisp', True),
main.Rule('bash', 'bash', 'bash', True),
main.Rule('lisp', 'lisp', 'lisp', True)]
assert list(main.get_rules(
Path('~'),
Mock(rules=['bash'])) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('bash', 'bash', 'bash')]
Mock(rules=['bash']))) == [main.Rule('bash', 'bash', 'bash', True),
main.Rule('bash', 'bash', 'bash', True)]
def test_get_command():
@@ -65,24 +73,24 @@ def test_get_command():
def test_get_matched_rule(capsys):
rules = [main.Rule('', lambda x, _: x.script == 'cd ..', None),
main.Rule('', lambda *_: False, None),
main.Rule('rule', Mock(side_effect=OSError('Denied')), None)]
rules = [main.Rule('', lambda x, _: x.script == 'cd ..', None, True),
main.Rule('', lambda *_: False, None, True),
main.Rule('rule', Mock(side_effect=OSError('Denied')), None, True)]
assert main.get_matched_rule(main.Command('ls', '', ''),
rules, None) is None
rules, Mock(no_colors=True)) is None
assert main.get_matched_rule(main.Command('cd ..', '', ''),
rules, None) == rules[0]
assert capsys.readouterr()[1].split('\n')[0]\
== '[WARN] rule: Traceback (most recent call last):'
rules, Mock(no_colors=True)) == rules[0]
assert capsys.readouterr()[1].split('\n')[0] \
== '[WARN] Rule rule:'
def test_run_rule(capsys):
with patch('thefuck.main.confirm', return_value=True):
main.run_rule(main.Rule('', None, lambda *_: 'new-command'),
main.run_rule(main.Rule('', None, lambda *_: 'new-command', True),
None, None)
assert capsys.readouterr() == ('new-command\n', '')
with patch('thefuck.main.confirm', return_value=False):
main.run_rule(main.Rule('', None, lambda *_: 'new-command'),
main.run_rule(main.Rule('', None, lambda *_: 'new-command', True),
None, None)
assert capsys.readouterr() == ('', '')
@@ -93,9 +101,11 @@ def test_confirm(capsys):
assert capsys.readouterr() == ('', 'command\n')
# When confirmation required and confirmed:
with patch('thefuck.main.sys.stdin.read', return_value='\n'):
assert main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]')
assert main.confirm('command', Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]')
# When confirmation required and ctrl+c:
with patch('thefuck.main.sys.stdin.read', side_effect=KeyboardInterrupt):
assert not main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]Aborted\n')
assert not main.confirm('command', Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n')

17
tests/test_utils.py Normal file
View File

@@ -0,0 +1,17 @@
from mock import Mock
from thefuck.utils import sudo_support
from thefuck.main import Command
def test_sudo_support():
fn = Mock(return_value=True, __name__='')
assert sudo_support(fn)(Command('sudo ls', 'out', 'err'), None)
fn.assert_called_once_with(Command('ls', 'out', 'err'), None)
fn.return_value = False
assert not sudo_support(fn)(Command('sudo ls', 'out', 'err'), None)
fn.return_value = 'pwd'
assert sudo_support(fn)(Command('sudo ls', 'out', 'err'), None) == 'sudo pwd'
assert sudo_support(fn)(Command('ls', 'out', 'err'), None) == 'pwd'

47
thefuck/logs.py Normal file
View File

@@ -0,0 +1,47 @@
import sys
from traceback import format_exception
import colorama
def color(color_, settings):
"""Utility for ability to disabling colored output."""
if settings.no_colors:
return ''
else:
return color_
def rule_failed(rule, exc_info, settings):
sys.stderr.write(
u'{warn}[WARN] Rule {name}:{reset}\n{trace}'
u'{warn}----------------------------{reset}\n\n'.format(
warn=color(colorama.Back.RED + colorama.Fore.WHITE
+ colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings),
name=rule.name,
trace=''.join(format_exception(*exc_info))))
def show_command(new_command, settings):
sys.stderr.write('{bold}{command}{reset}\n'.format(
command=new_command,
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, settings):
sys.stderr.write(
'{bold}{command}{reset} [{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command,
bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
sys.stderr.flush()
def failed(msg, settings):
sys.stderr.write('{red}{msg}{reset}\n'.format(
msg=msg,
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))

View File

@@ -5,12 +5,14 @@ from os.path import expanduser
from subprocess import Popen, PIPE
import os
import sys
from traceback import format_exception
from psutil import Process, TimeoutExpired
import colorama
from thefuck import logs
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default'))
def setup_user_dir():
@@ -30,6 +32,7 @@ def get_settings(user_dir):
settings.__dict__.setdefault('rules', None)
settings.__dict__.setdefault('wait_command', 3)
settings.__dict__.setdefault('require_confirmation', False)
settings.__dict__.setdefault('no_colors', False)
return settings
@@ -38,14 +41,20 @@ def is_rule_enabled(settings, rule):
isn't defined.
"""
return settings.rules is None or rule.name[:-3] in settings.rules
if settings.rules is None and rule.enabled_by_default:
return True
elif settings.rules and rule.name in settings.rules:
return True
else:
return False
def load_rule(rule):
"""Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule))
return Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command)
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True))
def get_rules(user_dir, settings):
@@ -54,8 +63,11 @@ def get_rules(user_dir, settings):
.joinpath('rules')\
.glob('*.py')
user = user_dir.joinpath('rules').glob('*.py')
return [load_rule(rule) for rule in sorted(list(bundled)) + list(user)
if rule.name != '__init__.py' and is_rule_enabled(settings, rule)]
for rule in sorted(list(bundled)) + list(user):
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if is_rule_enabled(settings, loaded_rule):
yield loaded_rule
def wait_output(settings, popen):
@@ -100,23 +112,21 @@ def get_matched_rule(command, rules, settings):
if rule.match(command, settings):
return rule
except Exception:
sys.stderr.write(u'[WARN] {}: {}---------------------\n\n'.format(
rule.name, ''.join(format_exception(*sys.exc_info()))))
logs.rule_failed(rule, sys.exc_info(), settings)
def confirm(new_command, settings):
"""Returns `True` when running of new command confirmed."""
if not settings.require_confirmation:
sys.stderr.write(new_command + '\n')
logs.show_command(new_command, settings)
return True
sys.stderr.write(new_command + ' [Enter/Ctrl+C]')
sys.stderr.flush()
logs.confirm_command(new_command, settings)
try:
sys.stdin.read(1)
return True
except KeyboardInterrupt:
sys.stderr.write('Aborted\n')
logs.failed('Aborted', settings)
return False
@@ -133,13 +143,14 @@ def is_second_run(command):
def main():
colorama.init()
user_dir = setup_user_dir()
settings = get_settings(user_dir)
command = get_command(settings, sys.argv)
if command:
if is_second_run(command):
print("echo Can't fuck twice")
logs.failed("Can't fuck twice", settings)
return
rules = get_rules(user_dir, settings)
@@ -148,4 +159,4 @@ def main():
run_rule(matched_rule, command, settings)
return
print('echo No fuck given')
logs.failed('No fuck given', settings)

View File

@@ -1,10 +1,13 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return command.script.startswith('cp ') \
and 'cp: omitting directory' in command.stderr.lower()
@sudo_support
def get_new_command(command, settings):
return re.sub(r'^cp', 'cp -a', command.script)

View File

@@ -1,11 +1,14 @@
import os
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return os.path.exists(command.script.split()[0]) \
and 'command not found' in command.stderr
@sudo_support
def get_new_command(command, settings):
return u'./{}'.format(command.script)

View File

@@ -1,12 +1,15 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return (command.script.startswith('lein')
and "is not a task. See 'lein help'" in command.stderr
and 'Did you mean this?' in command.stderr)
@sudo_support
def get_new_command(command, settings):
broken_cmd = re.findall(r"'([^']*)' is not a task",
command.stderr)[0]

View File

@@ -1,9 +1,13 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return ('mkdir' in command.script
and 'No such file or directory' in command.stderr)
@sudo_support
def get_new_command(command, settings):
return re.sub('^mkdir (.*)', 'mkdir -p \\1', command.script)

View File

@@ -1,6 +1,7 @@
from difflib import get_close_matches
import os
from pathlib import Path
from thefuck.utils import sudo_support
def _safe(fn, fallback):
@@ -17,12 +18,14 @@ def _get_all_bins():
if not _safe(exe.is_dir, True)]
@sudo_support
def match(command, settings):
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
_get_all_bins()))
@sudo_support
def get_new_command(command, settings):
old_command = command.script.split(' ')[0]
new_command = get_close_matches(old_command,

View File

@@ -1,7 +1,10 @@
from thefuck.utils import sudo_support
# add 'python' suffix to the command if
# 1) The script does not have execute permission or
# 2) is interpreted as shell script
@sudo_support
def match(command, settings):
toks = command.script.split()
return (len(toks) > 0
@@ -10,5 +13,6 @@ def match(command, settings):
'command not found' in command.stderr))
@sudo_support
def get_new_command(command, settings):
return 'python ' + command.script

View File

@@ -1,9 +1,13 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return ('rm' in command.script
and 'is a directory' in command.stderr)
@sudo_support
def get_new_command(command, settings):
return re.sub('^rm (.*)', 'rm -rf \\1', command.script)

16
thefuck/rules/rm_root.py Normal file
View File

@@ -0,0 +1,16 @@
from thefuck.utils import sudo_support
enabled_by_default = False
@sudo_support
def match(command, settings):
return ({'rm', '/'}.issubset(command.script.split())
and '--no-preserve-root' not in command.script
and '--no-preserve-root' in command.stderr)
@sudo_support
def get_new_command(command, settings):
return u'{} --no-preserve-root'.format(command.script)

View File

@@ -5,7 +5,8 @@ patterns = ['permission denied',
'non-root users cannot',
'Operation not permitted',
'root privilege',
'This command has to be run under the root user.']
'This command has to be run under the root user.',
'You need to be root to perform this command.']
def match(command, settings):

View File

@@ -1,5 +1,7 @@
from functools import wraps
import os
import six
from thefuck.main import Command
def which(program):
@@ -41,3 +43,22 @@ def wrap_settings(params):
return fn(command, settings)
return wrapper
return decorator
def sudo_support(fn):
"""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)
result = fn(Command(command.script[5:],
command.stdout,
command.stderr),
settings)
if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result)
else:
return result
return wrapper