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/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_cp_omitting_directory.py b/tests/rules/test_cp_omitting_directory.py new file mode 100644 index 00000000..0f4a3bf8 --- /dev/null +++ b/tests/rules/test_cp_omitting_directory.py @@ -0,0 +1,14 @@ +from mock import Mock +from thefuck.rules.cp_omitting_directory import match, get_new_command + + +def test_match(): + assert match(Mock(script='cp dir', stderr="cp: omitting directory 'dir'"), + None) + assert not match(Mock(script='some dir', + stderr="cp: omitting directory 'dir'"), None) + assert not match(Mock(script='cp dir', stderr=""), None) + + +def test_get_new_command(): + assert get_new_command(Mock(script='cp dir'), None) == 'cp -a dir' diff --git a/tests/rules/test_has_exists_script.py b/tests/rules/test_has_exists_script.py new file mode 100644 index 00000000..36938be2 --- /dev/null +++ b/tests/rules/test_has_exists_script.py @@ -0,0 +1,20 @@ +from mock import Mock, patch +from thefuck.rules. has_exists_script import match, get_new_command + + +def test_match(): + with patch('os.path.exists', return_value=True): + assert match(Mock(script='main', stderr='main: command not found'), + None) + assert match(Mock(script='main --help', + stderr='main: command not found'), + None) + assert not match(Mock(script='main', stderr=''), None) + + with patch('os.path.exists', return_value=False): + assert not match(Mock(script='main', stderr='main: command not found'), + None) + + +def test_get_new_command(): + assert get_new_command(Mock(script='main --help'), None) == './main --help' diff --git a/tests/rules/test_mkdir_p.py b/tests/rules/test_mkdir_p.py new file mode 100644 index 00000000..128be2f3 --- /dev/null +++ b/tests/rules/test_mkdir_p.py @@ -0,0 +1,13 @@ +from thefuck.main import Command +from thefuck.rules.mkdir_p import match, get_new_command + + +def test_match(): + assert match(Command('mkdir foo/bar/baz', '', 'mkdir: foo/bar: No such file or directory'), None) + assert not match(Command('mkdir foo/bar/baz', '', ''), None) + assert not match(Command('mkdir foo/bar/baz', '', 'foo bar baz'), None) + assert not match(Command('', '', ''), None) + + +def test_get_new_command(): + assert get_new_command(Command('mkdir foo/bar/baz', '', ''), None) == 'mkdir -p foo/bar/baz' diff --git a/tests/rules/test_python_command.py b/tests/rules/test_python_command.py new file mode 100644 index 00000000..e8071263 --- /dev/null +++ b/tests/rules/test_python_command.py @@ -0,0 +1,9 @@ +from thefuck.main import Command +from thefuck.rules.python_command import match, get_new_command + +def test_match(): + assert match(Command('temp.py', '', 'Permission denied'), None) + assert not match(Command('', '', ''), None) + +def test_get_new_command(): + assert get_new_command(Command('./test_sudo.py', '', ''), None) == 'python ./test_sudo.py' diff --git a/tests/rules/test_rm_dir.py b/tests/rules/test_rm_dir.py new file mode 100644 index 00000000..2362d0c3 --- /dev/null +++ b/tests/rules/test_rm_dir.py @@ -0,0 +1,13 @@ +from thefuck.main import Command +from thefuck.rules.rm_dir import match, get_new_command + + +def test_match(): + assert match(Command('rm foo', '', 'rm: foo: is a directory'), None) + assert not match(Command('rm foo', '', ''), None) + assert not match(Command('rm foo', '', 'foo bar baz'), None) + assert not match(Command('', '', ''), None) + + +def test_get_new_command(): + assert get_new_command(Command('rm foo', '', ''), None) == 'rm -rf foo' 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 new file mode 100644 index 00000000..991f3983 --- /dev/null +++ b/tests/rules/test_switch_lang.py @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- + +from mock import Mock +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) + + +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_logs.py b/tests/test_logs.py new file mode 100644 index 00000000..bf3780e1 --- /dev/null +++ b/tests/test_logs.py @@ -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)) == '' diff --git a/thefuck/logs.py b/thefuck/logs.py new file mode 100644 index 00000000..9bfb02bc --- /dev/null +++ b/thefuck/logs.py @@ -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))) diff --git a/thefuck/rules/has_exists_script.py b/thefuck/rules/has_exists_script.py new file mode 100644 index 00000000..4ceac489 --- /dev/null +++ b/thefuck/rules/has_exists_script.py @@ -0,0 +1,11 @@ +import os + + +def match(command, settings): + return os.path.exists(command.script.split()[0]) \ + and 'command not found' in command.stderr + + +def get_new_command(command, settings): + return u'./{}'.format(command.script) + diff --git a/thefuck/rules/mkdir_p.py b/thefuck/rules/mkdir_p.py new file mode 100644 index 00000000..896f08f7 --- /dev/null +++ b/thefuck/rules/mkdir_p.py @@ -0,0 +1,9 @@ +import re + +def match(command, settings): + return ('mkdir' in command.script + and 'No such file or directory' in command.stderr) + + +def get_new_command(command, settings): + return re.sub('^mkdir (.*)', 'mkdir -p \\1', command.script) diff --git a/thefuck/rules/python_command.py b/thefuck/rules/python_command.py new file mode 100644 index 00000000..507a934b --- /dev/null +++ b/thefuck/rules/python_command.py @@ -0,0 +1,14 @@ +# add 'python' suffix to the command if +# 1) The script does not have execute permission or +# 2) is interpreted as shell script + +def match(command, settings): + toks = command.script.split() + return (len(toks) > 0 + and toks[0].endswith('.py') + and ('Permission denied' in command.stderr or + 'command not found' in command.stderr)) + + +def get_new_command(command, settings): + return 'python ' + command.script diff --git a/thefuck/rules/rm_dir.py b/thefuck/rules/rm_dir.py new file mode 100644 index 00000000..f9349ea4 --- /dev/null +++ b/thefuck/rules/rm_dir.py @@ -0,0 +1,9 @@ +import re + +def match(command, settings): + return ('rm' in command.script + and 'is a directory' in command.stderr) + + +def get_new_command(command, settings): + return re.sub('^rm (.*)', 'rm -rf \\1', command.script) 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/switch_lang.py b/thefuck/rules/switch_lang.py new file mode 100644 index 00000000..af427208 --- /dev/null +++ b/thefuck/rules/switch_lang.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- + +target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?''' + +source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''', + u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''', + u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?'''] + + +def _get_matched_layout(command): + for source_layout in source_layouts: + if all([ch in source_layout or ch in '-_' + for ch in command.script.split(' ')[0]]): + return source_layout + + +def match(command, settings): + return 'not found' in command.stderr and _get_matched_layout(command) + + +def _switch(ch, layout): + if ch in layout: + return target_layout[layout.index(ch)] + else: + return ch + + +def get_new_command(command, settings): + matched_layout = _get_matched_layout(command) + return ''.join(_switch(ch, matched_layout) for ch in command.script) +