From 3af5c80d293c579591b58f63a5b01574f76991c9 Mon Sep 17 00:00:00 2001 From: Soheil Rashidi Date: Tue, 21 Apr 2015 12:57:35 +0430 Subject: [PATCH 01/13] Add 'root privilege' pattern to sudo rule. --- thefuck/rules/sudo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/thefuck/rules/sudo.py b/thefuck/rules/sudo.py index e1a18359..d6ad091d 100644 --- a/thefuck/rules/sudo.py +++ b/thefuck/rules/sudo.py @@ -3,7 +3,8 @@ patterns = ['permission denied', 'pkg: Insufficient privileges', 'you cannot perform this operation unless you are root', 'non-root users cannot', - 'Operation not permitted'] + 'Operation not permitted', + 'root privilege'] def match(command, settings): From d5b4bddc4cf958e362ed0ee1b8a6c474fc78e242 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 21 Apr 2015 14:26:45 +0200 Subject: [PATCH 02/13] #74 Don't fail when runned without args --- tests/test_main.py | 1 + thefuck/main.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index f2c2843e..af8c09aa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -61,6 +61,7 @@ def test_get_command(): stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) + assert main.get_command(Mock(), ['']) is None def test_get_matched_rule(): diff --git a/thefuck/main.py b/thefuck/main.py index 626a1ef7..b3e0ee27 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -80,6 +80,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): From 888756d519ba1915c37e2da3e137f35c2e5e3cea Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 21 Apr 2015 14:40:52 +0200 Subject: [PATCH 03/13] #74 Don't fail when rule throws exception --- tests/test_main.py | 27 +++++++++++++++------------ thefuck/main.py | 14 ++++++++++---- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index af8c09aa..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(): @@ -64,22 +64,25 @@ def test_get_command(): 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 b3e0ee27..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): @@ -94,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): From 54d82f9528ae9cb0b7fa62522156ad832c976ed1 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 21 Apr 2015 14:41:28 +0200 Subject: [PATCH 04/13] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a552b9d6..334f996d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup(name='thefuck', - version="1.22", + version="1.23", description="Magnificent app which corrects your previous console command", author='Vladimir Iakovlev', author_email='nvbn.rm@gmail.com', From 8e18ff6eab5efe3a65b0af58d4e3e1889c558669 Mon Sep 17 00:00:00 2001 From: Gen2 Date: Tue, 21 Apr 2015 13:46:38 +0100 Subject: [PATCH 05/13] Fix broken settings link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56e7bb83..2c0109f0 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ REPL-y 0.3.1 ``` If you are scary to blindly run changed command, there's `require_confirmation` -[settings](#Settings) option: +[settings](#settings) option: ```bash ➜ apt-get install vim From 3f21d5fc3f3a2d985ab60ff8fcf1c1631c905721 Mon Sep 17 00:00:00 2001 From: Matthew Field Date: Tue, 21 Apr 2015 08:47:14 -0600 Subject: [PATCH 06/13] Fix spelling mistake --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c0109f0..a5128559 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ REPL-y 0.3.1 ... ``` -If you are scary to blindly run changed command, there's `require_confirmation` +If you are scared to blindly run changed command, there's `require_confirmation` [settings](#settings) option: ```bash From 943613a194ee76be9a73eedcb82c0036bb37b3f0 Mon Sep 17 00:00:00 2001 From: Nic West Date: Tue, 21 Apr 2015 17:05:52 +0100 Subject: [PATCH 07/13] add thing for when known hosts have changed --- tests/rules/test_ssh_known_host.py | 74 ++++++++++++++++++++++++++++++ thefuck/rules/ssh_known_hosts.py | 38 +++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 tests/rules/test_ssh_known_host.py create mode 100644 thefuck/rules/ssh_known_hosts.py diff --git a/tests/rules/test_ssh_known_host.py b/tests/rules/test_ssh_known_host.py new file mode 100644 index 00000000..74a81469 --- /dev/null +++ b/tests/rules/test_ssh_known_host.py @@ -0,0 +1,74 @@ +import os +import pytest +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 + + class Mock: + was_called = False + + def __call__(self, *args, **kwargs): + self.was_called = True + + 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.was_called diff --git a/thefuck/rules/ssh_known_hosts.py b/thefuck/rules/ssh_known_hosts.py new file mode 100644 index 00000000..2f425bfe --- /dev/null +++ b/thefuck/rules/ssh_known_hosts.py @@ -0,0 +1,38 @@ +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) + print offending + for filepath, lineno in offending: + with open(filepath, 'r') as fh: + lines = fh.readlines() + del lines[int(lineno) - 1] + print lines + with open(filepath, 'w') as fh: + fh.writelines(lines) + + +def get_new_command(command, settings): + remove_offending_keys(command, settings) + return command.script From 2a7cbef3b5e364fa6da4850178b06ed85bb5304c Mon Sep 17 00:00:00 2001 From: SanketDG Date: Tue, 21 Apr 2015 23:41:49 +0530 Subject: [PATCH 08/13] add tests for cd_parent --- tests/rules/test_cd_parent.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/rules/test_cd_parent.py 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 ..' From 4b8d4926aa2362340f4e8b57ee55e48846ccb6c3 Mon Sep 17 00:00:00 2001 From: Dionysis Zindros Date: Tue, 21 Apr 2015 22:00:05 +0200 Subject: [PATCH 09/13] Add Greek to switch lang --- thefuck/rules/switch_lang.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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): From 4f10fe647d31e455c2a43c6ea326cf6f3c1d49ac Mon Sep 17 00:00:00 2001 From: Dionysis Zindros Date: Tue, 21 Apr 2015 22:09:48 +0200 Subject: [PATCH 10/13] Add tests for greek langage --- tests/rules/test_switch_lang.py | 7 +++++++ 1 file changed, 7 insertions(+) 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' From d1416a6c2a54cfba46675b44a2b99a0cd05d7c83 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 21 Apr 2015 22:10:53 +0200 Subject: [PATCH 11/13] #82 Remove unned print, fix python 3 support --- tests/rules/test_ssh_known_host.py | 13 ++++--------- thefuck/rules/ssh_known_hosts.py | 3 +-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/rules/test_ssh_known_host.py b/tests/rules/test_ssh_known_host.py index 74a81469..9c8dc0d1 100644 --- a/tests/rules/test_ssh_known_host.py +++ b/tests/rules/test_ssh_known_host.py @@ -1,5 +1,6 @@ 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 @@ -41,7 +42,7 @@ Host key verification failed.""".format(path, '98.765.432.321') def test_match(ssh_error): - errormsg, _, _, _ = 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) @@ -55,20 +56,14 @@ 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'] + 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 - class Mock: - was_called = False - - def __call__(self, *args, **kwargs): - self.was_called = True - 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.was_called + assert method.call_count diff --git a/thefuck/rules/ssh_known_hosts.py b/thefuck/rules/ssh_known_hosts.py index 2f425bfe..ab73c422 100644 --- a/thefuck/rules/ssh_known_hosts.py +++ b/thefuck/rules/ssh_known_hosts.py @@ -1,4 +1,5 @@ import re + patterns = [ r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!', r'WARNING: POSSIBLE DNS SPOOFING DETECTED!', @@ -23,12 +24,10 @@ def match(command, settings): def remove_offending_keys(command, settings): offending = offending_pattern.findall(command.stderr) - print offending for filepath, lineno in offending: with open(filepath, 'r') as fh: lines = fh.readlines() del lines[int(lineno) - 1] - print lines with open(filepath, 'w') as fh: fh.writelines(lines) From 20f8a4ad17e4fb014d38a390b98fed872cc4ebc8 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 21 Apr 2015 22:30:15 +0200 Subject: [PATCH 12/13] Bump to 1.24 --- release.py | 30 ++++++++++++++++++++++++++++++ setup.py | 8 ++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100755 release.py diff --git a/release.py b/release.py new file mode 100755 index 00000000..b39501a5 --- /dev/null +++ b/release.py @@ -0,0 +1,30 @@ +#!/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 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 334f996d..c822bb5c 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,18 @@ from setuptools import setup, find_packages +VERSION = '1.24' + + setup(name='thefuck', - version="1.23", + 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'], From d1b1465f4e80dc08fd210638c05248890c974b35 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 21 Apr 2015 22:31:01 +0200 Subject: [PATCH 13/13] Bump to 1.26 --- release.py | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/release.py b/release.py index b39501a5..e22209b6 100755 --- a/release.py +++ b/release.py @@ -23,6 +23,7 @@ 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) diff --git a/setup.py b/setup.py index c822bb5c..d19f72dc 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.24' +VERSION = '1.26' setup(name='thefuck',