diff --git a/README.md b/README.md index 56e7bb83..a5128559 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ REPL-y 0.3.1 ... ``` -If you are scary to blindly run changed command, there's `require_confirmation` -[settings](#Settings) option: +If you are scared to blindly run changed command, there's `require_confirmation` +[settings](#settings) option: ```bash ➜ apt-get install vim diff --git a/release.py b/release.py new file mode 100755 index 00000000..e22209b6 --- /dev/null +++ b/release.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +from subprocess import call +import re + + +version = None + + +def get_new_setup_py_lines(): + global version + with open('setup.py', 'r') as sf: + current_setup = sf.readlines() + for line in current_setup: + if line.startswith('VERSION = '): + major, minor = re.findall(r"VERSION = '(\d+)\.(\d+)'", line)[0] + version = "{}.{}".format(major, int(minor) + 1) + yield "VERSION = '{}'\n".format(version) + else: + yield line + + +lines = list(get_new_setup_py_lines()) +with open('setup.py', 'w') as sf: + sf.writelines(lines) + +call('git pull', shell=True) +call('git commit -am "Bump to {}"'.format(version), shell=True) +call('git tag {}'.format(version), shell=True) +call('git push', shell=True) +call('git push --tags', shell=True) +call('python setup.py sdist upload', shell=True) diff --git a/setup.py b/setup.py index a552b9d6..d19f72dc 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,18 @@ from setuptools import setup, find_packages +VERSION = '1.26' + + setup(name='thefuck', - version="1.22", + version=VERSION, description="Magnificent app which corrects your previous console command", author='Vladimir Iakovlev', author_email='nvbn.rm@gmail.com', url='https://github.com/nvbn/thefuck', license='MIT', - packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + packages=find_packages(exclude=['ez_setup', 'examples', + 'tests', 'release']), include_package_data=True, zip_safe=False, install_requires=['pathlib', 'psutil'], diff --git a/tests/rules/test_cd_parent.py b/tests/rules/test_cd_parent.py new file mode 100644 index 00000000..7a0fbc8b --- /dev/null +++ b/tests/rules/test_cd_parent.py @@ -0,0 +1,12 @@ +from thefuck.main import Command +from thefuck.rules.cd_parent import match, get_new_command + + +def test_match(): + assert match(Command('cd..', '', 'cd..: command not found'), None) + assert not match(Command('', '', ''), None) + + +def test_get_new_command(): + assert get_new_command( + Command('cd..', '', ''), None) == 'cd ..' diff --git a/tests/rules/test_ssh_known_host.py b/tests/rules/test_ssh_known_host.py new file mode 100644 index 00000000..9c8dc0d1 --- /dev/null +++ b/tests/rules/test_ssh_known_host.py @@ -0,0 +1,69 @@ +import os +import pytest +from mock import Mock +from thefuck.main import Command +from thefuck.rules.ssh_known_hosts import match, get_new_command, remove_offending_keys + + +@pytest.fixture +def ssh_error(tmpdir): + path = os.path.join(str(tmpdir), 'known_hosts') + + def reset(path): + with open(path, 'w') as fh: + lines = [ + '123.234.567.890 asdjkasjdakjsd\n' + '98.765.432.321 ejioweojwejrosj\n' + '111.222.333.444 qwepoiwqepoiss\n' + ] + fh.writelines(lines) + + def known_hosts(path): + with open(path, 'r') as fh: + return fh.readlines() + + reset(path) + + errormsg = u"""@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! +Someone could be eavesdropping on you right now (man-in-the-middle attack)! +It is also possible that a host key has just been changed. +The fingerprint for the RSA key sent by the remote host is +b6:cb:07:34:c0:a0:94:d3:0d:69:83:31:f4:c5:20:9b. +Please contact your system administrator. +Add correct host key in {0} to get rid of this message. +Offending RSA key in {0}:2 +RSA host key for {1} has changed and you have requested strict checking. +Host key verification failed.""".format(path, '98.765.432.321') + + return errormsg, path, reset, known_hosts + + +def test_match(ssh_error): + errormsg, _, _, _ = ssh_error + assert match(Command('ssh', '', errormsg), None) + assert match(Command('ssh', '', errormsg), None) + assert match(Command('scp something something', '', errormsg), None) + assert match(Command('scp something something', '', errormsg), None) + assert not match(Command('', '', errormsg), None) + assert not match(Command('notssh', '', errormsg), None) + assert not match(Command('ssh', '', ''), None) + + +def test_remove_offending_keys(ssh_error): + errormsg, path, reset, known_hosts = ssh_error + command = Command('ssh user@host', '', errormsg) + remove_offending_keys(command, None) + expected = ['123.234.567.890 asdjkasjdakjsd\n', '111.222.333.444 qwepoiwqepoiss\n'] + assert known_hosts(path) == expected + + +def test_get_new_command(ssh_error, monkeypatch): + errormsg, _, _, _ = ssh_error + + method = Mock() + monkeypatch.setattr('thefuck.rules.ssh_known_hosts.remove_offending_keys', method) + assert get_new_command(Command('ssh user@host', '', errormsg), None) == 'ssh user@host' + assert method.call_count diff --git a/tests/rules/test_switch_lang.py b/tests/rules/test_switch_lang.py index 363d6417..991f3983 100644 --- a/tests/rules/test_switch_lang.py +++ b/tests/rules/test_switch_lang.py @@ -7,8 +7,13 @@ from thefuck.rules import switch_lang def test_match(): assert switch_lang.match(Mock(stderr='command not found: фзе-пуе', script=u'фзе-пуе'), None) + assert switch_lang.match(Mock(stderr='command not found: λσ', + script=u'λσ'), None) + assert not switch_lang.match(Mock(stderr='command not found: pat-get', script=u'pat-get'), None) + assert not switch_lang.match(Mock(stderr='command not found: ls', + script=u'ls'), None) assert not switch_lang.match(Mock(stderr='some info', script=u'фзе-пуе'), None) @@ -16,3 +21,5 @@ def test_match(): def test_get_new_command(): assert switch_lang.get_new_command( Mock(script=u'фзе-пуе штыефдд мшь'), None) == 'apt-get install vim' + assert switch_lang.get_new_command( + Mock(script=u'λσ -λα'), None) == 'ls -la' diff --git a/tests/test_main.py b/tests/test_main.py index f2c2843e..0345bc2e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -24,7 +24,7 @@ def test_load_rule(): return_value=Mock( match=match, get_new_command=get_new_command)) as load_source: - assert main.load_rule(Path('/rules/bash.py')) == main.Rule(match, get_new_command) + assert main.load_rule(Path('/rules/bash.py')) == main.Rule('bash', match, get_new_command) load_source.assert_called_once_with('bash', '/rules/bash.py') @@ -35,14 +35,14 @@ def test_get_rules(): glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] assert main.get_rules( Path('~'), - Mock(rules=None)) == [main.Rule('bash', 'bash'), - main.Rule('lisp', 'lisp'), - main.Rule('bash', 'bash'), - main.Rule('lisp', 'lisp')] + 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( Path('~'), - Mock(rules=['bash'])) == [main.Rule('bash', 'bash'), - main.Rule('bash', 'bash')] + Mock(rules=['bash'])) == [main.Rule('bash', 'bash', 'bash'), + main.Rule('bash', 'bash', 'bash')] def test_get_command(): @@ -61,24 +61,28 @@ def test_get_command(): stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) + assert main.get_command(Mock(), ['']) is None -def test_get_matched_rule(): - rules = [main.Rule(lambda x, _: x.script == 'cd ..', None), - main.Rule(lambda *_: False, None)] +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)] assert main.get_matched_rule(main.Command('ls', '', ''), rules, None) 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):' 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'), 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'), None, None) assert capsys.readouterr() == ('', '') diff --git a/thefuck/main.py b/thefuck/main.py index 626a1ef7..3c034d8f 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -5,11 +5,12 @@ from os.path import expanduser from subprocess import Popen, PIPE import os import sys +from traceback import format_exception from psutil import Process, TimeoutExpired Command = namedtuple('Command', ('script', 'stdout', 'stderr')) -Rule = namedtuple('Rule', ('match', 'get_new_command')) +Rule = namedtuple('Rule', ('name', 'match', 'get_new_command')) def setup_user_dir(): @@ -43,7 +44,8 @@ def is_rule_enabled(settings, rule): def load_rule(rule): """Imports rule module and returns it.""" rule_module = load_source(rule.name[:-3], str(rule)) - return Rule(rule_module.match, rule_module.get_new_command) + return Rule(rule.name[:-3], rule_module.match, + rule_module.get_new_command) def get_rules(user_dir, settings): @@ -80,6 +82,10 @@ def get_command(settings, args): script = ' '.join(arg.decode('utf-8') for arg in args[1:]) else: script = ' '.join(args[1:]) + + if not script: + return + result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=dict(os.environ, LANG='C')) if wait_output(settings, result): @@ -90,8 +96,12 @@ def get_command(settings, args): def get_matched_rule(command, rules, settings): """Returns first matched rule for command.""" for rule in rules: - if rule.match(command, settings): - return rule + try: + 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())))) def confirm(new_command, settings): diff --git a/thefuck/rules/ssh_known_hosts.py b/thefuck/rules/ssh_known_hosts.py new file mode 100644 index 00000000..ab73c422 --- /dev/null +++ b/thefuck/rules/ssh_known_hosts.py @@ -0,0 +1,37 @@ +import re + +patterns = [ + r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!', + r'WARNING: POSSIBLE DNS SPOOFING DETECTED!', + r"Warning: the \S+ host key for '([^']+)' differs from the key for the IP address '([^']+)'", +] +offending_pattern = re.compile( + r'(?:Offending (?:key for IP|\S+ key)|Matching host key) in ([^:]+):(\d+)', + re.MULTILINE) + +commands = ['ssh', 'scp'] + + +def match(command, settings): + if not command.script: + return False + if not command.script.split()[0] in commands: + return False + if not any([re.findall(pattern, command.stderr) for pattern in patterns]): + return False + return True + + +def remove_offending_keys(command, settings): + offending = offending_pattern.findall(command.stderr) + for filepath, lineno in offending: + with open(filepath, 'r') as fh: + lines = fh.readlines() + del lines[int(lineno) - 1] + with open(filepath, 'w') as fh: + fh.writelines(lines) + + +def get_new_command(command, settings): + remove_offending_keys(command, settings) + return command.script diff --git a/thefuck/rules/sudo.py b/thefuck/rules/sudo.py index 476ae212..39604acc 100644 --- a/thefuck/rules/sudo.py +++ b/thefuck/rules/sudo.py @@ -4,6 +4,7 @@ patterns = ['permission denied', 'you cannot perform this operation unless you are root', 'non-root users cannot', 'Operation not permitted', + 'root privilege', 'This command has to be run under the root user.'] diff --git a/thefuck/rules/switch_lang.py b/thefuck/rules/switch_lang.py index 572c772c..af427208 100644 --- a/thefuck/rules/switch_lang.py +++ b/thefuck/rules/switch_lang.py @@ -3,7 +3,8 @@ target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?''' source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''', - u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟'''] + u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''', + u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?'''] def _get_matched_layout(command):