From 2a79a5e413b41bbbe3886828a97958ca824e7bdc Mon Sep 17 00:00:00 2001 From: SpyCheese Date: Sun, 19 Apr 2015 09:03:34 +0500 Subject: [PATCH 001/107] Create rm_root.py --- thefuck/rules/rm_root.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 thefuck/rules/rm_root.py diff --git a/thefuck/rules/rm_root.py b/thefuck/rules/rm_root.py new file mode 100644 index 0000000..95d9fbd --- /dev/null +++ b/thefuck/rules/rm_root.py @@ -0,0 +1,8 @@ +def match(command, settings): + return ('rm' in command.script + and '--help' not in command.script + and '--no-preserve-root' in command.stderr) + + +def get_new_command(command, settings): + return '{} --no-preserve-root'.format(command.script) From f113bae59d97ab1cd90857680e8c6e9320b96327 Mon Sep 17 00:00:00 2001 From: SpyCheese Date: Sun, 19 Apr 2015 09:12:19 +0500 Subject: [PATCH 002/107] Update rm_root.py --- thefuck/rules/rm_root.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/thefuck/rules/rm_root.py b/thefuck/rules/rm_root.py index 95d9fbd..b97eda2 100644 --- a/thefuck/rules/rm_root.py +++ b/thefuck/rules/rm_root.py @@ -1,6 +1,6 @@ def match(command, settings): - return ('rm' in command.script - and '--help' not in command.script + return ('/' in command.script + and '--no-preserve-root' not in command.script and '--no-preserve-root' in command.stderr) From ceeccf1cd7de156ae48ce5aa7f01366015fa6de9 Mon Sep 17 00:00:00 2001 From: SpyCheese Date: Sun, 19 Apr 2015 10:21:46 +0500 Subject: [PATCH 003/107] Update rm_root.py Okay, there was an incorrect match function. --- thefuck/rules/rm_root.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/rules/rm_root.py b/thefuck/rules/rm_root.py index b97eda2..853ce84 100644 --- a/thefuck/rules/rm_root.py +++ b/thefuck/rules/rm_root.py @@ -1,5 +1,5 @@ def match(command, settings): - return ('/' in command.script + return ('/' in command.script.split() and '--no-preserve-root' not in command.script and '--no-preserve-root' in command.stderr) From 93b6a623e1c527b8e08093a986ca23fbb189d949 Mon Sep 17 00:00:00 2001 From: Lukas Vacek Date: Tue, 21 Apr 2015 17:59:44 +0200 Subject: [PATCH 004/107] adding rule to run "sudo apt-get install" --- thefuck/rules/apt_get.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 thefuck/rules/apt_get.py diff --git a/thefuck/rules/apt_get.py b/thefuck/rules/apt_get.py new file mode 100644 index 0000000..0e7af59 --- /dev/null +++ b/thefuck/rules/apt_get.py @@ -0,0 +1,28 @@ +import sys + +def match(command, settings): + try: + import CommandNotFound + if 'not found' in command.stderr: + try: + c = CommandNotFound.CommandNotFound() + pkgs = c.getPackages(command.script.split(" ")[0]) + name,_ = pkgs[0] + return True + except IndexError: + # IndexError is thrown when no matching package is found + return False + except: + return False + +def get_new_command(command, settings): + try: + import CommandNotFound + c = CommandNotFound.CommandNotFound() + if 'not found' in command.stderr: + pkgs = c.getPackages(command.script.split(" ")[0]) + name,_ = pkgs[0] + return "sudo apt-get install %s" % name + except: + sys.stderr.write("Can't apt fuck\n") + return "" From 957209bdb681ba169dc7b0ce4088554b51f7b6fd Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 15:59:44 +0200 Subject: [PATCH 005/107] Add ability to bundle disabled by default rules --- tests/test_main.py | 48 +++++++++++++++++++++++++++------------------- thefuck/main.py | 20 ++++++++++++++----- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 47bbe79..6ad6e13 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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, Mock(no_colors=True)) is None assert main.get_matched_rule(main.Command('cd ..', '', ''), rules, Mock(no_colors=True)) == rules[0] - assert capsys.readouterr()[1].split('\n')[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() == ('', '') diff --git a/thefuck/main.py b/thefuck/main.py index 4975e7b..a6c24ee 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -11,7 +11,8 @@ 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(): @@ -40,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): @@ -56,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): From fa4e4522b78ccf0811e8b6b671a20fab6099c1f2 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 16:08:54 +0200 Subject: [PATCH 006/107] #43 Add `rm_root` as disabled by default rule --- README.md | 4 ++++ thefuck/main.py | 3 ++- thefuck/rules/rm_root.py | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f18fafb..620d236 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/thefuck/main.py b/thefuck/main.py index a6c24ee..32c1c7c 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -153,7 +153,8 @@ def main(): logs.failed("Can't fuck twice", settings) return - rules = get_rules(user_dir, settings) + rules = list(get_rules(user_dir, settings)) + sys.stderr.write(str([r.name for r in rules]) + '\n') matched_rule = get_matched_rule(command, rules, settings) if matched_rule: run_rule(matched_rule, command, settings) diff --git a/thefuck/rules/rm_root.py b/thefuck/rules/rm_root.py index 853ce84..fc75332 100644 --- a/thefuck/rules/rm_root.py +++ b/thefuck/rules/rm_root.py @@ -1,5 +1,8 @@ +enabled_by_default = False + + def match(command, settings): - return ('/' in command.script.split() + return ({'rm', '/'}.issubset(command.script.split()) and '--no-preserve-root' not in command.script and '--no-preserve-root' in command.stderr) From 7010b3a7f601608c4f3f943a374ba0ef098fc2da Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 16:22:10 +0200 Subject: [PATCH 007/107] #43 Add test for `rm_root` --- tests/rules/test_rm_root.py | 18 ++++++++++++++++++ thefuck/main.py | 3 +-- thefuck/rules/rm_root.py | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 tests/rules/test_rm_root.py diff --git a/tests/rules/test_rm_root.py b/tests/rules/test_rm_root.py new file mode 100644 index 0000000..003cb80 --- /dev/null +++ b/tests/rules/test_rm_root.py @@ -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' diff --git a/thefuck/main.py b/thefuck/main.py index 32c1c7c..a6c24ee 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -153,8 +153,7 @@ def main(): logs.failed("Can't fuck twice", settings) return - rules = list(get_rules(user_dir, settings)) - sys.stderr.write(str([r.name for r in rules]) + '\n') + rules = get_rules(user_dir, settings) matched_rule = get_matched_rule(command, rules, settings) if matched_rule: run_rule(matched_rule, command, settings) diff --git a/thefuck/rules/rm_root.py b/thefuck/rules/rm_root.py index fc75332..d07543b 100644 --- a/thefuck/rules/rm_root.py +++ b/thefuck/rules/rm_root.py @@ -8,4 +8,4 @@ def match(command, settings): def get_new_command(command, settings): - return '{} --no-preserve-root'.format(command.script) + return u'{} --no-preserve-root'.format(command.script) From e7b78205f4c7cb060ebe5670f1d0986dbb0cf924 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 16:45:38 +0200 Subject: [PATCH 008/107] Add transparent sudo support for rules where it required --- setup.py | 2 +- tests/test_utils.py | 17 +++++++++++++++++ thefuck/rules/cp_omitting_directory.py | 3 +++ thefuck/rules/has_exists_script.py | 3 +++ thefuck/rules/lein_not_task.py | 3 +++ thefuck/rules/mkdir_p.py | 4 ++++ thefuck/rules/no_command.py | 3 +++ thefuck/rules/python_command.py | 4 ++++ thefuck/rules/rm_dir.py | 4 ++++ thefuck/rules/rm_root.py | 5 +++++ thefuck/utils.py | 21 +++++++++++++++++++++ 11 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/test_utils.py diff --git a/setup.py b/setup.py index 701e9af..b9470cc 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,6 @@ setup(name='thefuck', 'tests', 'release']), include_package_data=True, zip_safe=False, - install_requires=['pathlib', 'psutil', 'colorama'], + install_requires=['pathlib', 'psutil', 'colorama', 'six'], entry_points={'console_scripts': [ 'thefuck = thefuck.main:main']}) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..fed491a --- /dev/null +++ b/tests/test_utils.py @@ -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' diff --git a/thefuck/rules/cp_omitting_directory.py b/thefuck/rules/cp_omitting_directory.py index 14e84c9..4dee781 100644 --- a/thefuck/rules/cp_omitting_directory.py +++ b/thefuck/rules/cp_omitting_directory.py @@ -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) diff --git a/thefuck/rules/has_exists_script.py b/thefuck/rules/has_exists_script.py index 4ceac48..19a7e48 100644 --- a/thefuck/rules/has_exists_script.py +++ b/thefuck/rules/has_exists_script.py @@ -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) diff --git a/thefuck/rules/lein_not_task.py b/thefuck/rules/lein_not_task.py index efc25a1..a043263 100644 --- a/thefuck/rules/lein_not_task.py +++ b/thefuck/rules/lein_not_task.py @@ -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] diff --git a/thefuck/rules/mkdir_p.py b/thefuck/rules/mkdir_p.py index 896f08f..03b40ce 100644 --- a/thefuck/rules/mkdir_p.py +++ b/thefuck/rules/mkdir_p.py @@ -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) diff --git a/thefuck/rules/no_command.py b/thefuck/rules/no_command.py index ce90926..917e1fc 100644 --- a/thefuck/rules/no_command.py +++ b/thefuck/rules/no_command.py @@ -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, diff --git a/thefuck/rules/python_command.py b/thefuck/rules/python_command.py index 507a934..f2bc8dc 100644 --- a/thefuck/rules/python_command.py +++ b/thefuck/rules/python_command.py @@ -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 diff --git a/thefuck/rules/rm_dir.py b/thefuck/rules/rm_dir.py index f9349ea..557278b 100644 --- a/thefuck/rules/rm_dir.py +++ b/thefuck/rules/rm_dir.py @@ -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) diff --git a/thefuck/rules/rm_root.py b/thefuck/rules/rm_root.py index d07543b..ed0121f 100644 --- a/thefuck/rules/rm_root.py +++ b/thefuck/rules/rm_root.py @@ -1,11 +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) diff --git a/thefuck/utils.py b/thefuck/utils.py index fa4ee1e..534a634 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -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 From 69ddd82baeb5ead4366c4e6a1bee7f28b4aacca1 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 16:46:06 +0200 Subject: [PATCH 009/107] Bump to 1.27 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b9470cc..6c07314 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.26' +VERSION = '1.27' setup(name='thefuck', From b4b599df805270017c8460baaa8bca430ca6bef3 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 16:52:09 +0200 Subject: [PATCH 010/107] Update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 620d236..aaaa46a 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ using matched rule and run it. Rules enabled by default: * `no_command` – fixes wrong console commands, for example `vom/vim`; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `rm_dir` – adds `-rf` when you trying to remove directory; +* `ssh_known_hosts` – removes host from `known_hosts` on warning; * `sudo` – prepends `sudo` to previous command if it failed because of permissions; * `switch_layout` – switches command from your local layout to en. From c788dfbc1498e3d7efae7274e986662645dd49fa Mon Sep 17 00:00:00 2001 From: Timo Furrer Date: Wed, 22 Apr 2015 19:04:52 +0200 Subject: [PATCH 011/107] fix rm dir rule to make it case insensitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In bash the output for the command `rm -f foo/` is: rm: cannot remove ‘foo/’: Is a directory And not: rm: cannot remove ‘foo/’: is a directory --- tests/rules/test_rm_dir.py | 1 + thefuck/rules/rm_dir.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rules/test_rm_dir.py b/tests/rules/test_rm_dir.py index 2362d0c..d0ff324 100644 --- a/tests/rules/test_rm_dir.py +++ b/tests/rules/test_rm_dir.py @@ -4,6 +4,7 @@ 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 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) diff --git a/thefuck/rules/rm_dir.py b/thefuck/rules/rm_dir.py index 557278b..89b1d2b 100644 --- a/thefuck/rules/rm_dir.py +++ b/thefuck/rules/rm_dir.py @@ -5,7 +5,7 @@ from thefuck.utils import sudo_support @sudo_support def match(command, settings): return ('rm' in command.script - and 'is a directory' in command.stderr) + and 'is a directory' in command.stderr.lower()) @sudo_support From 69a95164773772262816f5f260074f1070d7f43d Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 20:18:53 +0200 Subject: [PATCH 012/107] Add ability to change settings via environment variables --- README.md | 9 +++- tests/test_conf.py | 75 +++++++++++++++++++++++++++ tests/test_main.py | 34 ++++--------- tests/test_utils.py | 11 +++- thefuck/conf.py | 120 ++++++++++++++++++++++++++++++++++++++++++++ thefuck/logs.py | 10 ++-- thefuck/main.py | 30 ++--------- thefuck/utils.py | 5 +- 8 files changed, 233 insertions(+), 61 deletions(-) create mode 100644 tests/test_conf.py create mode 100644 thefuck/conf.py diff --git a/README.md b/README.md index aaaa46a..650ea8d 100644 --- a/README.md +++ b/README.md @@ -196,11 +196,18 @@ def get_new_command(command, settings): The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`: -* `rules` – list of enabled rules, by default all; +* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT`; * `require_confirmation` – require confirmation before running new command, by default `False`; * `wait_command` – max amount of time in seconds for getting previous command output; * `no_colors` – disable colored output. +Or via environment variables: + +* `THEFUCK_RULES` – list of enabled rules, like `DEFAULT:rm_root` or `sudo:no_command`; +* `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`; +* `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output; +* `THEFUCK_NO_COLORS` – disable colored output, `true/false`. + ## Developing Install `The Fuck` for development: diff --git a/tests/test_conf.py b/tests/test_conf.py new file mode 100644 index 0000000..c74d343 --- /dev/null +++ b/tests/test_conf.py @@ -0,0 +1,75 @@ +from mock import patch, Mock +from thefuck.main import Rule +from thefuck import conf + + +def test_rules_list(): + assert conf.RulesList(['bash', 'lisp']) == ['bash', 'lisp'] + assert conf.RulesList(['bash', 'lisp']) == conf.RulesList(['bash', 'lisp']) + assert Rule('lisp', None, None, False) in conf.RulesList(['lisp']) + assert Rule('bash', None, None, False) not in conf.RulesList(['lisp']) + + +def test_default(): + assert Rule('test', None, None, True) in conf.DEFAULT + assert Rule('test', None, None, False) not in conf.DEFAULT + assert Rule('test', None, None, False) in (conf.DEFAULT + ['test']) + + +def test_settings_defaults(): + with patch('thefuck.conf.load_source', return_value=object()), \ + patch('thefuck.conf.os.environ', new_callable=lambda: {}): + for key, val in conf.Settings.defaults.items(): + assert getattr(conf.Settings(Mock()), key) == val + + +def test_settings_from_file(): + with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'], + wait_command=10, + require_confirmation=True, + no_colors=True)), \ + patch('thefuck.conf.os.environ', new_callable=lambda: {}): + settings = conf.Settings(Mock()) + assert settings.rules == ['test'] + assert settings.wait_command == 10 + assert settings.require_confirmation is True + assert settings.no_colors is True + + +def test_settings_from_file_with_DEFAULT(): + with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT + ['test'], + wait_command=10, + require_confirmation=True, + no_colors=True)), \ + patch('thefuck.conf.os.environ', new_callable=lambda: {}): + settings = conf.Settings(Mock()) + assert settings.rules == conf.DEFAULT + ['test'] + + +def test_settings_from_env(): + with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'], + wait_command=10)), \ + patch('thefuck.conf.os.environ', + new_callable=lambda: {'THEFUCK_RULES': 'bash:lisp', + 'THEFUCK_WAIT_COMMAND': '55', + 'THEFUCK_REQUIRE_CONFIRMATION': 'true', + 'THEFUCK_NO_COLORS': 'false'}): + settings = conf.Settings(Mock()) + assert settings.rules == ['bash', 'lisp'] + assert settings.wait_command == 55 + assert settings.require_confirmation is True + assert settings.no_colors is False + + +def test_settings_from_env_with_DEFAULT(): + with patch('thefuck.conf.load_source', return_value=Mock()), \ + patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT:bash:lisp'}): + settings = conf.Settings(Mock()) + assert settings.rules == conf.DEFAULT + ['bash', 'lisp'] + + +def test_update_settings(): + settings = conf.BaseSettings({'key': 'val'}) + new_settings = settings.update(key='new-val') + assert new_settings.key == 'new-val' + assert settings.key == 'val' diff --git a/tests/test_main.py b/tests/test_main.py index 6ad6e13..12f06ea 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,25 +1,7 @@ from subprocess import PIPE from pathlib import PosixPath, Path from mock import patch, Mock -from thefuck import main - - -def test_get_settings(): - with patch('thefuck.main.load_source', return_value=Mock(rules=['bash'])): - assert main.get_settings(Path('/')).rules == ['bash'] - with patch('thefuck.main.load_source', return_value=Mock(spec=[])): - assert main.get_settings(Path('/')).rules is None - - -def test_is_rule_enabled(): - 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)) +from thefuck import main, conf def test_load_rule(): @@ -43,14 +25,16 @@ def test_get_rules(): glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] assert list(main.get_rules( Path('~'), - 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)] + Mock(rules=conf.DEFAULT))) \ + == [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', True), - main.Rule('bash', 'bash', 'bash', True)] + Mock(rules=conf.RulesList(['bash'])))) \ + == [main.Rule('bash', 'bash', 'bash', True), + main.Rule('bash', 'bash', 'bash', True)] def test_get_command(): diff --git a/tests/test_utils.py b/tests/test_utils.py index fed491a..e1ec93d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,15 @@ from mock import Mock -from thefuck.utils import sudo_support +from thefuck.utils import sudo_support, wrap_settings from thefuck.main import Command +from thefuck.conf import BaseSettings + + +def test_wrap_settings(): + fn = lambda _, settings: settings._conf + assert wrap_settings({'key': 'val'})(fn)(None, BaseSettings({})) \ + == {'key': 'val'} + assert wrap_settings({'key': 'new-val'})(fn)( + None, BaseSettings({'key': 'val'})) == {'key': 'new-val'} def test_sudo_support(): diff --git a/thefuck/conf.py b/thefuck/conf.py new file mode 100644 index 0000000..4593905 --- /dev/null +++ b/thefuck/conf.py @@ -0,0 +1,120 @@ +from copy import copy +from imp import load_source +import os +import sys +from six import text_type +from . import logs + + +class RulesList(object): + """Wrapper a top of list for string rules names.""" + + def __init__(self, rules): + self.rules = rules + + def __contains__(self, item): + return item.name in self.rules + + def __getattr__(self, item): + return getattr(self.rules, item) + + def __eq__(self, other): + return self.rules == other + + +class _DefaultRules(RulesList): + def __add__(self, items): + return _DefaultRules(self.rules + items) + + def __contains__(self, item): + return item.enabled_by_default or \ + super(_DefaultRules, self).__contains__(item) + + def __eq__(self, other): + if isinstance(other, _DefaultRules): + return self.rules == other.rules + else: + return False + + +DEFAULT = _DefaultRules([]) + + +class BaseSettings(object): + def __init__(self, conf): + self._conf = conf + + def __getattr__(self, item): + return self._conf.get(item) + + def update(self, **kwargs): + """Returns new settings with new values from `kwargs`.""" + conf = copy(self._conf) + conf.update(kwargs) + return BaseSettings(conf) + + +class Settings(BaseSettings): + """Settings loaded from defaults/file/env.""" + defaults = {'rules': DEFAULT, + 'wait_command': 3, + 'require_confirmation': False, + 'no_colors': False} + + env_to_attr = {'THEFUCK_RULES': 'rules', + 'THEFUCK_WAIT_COMMAND': 'wait_command', + 'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation', + 'THEFUCK_NO_COLORS': 'no_colors'} + + def __init__(self, user_dir): + super(Settings, self).__init__(self._load_conf(user_dir)) + + def _load_conf(self, user_dir): + conf = copy(self.defaults) + try: + conf.update(self._load_from_file(user_dir)) + except: + logs.exception("Can't load settings from file", + sys.exc_info(), + BaseSettings(conf)) + try: + conf.update(self._load_from_env()) + except: + logs.exception("Can't load settings from env", + sys.exc_info(), + BaseSettings(conf)) + if not isinstance(conf['rules'], RulesList): + conf['rules'] = RulesList(conf['rules']) + return conf + + def _load_from_file(self, user_dir): + """Loads settings from file.""" + settings = load_source('settings', + text_type(user_dir.joinpath('settings.py'))) + return {key: getattr(settings, key) + for key in self.defaults.keys() + if hasattr(settings, key)} + + def _load_from_env(self): + """Loads settings from env.""" + return {attr: self._val_from_env(env, attr) + for env, attr in self.env_to_attr.items() + if env in os.environ} + + def _val_from_env(self, env, attr): + """Transforms env-strings to python.""" + val = os.environ[env] + if attr == 'rules': + val = self._rules_from_env(val) + elif attr == 'wait_command': + val = int(val) + elif attr in ('require_confirmation', 'no_colors'): + val = val.lower() == 'true' + return val + + def _rules_from_env(self, val): + """Transforms rules list from env-string to python.""" + val = val.split(':') + if 'DEFAULT' in val: + val = DEFAULT + [rule for rule in val if rule != 'DEFAULT'] + return val diff --git a/thefuck/logs.py b/thefuck/logs.py index 9bfb02b..b21b245 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -11,17 +11,21 @@ def color(color_, settings): return color_ -def rule_failed(rule, exc_info, settings): +def exception(title, exc_info, settings): sys.stderr.write( - u'{warn}[WARN] Rule {name}:{reset}\n{trace}' + u'{warn}[WARN] {title}:{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, + title=title, trace=''.join(format_exception(*exc_info)))) +def rule_failed(rule, exc_info, settings): + exception('Rule {}'.format(rule.name), exc_info, settings) + + def show_command(new_command, settings): sys.stderr.write('{bold}{command}{reset}\n'.format( command=new_command, diff --git a/thefuck/main.py b/thefuck/main.py index a6c24ee..2db6210 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -7,7 +7,7 @@ import os import sys from psutil import Process, TimeoutExpired import colorama -from thefuck import logs +from . import logs, conf Command = namedtuple('Command', ('script', 'stdout', 'stderr')) @@ -25,30 +25,6 @@ def setup_user_dir(): return user_dir -def get_settings(user_dir): - """Returns prepared settings module.""" - settings = load_source('settings', - str(user_dir.joinpath('settings.py'))) - 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 - - -def is_rule_enabled(settings, rule): - """Returns `True` when rule mentioned in `rules` or `rules` - isn't defined. - - """ - 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)) @@ -66,7 +42,7 @@ def get_rules(user_dir, settings): 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): + if loaded_rule in settings.rules: yield loaded_rule @@ -145,7 +121,7 @@ def is_second_run(command): def main(): colorama.init() user_dir = setup_user_dir() - settings = get_settings(user_dir) + settings = conf.Settings(user_dir) command = get_command(settings, sys.argv) if command: diff --git a/thefuck/utils.py b/thefuck/utils.py index 534a634..4c87970 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -37,10 +37,7 @@ def wrap_settings(params): def decorator(fn): @wraps(fn) def wrapper(command, settings): - for key, val in params.items(): - if not hasattr(settings, key): - setattr(settings, key, val) - return fn(command, settings) + return fn(command, settings.update(**params)) return wrapper return decorator From 0553d57ec145db4a043b3f115a5e8b7865c3a4bc Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 22:29:23 +0200 Subject: [PATCH 013/107] Don't mess with inheritance for filling settings --- tests/test_conf.py | 14 ++--- tests/test_utils.py | 6 +-- thefuck/conf.py | 122 +++++++++++++++++++++++--------------------- thefuck/main.py | 2 +- 4 files changed, 74 insertions(+), 70 deletions(-) diff --git a/tests/test_conf.py b/tests/test_conf.py index c74d343..41c693e 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -19,8 +19,8 @@ def test_default(): def test_settings_defaults(): with patch('thefuck.conf.load_source', return_value=object()), \ patch('thefuck.conf.os.environ', new_callable=lambda: {}): - for key, val in conf.Settings.defaults.items(): - assert getattr(conf.Settings(Mock()), key) == val + for key, val in conf.DEFAULT_SETTINGS.items(): + assert getattr(conf.get_settings(Mock()), key) == val def test_settings_from_file(): @@ -29,7 +29,7 @@ def test_settings_from_file(): require_confirmation=True, no_colors=True)), \ patch('thefuck.conf.os.environ', new_callable=lambda: {}): - settings = conf.Settings(Mock()) + settings = conf.get_settings(Mock()) assert settings.rules == ['test'] assert settings.wait_command == 10 assert settings.require_confirmation is True @@ -42,7 +42,7 @@ def test_settings_from_file_with_DEFAULT(): require_confirmation=True, no_colors=True)), \ patch('thefuck.conf.os.environ', new_callable=lambda: {}): - settings = conf.Settings(Mock()) + settings = conf.get_settings(Mock()) assert settings.rules == conf.DEFAULT + ['test'] @@ -54,7 +54,7 @@ def test_settings_from_env(): 'THEFUCK_WAIT_COMMAND': '55', 'THEFUCK_REQUIRE_CONFIRMATION': 'true', 'THEFUCK_NO_COLORS': 'false'}): - settings = conf.Settings(Mock()) + settings = conf.get_settings(Mock()) assert settings.rules == ['bash', 'lisp'] assert settings.wait_command == 55 assert settings.require_confirmation is True @@ -64,12 +64,12 @@ def test_settings_from_env(): def test_settings_from_env_with_DEFAULT(): with patch('thefuck.conf.load_source', return_value=Mock()), \ patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT:bash:lisp'}): - settings = conf.Settings(Mock()) + settings = conf.get_settings(Mock()) assert settings.rules == conf.DEFAULT + ['bash', 'lisp'] def test_update_settings(): - settings = conf.BaseSettings({'key': 'val'}) + settings = conf.Settings({'key': 'val'}) new_settings = settings.update(key='new-val') assert new_settings.key == 'new-val' assert settings.key == 'val' diff --git a/tests/test_utils.py b/tests/test_utils.py index e1ec93d..4343904 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,15 +1,15 @@ from mock import Mock from thefuck.utils import sudo_support, wrap_settings from thefuck.main import Command -from thefuck.conf import BaseSettings +from thefuck.conf import Settings def test_wrap_settings(): fn = lambda _, settings: settings._conf - assert wrap_settings({'key': 'val'})(fn)(None, BaseSettings({})) \ + assert wrap_settings({'key': 'val'})(fn)(None, Settings({})) \ == {'key': 'val'} assert wrap_settings({'key': 'new-val'})(fn)( - None, BaseSettings({'key': 'val'})) == {'key': 'new-val'} + None, Settings({'key': 'val'})) == {'key': 'new-val'} def test_sudo_support(): diff --git a/thefuck/conf.py b/thefuck/conf.py index 4593905..2f3da7d 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -40,7 +40,7 @@ class _DefaultRules(RulesList): DEFAULT = _DefaultRules([]) -class BaseSettings(object): +class Settings(object): def __init__(self, conf): self._conf = conf @@ -51,70 +51,74 @@ class BaseSettings(object): """Returns new settings with new values from `kwargs`.""" conf = copy(self._conf) conf.update(kwargs) - return BaseSettings(conf) + return Settings(conf) -class Settings(BaseSettings): - """Settings loaded from defaults/file/env.""" - defaults = {'rules': DEFAULT, - 'wait_command': 3, - 'require_confirmation': False, - 'no_colors': False} +DEFAULT_SETTINGS = {'rules': DEFAULT, + 'wait_command': 3, + 'require_confirmation': False, + 'no_colors': False} - env_to_attr = {'THEFUCK_RULES': 'rules', - 'THEFUCK_WAIT_COMMAND': 'wait_command', - 'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation', - 'THEFUCK_NO_COLORS': 'no_colors'} +ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', + 'THEFUCK_WAIT_COMMAND': 'wait_command', + 'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation', + 'THEFUCK_NO_COLORS': 'no_colors'} - def __init__(self, user_dir): - super(Settings, self).__init__(self._load_conf(user_dir)) - def _load_conf(self, user_dir): - conf = copy(self.defaults) - try: - conf.update(self._load_from_file(user_dir)) - except: - logs.exception("Can't load settings from file", - sys.exc_info(), - BaseSettings(conf)) - try: - conf.update(self._load_from_env()) - except: - logs.exception("Can't load settings from env", - sys.exc_info(), - BaseSettings(conf)) - if not isinstance(conf['rules'], RulesList): - conf['rules'] = RulesList(conf['rules']) - return conf +def _settings_from_file(user_dir): + """Loads settings from file.""" + settings = load_source('settings', + text_type(user_dir.joinpath('settings.py'))) + return {key: getattr(settings, key) + for key in DEFAULT_SETTINGS.keys() + if hasattr(settings, key)} - def _load_from_file(self, user_dir): - """Loads settings from file.""" - settings = load_source('settings', - text_type(user_dir.joinpath('settings.py'))) - return {key: getattr(settings, key) - for key in self.defaults.keys() - if hasattr(settings, key)} - def _load_from_env(self): - """Loads settings from env.""" - return {attr: self._val_from_env(env, attr) - for env, attr in self.env_to_attr.items() - if env in os.environ} +def _rules_from_env(val): + """Transforms rules list from env-string to python.""" + val = val.split(':') + if 'DEFAULT' in val: + val = DEFAULT + [rule for rule in val if rule != 'DEFAULT'] + return val - def _val_from_env(self, env, attr): - """Transforms env-strings to python.""" - val = os.environ[env] - if attr == 'rules': - val = self._rules_from_env(val) - elif attr == 'wait_command': - val = int(val) - elif attr in ('require_confirmation', 'no_colors'): - val = val.lower() == 'true' - return val - def _rules_from_env(self, val): - """Transforms rules list from env-string to python.""" - val = val.split(':') - if 'DEFAULT' in val: - val = DEFAULT + [rule for rule in val if rule != 'DEFAULT'] - return val +def _val_from_env(env, attr): + """Transforms env-strings to python.""" + val = os.environ[env] + if attr == 'rules': + val = _rules_from_env(val) + elif attr == 'wait_command': + val = int(val) + elif attr in ('require_confirmation', 'no_colors'): + val = val.lower() == 'true' + return val + + +def _settings_from_env(): + """Loads settings from env.""" + return {attr: _val_from_env(env, attr) + for env, attr in ENV_TO_ATTR.items() + if env in os.environ} + + +def get_settings(user_dir): + """Returns settings filled with values from `settings.py` and env.""" + conf = copy(DEFAULT_SETTINGS) + try: + conf.update(_settings_from_file(user_dir)) + except Exception: + logs.exception("Can't load settings from file", + sys.exc_info(), + Settings(conf)) + + try: + conf.update(_settings_from_env()) + except Exception: + logs.exception("Can't load settings from env", + sys.exc_info(), + Settings(conf)) + + if not isinstance(conf['rules'], RulesList): + conf['rules'] = RulesList(conf['rules']) + + return Settings(conf) diff --git a/thefuck/main.py b/thefuck/main.py index 2db6210..341c962 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -121,7 +121,7 @@ def is_second_run(command): def main(): colorama.init() user_dir = setup_user_dir() - settings = conf.Settings(user_dir) + settings = conf.get_settings(user_dir) command = get_command(settings, sys.argv) if command: From 20b6c4c1600e5d696c0f004242b6d21ce90ea4f0 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 22:36:18 +0200 Subject: [PATCH 014/107] Inherit `RulesNamesList` from `list` --- tests/test_conf.py | 10 +++++----- tests/test_main.py | 2 +- thefuck/conf.py | 29 ++++++++++------------------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/tests/test_conf.py b/tests/test_conf.py index 41c693e..929a41e 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -3,11 +3,11 @@ from thefuck.main import Rule from thefuck import conf -def test_rules_list(): - assert conf.RulesList(['bash', 'lisp']) == ['bash', 'lisp'] - assert conf.RulesList(['bash', 'lisp']) == conf.RulesList(['bash', 'lisp']) - assert Rule('lisp', None, None, False) in conf.RulesList(['lisp']) - assert Rule('bash', None, None, False) not in conf.RulesList(['lisp']) +def test_rules_names_list(): + assert conf.RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp'] + assert conf.RulesNamesList(['bash', 'lisp']) == conf.RulesNamesList(['bash', 'lisp']) + assert Rule('lisp', None, None, False) in conf.RulesNamesList(['lisp']) + assert Rule('bash', None, None, False) not in conf.RulesNamesList(['lisp']) def test_default(): diff --git a/tests/test_main.py b/tests/test_main.py index 12f06ea..aacc17d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -32,7 +32,7 @@ def test_get_rules(): main.Rule('lisp', 'lisp', 'lisp', True)] assert list(main.get_rules( Path('~'), - Mock(rules=conf.RulesList(['bash'])))) \ + Mock(rules=conf.RulesNamesList(['bash'])))) \ == [main.Rule('bash', 'bash', 'bash', True), main.Rule('bash', 'bash', 'bash', True)] diff --git a/thefuck/conf.py b/thefuck/conf.py index 2f3da7d..6c2b894 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -6,38 +6,29 @@ from six import text_type from . import logs -class RulesList(object): +class RulesNamesList(list): """Wrapper a top of list for string rules names.""" - def __init__(self, rules): - self.rules = rules - def __contains__(self, item): - return item.name in self.rules - - def __getattr__(self, item): - return getattr(self.rules, item) - - def __eq__(self, other): - return self.rules == other + return super(RulesNamesList, self).__contains__(item.name) -class _DefaultRules(RulesList): +class _DefaultRulesNames(RulesNamesList): def __add__(self, items): - return _DefaultRules(self.rules + items) + return _DefaultRulesNames(list(self) + items) def __contains__(self, item): return item.enabled_by_default or \ - super(_DefaultRules, self).__contains__(item) + super(_DefaultRulesNames, self).__contains__(item) def __eq__(self, other): - if isinstance(other, _DefaultRules): - return self.rules == other.rules + if isinstance(other, _DefaultRulesNames): + return super(_DefaultRulesNames, self).__eq__(other) else: return False -DEFAULT = _DefaultRules([]) +DEFAULT = _DefaultRulesNames([]) class Settings(object): @@ -118,7 +109,7 @@ def get_settings(user_dir): sys.exc_info(), Settings(conf)) - if not isinstance(conf['rules'], RulesList): - conf['rules'] = RulesList(conf['rules']) + if not isinstance(conf['rules'], RulesNamesList): + conf['rules'] = RulesNamesList(conf['rules']) return Settings(conf) From 54c408a6b572e84f75b1f11133d6769a6aef3dab Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 22:37:11 +0200 Subject: [PATCH 015/107] Rename `DEFAULT` to `DEFAULT_RULES` --- README.md | 4 ++-- tests/test_conf.py | 14 +++++++------- tests/test_main.py | 2 +- thefuck/conf.py | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 650ea8d..8b98fec 100644 --- a/README.md +++ b/README.md @@ -196,14 +196,14 @@ def get_new_command(command, settings): The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`: -* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT`; +* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`; * `require_confirmation` – require confirmation before running new command, by default `False`; * `wait_command` – max amount of time in seconds for getting previous command output; * `no_colors` – disable colored output. Or via environment variables: -* `THEFUCK_RULES` – list of enabled rules, like `DEFAULT:rm_root` or `sudo:no_command`; +* `THEFUCK_RULES` – list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`; * `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`; * `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output; * `THEFUCK_NO_COLORS` – disable colored output, `true/false`. diff --git a/tests/test_conf.py b/tests/test_conf.py index 929a41e..f25e9f5 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -11,9 +11,9 @@ def test_rules_names_list(): def test_default(): - assert Rule('test', None, None, True) in conf.DEFAULT - assert Rule('test', None, None, False) not in conf.DEFAULT - assert Rule('test', None, None, False) in (conf.DEFAULT + ['test']) + assert Rule('test', None, None, True) in conf.DEFAULT_RULES + assert Rule('test', None, None, False) not in conf.DEFAULT_RULES + assert Rule('test', None, None, False) in (conf.DEFAULT_RULES + ['test']) def test_settings_defaults(): @@ -37,13 +37,13 @@ def test_settings_from_file(): def test_settings_from_file_with_DEFAULT(): - with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT + ['test'], + with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT_RULES + ['test'], wait_command=10, require_confirmation=True, no_colors=True)), \ patch('thefuck.conf.os.environ', new_callable=lambda: {}): settings = conf.get_settings(Mock()) - assert settings.rules == conf.DEFAULT + ['test'] + assert settings.rules == conf.DEFAULT_RULES + ['test'] def test_settings_from_env(): @@ -63,9 +63,9 @@ def test_settings_from_env(): def test_settings_from_env_with_DEFAULT(): with patch('thefuck.conf.load_source', return_value=Mock()), \ - patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT:bash:lisp'}): + patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}): settings = conf.get_settings(Mock()) - assert settings.rules == conf.DEFAULT + ['bash', 'lisp'] + assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp'] def test_update_settings(): diff --git a/tests/test_main.py b/tests/test_main.py index aacc17d..19e460c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -25,7 +25,7 @@ def test_get_rules(): glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] assert list(main.get_rules( Path('~'), - Mock(rules=conf.DEFAULT))) \ + Mock(rules=conf.DEFAULT_RULES))) \ == [main.Rule('bash', 'bash', 'bash', True), main.Rule('lisp', 'lisp', 'lisp', True), main.Rule('bash', 'bash', 'bash', True), diff --git a/thefuck/conf.py b/thefuck/conf.py index 6c2b894..2da0f5c 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -28,7 +28,7 @@ class _DefaultRulesNames(RulesNamesList): return False -DEFAULT = _DefaultRulesNames([]) +DEFAULT_RULES = _DefaultRulesNames([]) class Settings(object): @@ -45,7 +45,7 @@ class Settings(object): return Settings(conf) -DEFAULT_SETTINGS = {'rules': DEFAULT, +DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'wait_command': 3, 'require_confirmation': False, 'no_colors': False} @@ -68,8 +68,8 @@ def _settings_from_file(user_dir): def _rules_from_env(val): """Transforms rules list from env-string to python.""" val = val.split(':') - if 'DEFAULT' in val: - val = DEFAULT + [rule for rule in val if rule != 'DEFAULT'] + if 'DEFAULT_RULES' in val: + val = DEFAULT_RULES + [rule for rule in val if rule != 'DEFAULT_RULES'] return val From ca67080bd9569c6b1096329f11a3661d0f78f3c9 Mon Sep 17 00:00:00 2001 From: Scott Abernethy Date: Thu, 23 Apr 2015 09:00:18 +1200 Subject: [PATCH 016/107] Brew installation note in README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 650ea8d..975bf1d 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,12 @@ If it fails try to use `easy_install`: sudo easy_install thefuck ``` +Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)): + +```bash +brew install thefuck +``` + And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX): ```bash From d3d1f992328dfaf245ef268090cf54bb492815b7 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 23:04:22 +0200 Subject: [PATCH 017/107] Move special data types to `types` --- tests/rules/test_cd_parent.py | 2 +- tests/rules/test_git_not_command.py | 2 +- tests/rules/test_git_push.py | 2 +- tests/rules/test_has_exists_script.py | 2 +- tests/rules/test_mkdir_p.py | 2 +- tests/rules/test_python_command.py | 4 ++- tests/rules/test_rm_dir.py | 2 +- tests/rules/test_ssh_known_host.py | 2 +- tests/rules/test_sudo.py | 2 +- tests/test_conf.py | 16 +----------- tests/test_main.py | 34 +++++++++++++------------- tests/test_types.py | 15 ++++++++++++ tests/test_utils.py | 5 ++-- thefuck/conf.py | 35 ++++++--------------------- thefuck/main.py | 24 +++++++----------- thefuck/types.py | 26 ++++++++++++++++++++ thefuck/utils.py | 2 +- 17 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 tests/test_types.py create mode 100644 thefuck/types.py diff --git a/tests/rules/test_cd_parent.py b/tests/rules/test_cd_parent.py index 7a0fbc8..60f8760 100644 --- a/tests/rules/test_cd_parent.py +++ b/tests/rules/test_cd_parent.py @@ -1,4 +1,4 @@ -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.cd_parent import match, get_new_command diff --git a/tests/rules/test_git_not_command.py b/tests/rules/test_git_not_command.py index 657953f..91df959 100644 --- a/tests/rules/test_git_not_command.py +++ b/tests/rules/test_git_not_command.py @@ -1,5 +1,5 @@ import pytest -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.git_not_command import match, get_new_command diff --git a/tests/rules/test_git_push.py b/tests/rules/test_git_push.py index 7504a72..57f6359 100644 --- a/tests/rules/test_git_push.py +++ b/tests/rules/test_git_push.py @@ -1,5 +1,5 @@ import pytest -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.git_push import match, get_new_command diff --git a/tests/rules/test_has_exists_script.py b/tests/rules/test_has_exists_script.py index 36938be..e00c4bd 100644 --- a/tests/rules/test_has_exists_script.py +++ b/tests/rules/test_has_exists_script.py @@ -1,5 +1,5 @@ from mock import Mock, patch -from thefuck.rules. has_exists_script import match, get_new_command +from thefuck.rules.has_exists_script import match, get_new_command def test_match(): diff --git a/tests/rules/test_mkdir_p.py b/tests/rules/test_mkdir_p.py index 128be2f..dae196f 100644 --- a/tests/rules/test_mkdir_p.py +++ b/tests/rules/test_mkdir_p.py @@ -1,4 +1,4 @@ -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.mkdir_p import match, get_new_command diff --git a/tests/rules/test_python_command.py b/tests/rules/test_python_command.py index e807126..56d9b3f 100644 --- a/tests/rules/test_python_command.py +++ b/tests/rules/test_python_command.py @@ -1,9 +1,11 @@ -from thefuck.main import Command +from thefuck.types 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 index 2362d0c..2d5ed62 100644 --- a/tests/rules/test_rm_dir.py +++ b/tests/rules/test_rm_dir.py @@ -1,4 +1,4 @@ -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.rm_dir import match, get_new_command diff --git a/tests/rules/test_ssh_known_host.py b/tests/rules/test_ssh_known_host.py index 9c8dc0d..252cac1 100644 --- a/tests/rules/test_ssh_known_host.py +++ b/tests/rules/test_ssh_known_host.py @@ -1,7 +1,7 @@ import os import pytest from mock import Mock -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.ssh_known_hosts import match, get_new_command, remove_offending_keys diff --git a/tests/rules/test_sudo.py b/tests/rules/test_sudo.py index 4f0c831..4f3b3f5 100644 --- a/tests/rules/test_sudo.py +++ b/tests/rules/test_sudo.py @@ -1,4 +1,4 @@ -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.sudo import match, get_new_command diff --git a/tests/test_conf.py b/tests/test_conf.py index f25e9f5..b8e6d6c 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -1,15 +1,8 @@ from mock import patch, Mock -from thefuck.main import Rule +from thefuck.types import Rule from thefuck import conf -def test_rules_names_list(): - assert conf.RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp'] - assert conf.RulesNamesList(['bash', 'lisp']) == conf.RulesNamesList(['bash', 'lisp']) - assert Rule('lisp', None, None, False) in conf.RulesNamesList(['lisp']) - assert Rule('bash', None, None, False) not in conf.RulesNamesList(['lisp']) - - def test_default(): assert Rule('test', None, None, True) in conf.DEFAULT_RULES assert Rule('test', None, None, False) not in conf.DEFAULT_RULES @@ -66,10 +59,3 @@ def test_settings_from_env_with_DEFAULT(): patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}): settings = conf.get_settings(Mock()) assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp'] - - -def test_update_settings(): - settings = conf.Settings({'key': 'val'}) - new_settings = settings.update(key='new-val') - assert new_settings.key == 'new-val' - assert settings.key == 'val' diff --git a/tests/test_main.py b/tests/test_main.py index 19e460c..078f1a8 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,7 +1,7 @@ from subprocess import PIPE from pathlib import PosixPath, Path from mock import patch, Mock -from thefuck import main, conf +from thefuck import main, conf, types def test_load_rule(): @@ -13,7 +13,7 @@ def test_load_rule(): 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) + == types.Rule('bash', match, get_new_command, True) load_source.assert_called_once_with('bash', '/rules/bash.py') @@ -26,15 +26,15 @@ def test_get_rules(): assert list(main.get_rules( Path('~'), Mock(rules=conf.DEFAULT_RULES))) \ - == [main.Rule('bash', 'bash', 'bash', True), - main.Rule('lisp', 'lisp', 'lisp', True), - main.Rule('bash', 'bash', 'bash', True), - main.Rule('lisp', 'lisp', 'lisp', True)] + == [types.Rule('bash', 'bash', 'bash', True), + types.Rule('lisp', 'lisp', 'lisp', True), + types.Rule('bash', 'bash', 'bash', True), + types.Rule('lisp', 'lisp', 'lisp', True)] assert list(main.get_rules( Path('~'), - Mock(rules=conf.RulesNamesList(['bash'])))) \ - == [main.Rule('bash', 'bash', 'bash', True), - main.Rule('bash', 'bash', 'bash', True)] + Mock(rules=types.RulesNamesList(['bash'])))) \ + == [types.Rule('bash', 'bash', 'bash', True), + types.Rule('bash', 'bash', 'bash', True)] def test_get_command(): @@ -47,7 +47,7 @@ def test_get_command(): Popen.return_value.stderr.read.return_value = b'stderr' assert main.get_command(Mock(), ['thefuck', 'apt-get', 'search', 'vim']) \ - == main.Command('apt-get search vim', 'stdout', 'stderr') + == types.Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', shell=True, stdout=PIPE, @@ -57,12 +57,12 @@ def test_get_command(): def test_get_matched_rule(capsys): - 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 = [types.Rule('', lambda x, _: x.script == 'cd ..', None, True), + types.Rule('', lambda *_: False, None, True), + types.Rule('rule', Mock(side_effect=OSError('Denied')), None, True)] + assert main.get_matched_rule(types.Command('ls', '', ''), rules, Mock(no_colors=True)) is None - assert main.get_matched_rule(main.Command('cd ..', '', ''), + assert main.get_matched_rule(types.Command('cd ..', '', ''), rules, Mock(no_colors=True)) == rules[0] assert capsys.readouterr()[1].split('\n')[0] \ == '[WARN] Rule rule:' @@ -70,11 +70,11 @@ def test_get_matched_rule(capsys): def test_run_rule(capsys): with patch('thefuck.main.confirm', return_value=True): - main.run_rule(main.Rule('', None, lambda *_: 'new-command', True), + main.run_rule(types.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', True), + main.run_rule(types.Rule('', None, lambda *_: 'new-command', True), None, None) assert capsys.readouterr() == ('', '') diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 0000000..0df4a16 --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,15 @@ +from thefuck.types import Rule, RulesNamesList, Settings + + +def test_rules_names_list(): + assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp'] + assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp']) + assert Rule('lisp', None, None, False) in RulesNamesList(['lisp']) + assert Rule('bash', None, None, False) not in RulesNamesList(['lisp']) + + +def test_update_settings(): + settings = Settings({'key': 'val'}) + new_settings = settings.update(key='new-val') + assert new_settings.key == 'new-val' + assert settings.key == 'val' diff --git a/tests/test_utils.py b/tests/test_utils.py index 4343904..995614d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,10 @@ from mock import Mock from thefuck.utils import sudo_support, wrap_settings -from thefuck.main import Command -from thefuck.conf import Settings +from thefuck.types import Command, Settings def test_wrap_settings(): - fn = lambda _, settings: settings._conf + fn = lambda _, settings: settings assert wrap_settings({'key': 'val'})(fn)(None, Settings({})) \ == {'key': 'val'} assert wrap_settings({'key': 'new-val'})(fn)( diff --git a/thefuck/conf.py b/thefuck/conf.py index 2da0f5c..0c6f29a 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -3,17 +3,10 @@ from imp import load_source import os import sys from six import text_type -from . import logs +from . import logs, types -class RulesNamesList(list): - """Wrapper a top of list for string rules names.""" - - def __contains__(self, item): - return super(RulesNamesList, self).__contains__(item.name) - - -class _DefaultRulesNames(RulesNamesList): +class _DefaultRulesNames(types.RulesNamesList): def __add__(self, items): return _DefaultRulesNames(list(self) + items) @@ -31,20 +24,6 @@ class _DefaultRulesNames(RulesNamesList): DEFAULT_RULES = _DefaultRulesNames([]) -class Settings(object): - def __init__(self, conf): - self._conf = conf - - def __getattr__(self, item): - return self._conf.get(item) - - def update(self, **kwargs): - """Returns new settings with new values from `kwargs`.""" - conf = copy(self._conf) - conf.update(kwargs) - return Settings(conf) - - DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'wait_command': 3, 'require_confirmation': False, @@ -100,16 +79,16 @@ def get_settings(user_dir): except Exception: logs.exception("Can't load settings from file", sys.exc_info(), - Settings(conf)) + types.Settings(conf)) try: conf.update(_settings_from_env()) except Exception: logs.exception("Can't load settings from env", sys.exc_info(), - Settings(conf)) + types.Settings(conf)) - if not isinstance(conf['rules'], RulesNamesList): - conf['rules'] = RulesNamesList(conf['rules']) + if not isinstance(conf['rules'], types.RulesNamesList): + conf['rules'] = types.RulesNamesList(conf['rules']) - return Settings(conf) + return types.Settings(conf) diff --git a/thefuck/main.py b/thefuck/main.py index 341c962..ea286eb 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -1,4 +1,3 @@ -from collections import namedtuple from imp import load_source from pathlib import Path from os.path import expanduser @@ -7,12 +6,7 @@ import os import sys from psutil import Process, TimeoutExpired import colorama -from . import logs, conf - - -Command = namedtuple('Command', ('script', 'stdout', 'stderr')) -Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', - 'enabled_by_default')) +from . import logs, conf, types def setup_user_dir(): @@ -28,16 +22,16 @@ def setup_user_dir(): 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, - getattr(rule_module, 'enabled_by_default', True)) + return types.Rule(rule.name[:-3], rule_module.match, + rule_module.get_new_command, + getattr(rule_module, 'enabled_by_default', True)) def get_rules(user_dir, settings): """Returns all enabled rules.""" - bundled = Path(__file__).parent\ - .joinpath('rules')\ - .glob('*.py') + bundled = Path(__file__).parent \ + .joinpath('rules') \ + .glob('*.py') user = user_dir.joinpath('rules').glob('*.py') for rule in sorted(list(bundled)) + list(user): if rule.name != '__init__.py': @@ -77,8 +71,8 @@ def get_command(settings, args): result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=dict(os.environ, LANG='C')) if wait_output(settings, result): - return Command(script, result.stdout.read().decode('utf-8'), - result.stderr.read().decode('utf-8')) + return types.Command(script, result.stdout.read().decode('utf-8'), + result.stderr.read().decode('utf-8')) def get_matched_rule(command, rules, settings): diff --git a/thefuck/types.py b/thefuck/types.py new file mode 100644 index 0000000..b426788 --- /dev/null +++ b/thefuck/types.py @@ -0,0 +1,26 @@ +from collections import namedtuple + + +Command = namedtuple('Command', ('script', 'stdout', 'stderr')) + +Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', + 'enabled_by_default')) + + +class RulesNamesList(list): + """Wrapper a top of list for string rules names.""" + + def __contains__(self, item): + return super(RulesNamesList, self).__contains__(item.name) + + +class Settings(dict): + + def __getattr__(self, item): + return self.get(item) + + def update(self, **kwargs): + """Returns new settings with new values from `kwargs`.""" + conf = dict(self) + conf.update(kwargs) + return Settings(conf) diff --git a/thefuck/utils.py b/thefuck/utils.py index 4c87970..7ee66c2 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -1,7 +1,7 @@ from functools import wraps import os import six -from thefuck.main import Command +from .types import Command def which(program): From 12eab10028c8a31f920947f1249d43cb42e48784 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Wed, 22 Apr 2015 23:08:10 +0200 Subject: [PATCH 018/107] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 975bf1d..ce66738 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ and `get_new_command(command: Command, settings: Settings) -> str`. `Command` has three attributes: `script`, `stdout` and `stderr`. -`Settings` is `~/.thefuck/settings.py`. +`Settings` is a special object filled with `~/.thefuck/settings.py` and values from env, [more](#settings). Simple example of the rule for running script with `sudo`: From 0272e8a8018344d0f9757e21999ffb517d199a03 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 22 Apr 2015 23:37:02 +0200 Subject: [PATCH 019/107] Bump to 1.28 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6c07314..d27b9f0 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.27' +VERSION = '1.28' setup(name='thefuck', From 5de020bccd2a7ea6ba34087fef06f08df09ced41 Mon Sep 17 00:00:00 2001 From: Brobin Date: Wed, 22 Apr 2015 20:41:56 -0500 Subject: [PATCH 020/107] unf*ck sl -> ls --- thefuck/rules/sl_ls.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 thefuck/rules/sl_ls.py diff --git a/thefuck/rules/sl_ls.py b/thefuck/rules/sl_ls.py new file mode 100644 index 0000000..0b3d017 --- /dev/null +++ b/thefuck/rules/sl_ls.py @@ -0,0 +1,14 @@ +""" +This happens way too often + +When typing really fast cause I'm a 1337 H4X0R, +I often fuck up 'ls' and type 'sl'. No more! +""" + + +def match(command, settings): + return command.script == 'sl' + + +def get_new_command(command, settings): + return 'ls' From 776ff4e3dbe1337e68bef24483d92d29cd45f06f Mon Sep 17 00:00:00 2001 From: Brobin Date: Wed, 22 Apr 2015 20:45:12 -0500 Subject: [PATCH 021/107] updated readme for sl_ls --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6d584ed..ec680ef 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ using matched rule and run it. Rules enabled by default: * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; +* `sl_ls` – changes `sl` to `ls`; * `rm_dir` – adds `-rf` when you trying to remove directory; * `ssh_known_hosts` – removes host from `known_hosts` on warning; * `sudo` – prepends `sudo` to previous command if it failed because of permissions; From 157e3e95fc56353d3961d1ebde7a703103e7b335 Mon Sep 17 00:00:00 2001 From: Brobin Date: Wed, 22 Apr 2015 20:51:18 -0500 Subject: [PATCH 022/107] added sl_ls test :shipit: --- tests/rules/test_sl_ls.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/rules/test_sl_ls.py diff --git a/tests/rules/test_sl_ls.py b/tests/rules/test_sl_ls.py new file mode 100644 index 0000000..fd83b04 --- /dev/null +++ b/tests/rules/test_sl_ls.py @@ -0,0 +1,12 @@ + +from thefuck.types import Command +from thefuck.rules.sl_ls import match, get_new_command + + +def test_match(): + assert match(Command('sl', '', ''), None) + assert not match(Command('ls', '', ''), None) + + +def test_get_new_command(): + assert get_new_command(Command('sl', '', ''), None) == 'ls' From af2bfe7c582c588db7f2a148d46e1cf588938165 Mon Sep 17 00:00:00 2001 From: Namwoo Kim Date: Thu, 23 Apr 2015 15:25:12 +0900 Subject: [PATCH 023/107] Add a support for pip unknown commands --- tests/rules/test_pip_unknown_command.py | 24 ++++++++++++++++++++++++ thefuck/rules/pip_unknown_command.py | 15 +++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/rules/test_pip_unknown_command.py create mode 100644 thefuck/rules/pip_unknown_command.py diff --git a/tests/rules/test_pip_unknown_command.py b/tests/rules/test_pip_unknown_command.py new file mode 100644 index 0000000..61b5c99 --- /dev/null +++ b/tests/rules/test_pip_unknown_command.py @@ -0,0 +1,24 @@ +import pytest +from thefuck.types import Command +from thefuck.rules.pip_unknown_command import match, get_new_command + + +@pytest.fixture +def pip_unknown_cmd(): + return '''ERROR: unknown command "instatl" - maybe you meant "install"''' + + +@pytest.fixture +def pip_unknown_cmd_without_recommend(): + return '''ERROR: unknown command "i"''' + + +def test_match(pip_unknown_cmd, pip_unknown_cmd_without_recommend): + assert match(Command('pip instatl', '', pip_unknown_cmd), None) + assert not match(Command('pip i', '', pip_unknown_cmd_without_recommend), + None) + + +def test_get_new_command(pip_unknown_cmd): + assert get_new_command(Command('pip instatl', '', pip_unknown_cmd), None)\ + == 'pip install' diff --git a/thefuck/rules/pip_unknown_command.py b/thefuck/rules/pip_unknown_command.py new file mode 100644 index 0000000..162258e --- /dev/null +++ b/thefuck/rules/pip_unknown_command.py @@ -0,0 +1,15 @@ +import re + + +def match(command, settings): + return ('pip' in command.script and + 'unknown command' in command.stderr and + 'maybe you meant' in command.stderr) + + +def get_new_command(command, settings): + broken_cmd = re.findall(r'ERROR: unknown command \"([a-z]+)\"', + command.stderr)[0] + new_cmd = re.findall(r'maybe you meant \"([a-z]+)\"', command.stderr)[0] + + return command.script.replace(broken_cmd, new_cmd, 1) From bb42780ca5977e7eba7b21dd4a85491695d3d11a Mon Sep 17 00:00:00 2001 From: Namwoo Kim Date: Thu, 23 Apr 2015 15:44:43 +0900 Subject: [PATCH 024/107] Update README.md and remove whitespaces --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6d584ed..372bafe 100644 --- a/README.md +++ b/README.md @@ -131,12 +131,12 @@ end Or in your Powershell `$PROFILE` on Windows: ```powershell -function fuck { +function fuck { $fuck = $(thefuck (get-history -count 1).commandline) - if($fuck.startswith("echo")) { - $fuck.substring(5) - } - else { iex "$fuck" } + if($fuck.startswith("echo")) { + $fuck.substring(5) + } + else { iex "$fuck" } } ``` @@ -162,6 +162,7 @@ using matched rule and run it. Rules enabled by default: * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; +* `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `rm_dir` – adds `-rf` when you trying to remove directory; * `ssh_known_hosts` – removes host from `known_hosts` on warning; @@ -203,7 +204,7 @@ def get_new_command(command, settings): The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`: * `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`; -* `require_confirmation` – require confirmation before running new command, by default `False`; +* `require_confirmation` – require confirmation before running new command, by default `False`; * `wait_command` – max amount of time in seconds for getting previous command output; * `no_colors` – disable colored output. From 07b9aba0d0b21d4b52f471bc2c7403ad4b484d19 Mon Sep 17 00:00:00 2001 From: Alexander Kozhevnikov Date: Thu, 23 Apr 2015 10:29:34 +0300 Subject: [PATCH 025/107] MacOSX specific message Patch for understanding macosx message. Example case: ``` [10:24:48][bethrezen@bethrezen-mac ~]$ apachectl graceful This operation requires root. [10:24:54][bethrezen@bethrezen-mac ~]$ fuck No fuck given ``` --- thefuck/rules/sudo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/thefuck/rules/sudo.py b/thefuck/rules/sudo.py index 2c98579..2588ee5 100644 --- a/thefuck/rules/sudo.py +++ b/thefuck/rules/sudo.py @@ -6,6 +6,7 @@ patterns = ['permission denied', 'Operation not permitted', 'root privilege', 'This command has to be run under the root user.', + 'This operation requires root.', 'You need to be root to perform this command.'] From 24ce459f2cac721a08ab4e359119dc5603b03fa3 Mon Sep 17 00:00:00 2001 From: Namwoo Kim Date: Thu, 23 Apr 2015 17:00:57 +0900 Subject: [PATCH 026/107] Add a support for unknown brew commands - #83 --- tests/rules/test_brew_unknown_command.py | 28 ++++++++++++++++++++ thefuck/rules/brew_unknown_command.py | 33 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/rules/test_brew_unknown_command.py create mode 100644 thefuck/rules/brew_unknown_command.py diff --git a/tests/rules/test_brew_unknown_command.py b/tests/rules/test_brew_unknown_command.py new file mode 100644 index 0000000..408e998 --- /dev/null +++ b/tests/rules/test_brew_unknown_command.py @@ -0,0 +1,28 @@ +import pytest +from thefuck.types import Command +from thefuck.rules.brew_unknown_command import match, get_new_command +from thefuck.rules.brew_unknown_command import brew_commands + + +@pytest.fixture +def brew_unknown_cmd(): + return '''Error: Unknown command: inst''' + + +@pytest.fixture +def brew_unknown_cmd_instaa(): + return '''Error: Unknown command: instaa''' + + +def test_match(brew_unknown_cmd): + assert match(Command('brew inst', '', brew_unknown_cmd), None) + for command in brew_commands: + assert not match(Command('brew ' + command, '', ''), None) + + +def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd_instaa): + assert get_new_command(Command('brew inst', '', brew_unknown_cmd), None)\ + == 'brew list' + + assert get_new_command(Command('brew instaa', '', brew_unknown_cmd_instaa), + None) == 'brew install' diff --git a/thefuck/rules/brew_unknown_command.py b/thefuck/rules/brew_unknown_command.py new file mode 100644 index 0000000..447b3c1 --- /dev/null +++ b/thefuck/rules/brew_unknown_command.py @@ -0,0 +1,33 @@ +import difflib +import re +import thefuck.logs + +# This commands are based on Homebrew 0.9.5 +brew_commands = ['info', 'home', 'options', 'install', 'uninstall', 'search', + 'list', 'update', 'upgrade', 'pin', 'unpin', 'doctor', + 'create', 'edit'] + + +def _get_similar_commands(command): + return difflib.get_close_matches(command, brew_commands) + + +def match(command, settings): + is_proper_command = ('brew' in command.script and + 'Unknown command' in command.stderr) + + has_possible_commands = False + if is_proper_command: + broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)', + command.stderr)[0] + has_possible_commands = len(_get_similar_commands(broken_cmd)) > 0 + + return has_possible_commands + + +def get_new_command(command, settings): + broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)', + command.stderr)[0] + new_cmd = _get_similar_commands(broken_cmd)[0] + + return command.script.replace(broken_cmd, new_cmd, 1) From 961126421052a2811e83e5aa89d7a1890531608b Mon Sep 17 00:00:00 2001 From: Namwoo Kim Date: Thu, 23 Apr 2015 17:06:36 +0900 Subject: [PATCH 027/107] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6d584ed..e822cc7 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ sudo pip install thefuck --upgrade The Fuck tries to match rule for the previous command, create new command using matched rule and run it. Rules enabled by default: +* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `cd_parent` – changes `cd..` to `cd ..`; * `cp_omitting_directory` – adds `-a` when you `cp` directory; * `git_no_command` – fixes wrong git commands like `git brnch`; From 54b5cd61226994ebd5700fceb2e184ecfdc15f5f Mon Sep 17 00:00:00 2001 From: Namwoo Kim Date: Thu, 23 Apr 2015 17:42:03 +0900 Subject: [PATCH 028/107] Add a support for brew unavailable formulas --- tests/rules/test_brew_install.py | 49 ++++++++++++++++++++++++++++++++ thefuck/rules/brew_install.py | 43 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/rules/test_brew_install.py create mode 100644 thefuck/rules/brew_install.py diff --git a/tests/rules/test_brew_install.py b/tests/rules/test_brew_install.py new file mode 100644 index 0000000..bf06cd9 --- /dev/null +++ b/tests/rules/test_brew_install.py @@ -0,0 +1,49 @@ +import pytest +from thefuck.types import Command +from thefuck.rules.brew_install import match, get_new_command +from thefuck.rules.brew_install import brew_formulas + + +@pytest.fixture +def brew_no_available_formula(): + return '''Error: No available formula for elsticsearch ''' + + +@pytest.fixture +def brew_install_no_argument(): + return '''This command requires a formula argument''' + + +@pytest.fixture +def brew_already_installed(): + return '''Warning: git-2.3.5 already installed''' + + +def _is_not_okay_to_test(): + if 'elasticsearch' not in brew_formulas: + return True + return False + + +@pytest.mark.skipif(_is_not_okay_to_test(), + reason='No need to run if there\'s no formula') +def test_match(brew_no_available_formula, brew_already_installed, + brew_install_no_argument): + assert match(Command('brew install elsticsearch', '', + brew_no_available_formula), None) + assert not match(Command('brew install git', '', + brew_already_installed), None) + assert not match(Command('brew install', '', brew_install_no_argument), + None) + + +@pytest.mark.skipif(_is_not_okay_to_test(), + reason='No need to run if there\'s no formula') +def test_get_new_command(brew_no_available_formula): + assert get_new_command(Command('brew install elsticsearch', '', + brew_no_available_formula), None)\ + == 'brew install elasticsearch' + + assert get_new_command(Command('brew install aa', '', + brew_no_available_formula), + None) != 'brew install aha' diff --git a/thefuck/rules/brew_install.py b/thefuck/rules/brew_install.py new file mode 100644 index 0000000..0f33950 --- /dev/null +++ b/thefuck/rules/brew_install.py @@ -0,0 +1,43 @@ +import difflib +import os +import re +from subprocess import check_output + +import thefuck.logs + +# Formulars are base on each local system's status +brew_formulas = [] +try: + brew_path_prefix = check_output(['brew', '--prefix']).strip() + brew_formula_path = brew_path_prefix + '/Library/Formula' + + for file_name in os.listdir(brew_formula_path): + if file_name.endswith('.rb'): + brew_formulas.append(file_name.replace('.rb', '')) +except: + pass + + +def _get_similar_formulars(formula_name): + return difflib.get_close_matches(formula_name, brew_formulas, 1, 0.85) + + +def match(command, settings): + is_proper_command = ('brew install' in command.script and + 'No available formula' in command.stderr) + + has_possible_formulas = False + if is_proper_command: + formula = re.findall(r'Error: No available formula for ([a-z]+)', + command.stderr)[0] + has_possible_formulas = len(_get_similar_formulars(formula)) > 0 + + return has_possible_formulas + + +def get_new_command(command, settings): + not_exist_formula = re.findall(r'Error: No available formula for ([a-z]+)', + command.stderr)[0] + exist_formula = _get_similar_formulars(not_exist_formula)[0] + + return command.script.replace(not_exist_formula, exist_formula, 1) From 380827d1d992476be9b8e47822593d3ee3eb6c27 Mon Sep 17 00:00:00 2001 From: Nils Winkler Date: Thu, 23 Apr 2015 11:26:19 +0200 Subject: [PATCH 029/107] Using eval for Bash alias This fixes #108. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d584ed..f2137f8 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ brew install thefuck And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX): ```bash -alias fuck='$(thefuck $(fc -ln -1))' -# You can use whatever you want as an alias, like for mondays: +alias fuck='eval $(thefuck $(fc -ln -1))' +# You can use whatever you want as an alias, like for Mondays: alias FUCK='fuck' ``` From f1cce413b35b4a476568aa090ea1c86aff72f3bb Mon Sep 17 00:00:00 2001 From: Nils Winkler Date: Thu, 23 Apr 2015 08:35:50 +0200 Subject: [PATCH 030/107] Added rule for fixing Alt+Space character MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Happens on the Mac a lot when typing a pipe character (Alt+7), and keeping the Alt key pressed down for a bit too long, so instead of Space, you're typing Alt+Space. This rule replaces the Alt+Space with a simple Space character. $ ps -ef | grep foo -bash:  grep: command not found $ fuck ps -ef | grep foo --- README.md | 1 + tests/rules/test_fix_alt_space.py | 18 ++++++++++++++++++ thefuck/rules/fix_alt_space.py | 15 +++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 tests/rules/test_fix_alt_space.py create mode 100644 thefuck/rules/fix_alt_space.py diff --git a/README.md b/README.md index b492538..2a63b7d 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ using matched rule and run it. Rules enabled by default: * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `cd_parent` – changes `cd..` to `cd ..`; * `cp_omitting_directory` – adds `-a` when you `cp` directory; +* `fix_alt_space` – replaces Alt+Space with Space character; * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `has_exists_script` – prepends `./` when script/binary exists; diff --git a/tests/rules/test_fix_alt_space.py b/tests/rules/test_fix_alt_space.py new file mode 100644 index 0000000..2d66e08 --- /dev/null +++ b/tests/rules/test_fix_alt_space.py @@ -0,0 +1,18 @@ +# -*- encoding: utf-8 -*- + + +from thefuck.types import Command +from thefuck.rules.fix_alt_space import match, get_new_command + + +def test_match(): + """ The character before 'grep' is Alt+Space, which happens frequently on the Mac when typing + the pipe character (Alt+7), and holding the Alt key pressed for longer than necessary. """ + assert match(Command(u'ps -ef | grep foo', '', u'-bash:  grep: command not found'), None) + assert not match(Command('ps -ef | grep foo', '', ''), None) + assert not match(Command('', '', ''), None) + + +def test_get_new_command(): + """ Replace the Alt+Space character by a simple space """ + assert get_new_command(Command(u'ps -ef | grep foo', '', ''), None) == 'ps -ef | grep foo' diff --git a/thefuck/rules/fix_alt_space.py b/thefuck/rules/fix_alt_space.py new file mode 100644 index 0000000..3c74ab3 --- /dev/null +++ b/thefuck/rules/fix_alt_space.py @@ -0,0 +1,15 @@ +# -*- encoding: utf-8 -*- + +import re +from thefuck.utils import sudo_support + + +@sudo_support +def match(command, settings): + return ('command not found' in command.stderr.lower() + and u' ' in command.script) + + +@sudo_support +def get_new_command(command, settings): + return re.sub(u' ', ' ', command.script) From 473f5e6a333d89a54813daf5495d68b17490ae8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Sim=C3=B3n=20Andreo?= Date: Thu, 23 Apr 2015 17:34:34 +0200 Subject: [PATCH 031/107] Add composer rule --- tests/rules/test_composer_not_command.py | 48 ++++++++++++++++++++++++ thefuck/rules/composer_not_command.py | 16 ++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/rules/test_composer_not_command.py create mode 100644 thefuck/rules/composer_not_command.py diff --git a/tests/rules/test_composer_not_command.py b/tests/rules/test_composer_not_command.py new file mode 100644 index 0000000..c546289 --- /dev/null +++ b/tests/rules/test_composer_not_command.py @@ -0,0 +1,48 @@ +import pytest +from thefuck.main import Command +from thefuck.rules.composer_not_command import match, get_new_command + + +@pytest.fixture +def composer_not_command(): + return """ + + + [InvalidArgumentException] + Command "udpate" is not defined. + Did you mean this? + update + + +""" + + +@pytest.fixture +def composer_not_command_one_of_this(): + return """ + + + + [InvalidArgumentException] + Command "pdate" is not defined. + Did you mean one of these? + selfupdate + self-update + update + + + +""" + +def test_match(composer_not_command, composer_not_command_one_of_this): + assert match(Command('composer udpate', '', composer_not_command), None) + assert match(Command('composer pdate', '', composer_not_command_one_of_this), None) + assert not match(Command('ls update', '', composer_not_command), None) + #assert not match(Command('composer update', '', composer_command), None) + + +def test_get_new_command(composer_not_command, composer_not_command_one_of_this): + assert get_new_command(Command('composer udpate', '', composer_not_command), None)\ + == 'composer update' + assert get_new_command( + Command('composer pdate', '', composer_not_command_one_of_this), None) == 'composer selfupdate' diff --git a/thefuck/rules/composer_not_command.py b/thefuck/rules/composer_not_command.py new file mode 100644 index 0000000..ec8916e --- /dev/null +++ b/thefuck/rules/composer_not_command.py @@ -0,0 +1,16 @@ +import re + +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() + ) + ) + +def get_new_command(command, settings): + broken_cmd = re.findall(r"Command \"([^']*)\" is not defined", command.stderr)[0] + new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.stderr) + if not new_cmd: + new_cmd = re.findall(r'Did you mean one of these\?[^\n]*\n\s*([^\n]*)', command.stderr) + return command.script.replace(broken_cmd, new_cmd[0].strip(), 1) \ No newline at end of file From 1f96faef2cd530806310478f6e812113ed583874 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 23 Apr 2015 21:47:46 +0200 Subject: [PATCH 032/107] #116 Fix tests --- tests/rules/test_composer_not_command.py | 8 ++++---- thefuck/rules/composer_not_command.py | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/rules/test_composer_not_command.py b/tests/rules/test_composer_not_command.py index c546289..d1a2e02 100644 --- a/tests/rules/test_composer_not_command.py +++ b/tests/rules/test_composer_not_command.py @@ -1,5 +1,5 @@ import pytest -from thefuck.main import Command +from thefuck.types import Command from thefuck.rules.composer_not_command import match, get_new_command @@ -34,15 +34,15 @@ def composer_not_command_one_of_this(): """ + def test_match(composer_not_command, composer_not_command_one_of_this): assert match(Command('composer udpate', '', composer_not_command), None) assert match(Command('composer pdate', '', composer_not_command_one_of_this), None) assert not match(Command('ls update', '', composer_not_command), None) - #assert not match(Command('composer update', '', composer_command), None) def test_get_new_command(composer_not_command, composer_not_command_one_of_this): - assert get_new_command(Command('composer udpate', '', composer_not_command), None)\ - == 'composer update' + assert get_new_command(Command('composer udpate', '', composer_not_command), None) \ + == 'composer update' assert get_new_command( Command('composer pdate', '', composer_not_command_one_of_this), None) == 'composer selfupdate' diff --git a/thefuck/rules/composer_not_command.py b/thefuck/rules/composer_not_command.py index ec8916e..930608d 100644 --- a/thefuck/rules/composer_not_command.py +++ b/thefuck/rules/composer_not_command.py @@ -1,16 +1,15 @@ import re + 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() - ) - ) + and ('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): broken_cmd = re.findall(r"Command \"([^']*)\" is not defined", command.stderr)[0] new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.stderr) if not new_cmd: - new_cmd = re.findall(r'Did you mean one of these\?[^\n]*\n\s*([^\n]*)', command.stderr) + new_cmd = re.findall(r'Did you mean one of these\?[^\n]*\n\s*([^\n]*)', command.stderr) return command.script.replace(broken_cmd, new_cmd[0].strip(), 1) \ No newline at end of file From 48ec853436a7e4dd4caa12ccca7dc3ba374f2ae5 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 23 Apr 2015 21:50:38 +0200 Subject: [PATCH 033/107] Bump to 1.29 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d27b9f0..f247c2c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.28' +VERSION = '1.29' setup(name='thefuck', From 1fa7827f1a6a381cb20d9afb9bcdaa7ab3bd2355 Mon Sep 17 00:00:00 2001 From: Jonathan Arnett Date: Thu, 23 Apr 2015 18:35:18 -0400 Subject: [PATCH 034/107] Fixing fish shell example in README.md For me, `$history[1]` is the currently running command, so for the last one you want `$history[2]` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a63b7d..da503ab 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Or in `config.fish`: ```fish function fuck - eval (thefuck $history[1]) + eval (thefuck $history[2]) end ``` From a454438939e8c021a37ebe081d0b2dd4fef39667 Mon Sep 17 00:00:00 2001 From: Joseph Henrich Date: Thu, 23 Apr 2015 20:28:19 -0400 Subject: [PATCH 035/107] Fuck, I wanted ls -lah --- tests/rules/test_ls_lah.py | 13 +++++++++++++ thefuck/rules/ls_lah.py | 7 +++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/rules/test_ls_lah.py create mode 100644 thefuck/rules/ls_lah.py diff --git a/tests/rules/test_ls_lah.py b/tests/rules/test_ls_lah.py new file mode 100644 index 0000000..f7eb3ed --- /dev/null +++ b/tests/rules/test_ls_lah.py @@ -0,0 +1,13 @@ +from mock import patch, Mock +from thefuck.rules.ls_lah import match, get_new_command + + +def test_match(): + assert match(Mock(script='ls file.py'), None) + assert match(Mock(script='ls /opt'), None) + assert not match(Mock(script='ls -lah /opt'), None) + + +def test_get_new_command(): + assert get_new_command( Mock(script='ls file.py'), None) == 'ls -lah file.py' + assert get_new_command( Mock(script='ls'), None) == 'ls -lah' diff --git a/thefuck/rules/ls_lah.py b/thefuck/rules/ls_lah.py new file mode 100644 index 0000000..1de0283 --- /dev/null +++ b/thefuck/rules/ls_lah.py @@ -0,0 +1,7 @@ +def match(command, settings): + return ('ls' in command.script and not ('ls -' in command.script)) + +def get_new_command(command, settings): + command = command.script.split(' ') + command[0] = 'ls -lah' + return ' '.join(command) From e21befbcc481509f80acaa5f9579e1c7e695451c Mon Sep 17 00:00:00 2001 From: Joseph Henrich Date: Thu, 23 Apr 2015 20:33:38 -0400 Subject: [PATCH 036/107] add ls_lah to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2a63b7d..a8d65a6 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ using matched rule and run it. Rules enabled by default: * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `has_exists_script` – prepends `./` when script/binary exists; +* `ls_lah` – adds -lah to ls; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; From e55423899613ce62c530774dd02b6ef8d0d50202 Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 24 Apr 2015 05:22:19 +0200 Subject: [PATCH 037/107] #78 Disable when can't import `CommandNotFound` --- thefuck/rules/apt_get.py | 43 ++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/thefuck/rules/apt_get.py b/thefuck/rules/apt_get.py index 0e7af59..4d5eca6 100644 --- a/thefuck/rules/apt_get.py +++ b/thefuck/rules/apt_get.py @@ -1,28 +1,23 @@ -import sys +try: + import CommandNotFound +except ImportError: + enabled_by_default = False + def match(command, settings): - try: - import CommandNotFound - if 'not found' in command.stderr: - try: - c = CommandNotFound.CommandNotFound() - pkgs = c.getPackages(command.script.split(" ")[0]) - name,_ = pkgs[0] - return True - except IndexError: - # IndexError is thrown when no matching package is found - return False - except: - return False + if 'not found' in command.stderr: + try: + c = CommandNotFound.CommandNotFound() + pkgs = c.getPackages(command.script.split(" ")[0]) + name, _ = pkgs[0] + return True + except IndexError: + # IndexError is thrown when no matching package is found + return False + def get_new_command(command, settings): - try: - import CommandNotFound - c = CommandNotFound.CommandNotFound() - if 'not found' in command.stderr: - pkgs = c.getPackages(command.script.split(" ")[0]) - name,_ = pkgs[0] - return "sudo apt-get install %s" % name - except: - sys.stderr.write("Can't apt fuck\n") - return "" + c = CommandNotFound.CommandNotFound() + pkgs = c.getPackages(command.script.split(" ")[0]) + name, _ = pkgs[0] + return "sudo apt-get install {} && {}".format(name, command.script) From 58069f0a3eed25adf64efe8159b8f889dd17b1a1 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Thu, 23 Apr 2015 23:01:25 -0300 Subject: [PATCH 038/107] conf: initialize a settings file if it doesn't exist (fix #111) Signed-off-by: Pablo Santiago Blum de Aguiar --- tests/test_conf.py | 28 ++++++++++++++++++++++++++++ thefuck/conf.py | 22 ++++++++++++++++++++++ thefuck/main.py | 2 +- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/test_conf.py b/tests/test_conf.py index b8e6d6c..32e57b0 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -1,3 +1,4 @@ +import six from mock import patch, Mock from thefuck.types import Rule from thefuck import conf @@ -59,3 +60,30 @@ def test_settings_from_env_with_DEFAULT(): patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}): settings = conf.get_settings(Mock()) assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp'] + + +def test_initialize_settings_file_ignore_if_exists(): + settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock()) + user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) + conf.initialize_settings_file(user_dir_mock) + assert settings_path_mock.is_file.call_count == 1 + assert not settings_path_mock.open.called + + +def test_initialize_settings_file_create_if_exists_not(): + settings_file = six.StringIO() + settings_path_mock = Mock( + is_file=Mock(return_value=False), + open=Mock(return_value=Mock( + __exit__=lambda *args: None, __enter__=lambda *args: settings_file + )), + ) + user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) + conf.initialize_settings_file(user_dir_mock) + settings_file_contents = settings_file.getvalue() + assert settings_path_mock.is_file.call_count == 1 + assert settings_path_mock.open.call_count == 1 + assert conf.SETTINGS_HEADER in settings_file_contents + for setting in conf.DEFAULT_SETTINGS.items(): + assert '# {} = {}\n'.format(*setting) in settings_file_contents + settings_file.close() diff --git a/thefuck/conf.py b/thefuck/conf.py index 0c6f29a..d33b1e3 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -35,6 +35,19 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', 'THEFUCK_NO_COLORS': 'no_colors'} +SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file +# +# The rules are defined as in the example bellow: +# +# rules = ['cd_parent', 'git_push', 'python_command', 'sudo'] +# +# The default values are as follows. Uncomment and change to fit your needs. +# See https://github.com/nvbn/thefuck#settings for more information. +# + +""" + + def _settings_from_file(user_dir): """Loads settings from file.""" settings = load_source('settings', @@ -92,3 +105,12 @@ def get_settings(user_dir): conf['rules'] = types.RulesNamesList(conf['rules']) return types.Settings(conf) + + +def initialize_settings_file(user_dir): + settings_path = user_dir.joinpath('settings.py') + if not settings_path.is_file(): + with settings_path.open(mode='w') as settings_file: + settings_file.write(SETTINGS_HEADER) + for setting in DEFAULT_SETTINGS.items(): + settings_file.write(u'# {} = {}\n'.format(*setting)) diff --git a/thefuck/main.py b/thefuck/main.py index ea286eb..2ce1dd3 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -15,7 +15,7 @@ def setup_user_dir(): rules_dir = user_dir.joinpath('rules') if not rules_dir.is_dir(): rules_dir.mkdir(parents=True) - user_dir.joinpath('settings.py').touch() + conf.initialize_settings_file(user_dir) return user_dir From d12a8bcdd83dc53665b618814ed37a5e32ea05fa Mon Sep 17 00:00:00 2001 From: Nils Winkler Date: Fri, 24 Apr 2015 08:52:39 +0200 Subject: [PATCH 039/107] Added cd_mkdir rule This fixes #50 and #98. ```bash $ cd foo/bar/baz cd: foo: No such file or directory $ fuck mkdir -p foo/bar/baz && cd foo/bar/baz ``` Added matchers for both Bash and sh error messages. Depending on your default shell, the messages might be slightly different. --- README.md | 1 + tests/rules/test_cd_mkdir.py | 19 +++++++++++++++++++ thefuck/rules/cd_mkdir.py | 14 ++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 tests/rules/test_cd_mkdir.py create mode 100644 thefuck/rules/cd_mkdir.py diff --git a/README.md b/README.md index da503ab..d644b0c 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ using matched rule and run it. Rules enabled by default: * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `cd_parent` – changes `cd..` to `cd ..`; +* `cd_mkdir` – creates directories before cd'ing into them; * `cp_omitting_directory` – adds `-a` when you `cp` directory; * `fix_alt_space` – replaces Alt+Space with Space character; * `git_no_command` – fixes wrong git commands like `git brnch`; diff --git a/tests/rules/test_cd_mkdir.py b/tests/rules/test_cd_mkdir.py new file mode 100644 index 0000000..b3db292 --- /dev/null +++ b/tests/rules/test_cd_mkdir.py @@ -0,0 +1,19 @@ +from mock import Mock +from thefuck.rules.cd_mkdir import match, get_new_command + + +def test_match(): + assert match(Mock(script='cd foo', stderr='cd: foo: No such file or directory'), + None) + assert match(Mock(script='cd foo/bar/baz', stderr='cd: foo: No such file or directory'), + None) + assert match(Mock(script='cd foo/bar/baz', stderr='cd: can\'t cd to foo/bar/baz'), + None) + assert not match(Mock(script='cd foo', + stderr=''), None) + assert not match(Mock(script='', stderr=''), None) + + +def test_get_new_command(): + assert get_new_command(Mock(script='cd foo'), None) == 'mkdir -p foo && cd foo' + assert get_new_command(Mock(script='cd foo/bar/baz'), None) == 'mkdir -p foo/bar/baz && cd foo/bar/baz' diff --git a/thefuck/rules/cd_mkdir.py b/thefuck/rules/cd_mkdir.py new file mode 100644 index 0000000..7aa1d9d --- /dev/null +++ b/thefuck/rules/cd_mkdir.py @@ -0,0 +1,14 @@ +import re +from thefuck.utils import sudo_support + + +@sudo_support +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())) + + +@sudo_support +def get_new_command(command, settings): + return re.sub(r'^cd (.*)', 'mkdir -p \\1 && cd \\1', command.script) From 1e28671934ea92f9301673ea6f53ec3f5ac52060 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Fri, 24 Apr 2015 18:24:46 +0200 Subject: [PATCH 040/107] Revert "Fixing fish shell example in README.md" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d644b0c..fa45de8 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Or in `config.fish`: ```fish function fuck - eval (thefuck $history[2]) + eval (thefuck $history[1]) end ``` From 0009fb0588a19bb71450691286fa72eec393f9d5 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 25 Apr 2015 02:04:38 +0200 Subject: [PATCH 041/107] Bump to 1.30 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f247c2c..79e1c60 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.29' +VERSION = '1.30' setup(name='thefuck', From bb6b9a638c7b9dbae7fc95a3387d9bc0aea703e9 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Sat, 25 Apr 2015 02:11:07 +0200 Subject: [PATCH 042/107] #124 Add note that brew package isn't offical --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index fa45de8..df98303 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,7 @@ Install `The Fuck` with `pip`: sudo pip install thefuck ``` -If it fails try to use `easy_install`: - -```bash -sudo easy_install thefuck -``` - -Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)): +**Unoffical**: Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)): ```bash brew install thefuck From b7cb407637166f9a17c738eb6c2d58977ba5e7cf Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 25 Apr 2015 02:35:26 +0200 Subject: [PATCH 043/107] Add useful constructors for `Rule` and `Command` for tests --- tests/rules/test_brew_install.py | 20 +++++++-------- tests/rules/test_brew_unknown_command.py | 10 ++++---- tests/rules/test_cd_parent.py | 8 +++--- tests/rules/test_composer_not_command.py | 17 ++++++++----- tests/rules/test_fix_alt_space.py | 22 +++++++++-------- tests/rules/test_git_not_command.py | 16 ++++++------ tests/rules/test_git_push.py | 10 ++++---- tests/rules/test_mkdir_p.py | 15 +++++++----- tests/rules/test_pip_unknown_command.py | 11 +++++---- tests/rules/test_python_command.py | 9 ++++--- tests/rules/test_rm_dir.py | 12 ++++----- tests/rules/test_sl_ls.py | 8 +++--- tests/rules/test_ssh_known_host.py | 23 +++++++++--------- tests/rules/test_sudo.py | 12 ++++----- tests/test_conf.py | 8 +++--- tests/test_main.py | 31 ++++++++++++------------ tests/test_utils.py | 13 +++++----- tests/utils.py | 11 +++++++++ 18 files changed, 141 insertions(+), 115 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/rules/test_brew_install.py b/tests/rules/test_brew_install.py index bf06cd9..2ea58f1 100644 --- a/tests/rules/test_brew_install.py +++ b/tests/rules/test_brew_install.py @@ -1,7 +1,7 @@ import pytest -from thefuck.types import Command from thefuck.rules.brew_install import match, get_new_command from thefuck.rules.brew_install import brew_formulas +from tests.utils import Command @pytest.fixture @@ -29,21 +29,21 @@ def _is_not_okay_to_test(): reason='No need to run if there\'s no formula') def test_match(brew_no_available_formula, brew_already_installed, brew_install_no_argument): - assert match(Command('brew install elsticsearch', '', - brew_no_available_formula), None) - assert not match(Command('brew install git', '', - brew_already_installed), None) - assert not match(Command('brew install', '', brew_install_no_argument), + assert match(Command('brew install elsticsearch', + stderr=brew_no_available_formula), None) + assert not match(Command('brew install git', + stderr=brew_already_installed), None) + assert not match(Command('brew install', stderr=brew_install_no_argument), None) @pytest.mark.skipif(_is_not_okay_to_test(), reason='No need to run if there\'s no formula') def test_get_new_command(brew_no_available_formula): - assert get_new_command(Command('brew install elsticsearch', '', - brew_no_available_formula), None)\ + assert get_new_command(Command('brew install elsticsearch', + stderr=brew_no_available_formula), None)\ == 'brew install elasticsearch' - assert get_new_command(Command('brew install aa', '', - brew_no_available_formula), + assert get_new_command(Command('brew install aa', + stderr=brew_no_available_formula), None) != 'brew install aha' diff --git a/tests/rules/test_brew_unknown_command.py b/tests/rules/test_brew_unknown_command.py index 408e998..f7bdcdd 100644 --- a/tests/rules/test_brew_unknown_command.py +++ b/tests/rules/test_brew_unknown_command.py @@ -1,7 +1,7 @@ import pytest -from thefuck.types import Command from thefuck.rules.brew_unknown_command import match, get_new_command from thefuck.rules.brew_unknown_command import brew_commands +from tests.utils import Command @pytest.fixture @@ -15,14 +15,14 @@ def brew_unknown_cmd_instaa(): def test_match(brew_unknown_cmd): - assert match(Command('brew inst', '', brew_unknown_cmd), None) + assert match(Command('brew inst', stderr=brew_unknown_cmd), None) for command in brew_commands: - assert not match(Command('brew ' + command, '', ''), None) + assert not match(Command('brew ' + command), None) def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd_instaa): - assert get_new_command(Command('brew inst', '', brew_unknown_cmd), None)\ + assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd), None)\ == 'brew list' - assert get_new_command(Command('brew instaa', '', brew_unknown_cmd_instaa), + assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd_instaa), None) == 'brew install' diff --git a/tests/rules/test_cd_parent.py b/tests/rules/test_cd_parent.py index 60f8760..61d1ab1 100644 --- a/tests/rules/test_cd_parent.py +++ b/tests/rules/test_cd_parent.py @@ -1,12 +1,12 @@ -from thefuck.types import Command from thefuck.rules.cd_parent import match, get_new_command +from tests.utils import Command def test_match(): - assert match(Command('cd..', '', 'cd..: command not found'), None) - assert not match(Command('', '', ''), None) + assert match(Command('cd..', stderr='cd..: command not found'), None) + assert not match(Command(), None) def test_get_new_command(): assert get_new_command( - Command('cd..', '', ''), None) == 'cd ..' + Command('cd..'), None) == 'cd ..' diff --git a/tests/rules/test_composer_not_command.py b/tests/rules/test_composer_not_command.py index d1a2e02..15e630d 100644 --- a/tests/rules/test_composer_not_command.py +++ b/tests/rules/test_composer_not_command.py @@ -1,6 +1,6 @@ import pytest -from thefuck.types import Command from thefuck.rules.composer_not_command import match, get_new_command +from tests.utils import Command @pytest.fixture @@ -36,13 +36,18 @@ def composer_not_command_one_of_this(): def test_match(composer_not_command, composer_not_command_one_of_this): - assert match(Command('composer udpate', '', composer_not_command), None) - assert match(Command('composer pdate', '', composer_not_command_one_of_this), None) - assert not match(Command('ls update', '', composer_not_command), None) + assert match(Command('composer udpate', + stderr=composer_not_command), None) + assert match(Command('composer pdate', + stderr=composer_not_command_one_of_this), None) + assert not match(Command('ls update', stderr=composer_not_command), + None) def test_get_new_command(composer_not_command, composer_not_command_one_of_this): - assert get_new_command(Command('composer udpate', '', composer_not_command), None) \ + assert get_new_command(Command('composer udpate', + stderr=composer_not_command), None) \ == 'composer update' assert get_new_command( - Command('composer pdate', '', composer_not_command_one_of_this), None) == 'composer selfupdate' + Command('composer pdate', stderr=composer_not_command_one_of_this), + None) == 'composer selfupdate' diff --git a/tests/rules/test_fix_alt_space.py b/tests/rules/test_fix_alt_space.py index 2d66e08..2e8eea9 100644 --- a/tests/rules/test_fix_alt_space.py +++ b/tests/rules/test_fix_alt_space.py @@ -1,18 +1,20 @@ -# -*- encoding: utf-8 -*- - - -from thefuck.types import Command from thefuck.rules.fix_alt_space import match, get_new_command +from tests.utils import Command def test_match(): - """ The character before 'grep' is Alt+Space, which happens frequently on the Mac when typing - the pipe character (Alt+7), and holding the Alt key pressed for longer than necessary. """ - assert match(Command(u'ps -ef | grep foo', '', u'-bash:  grep: command not found'), None) - assert not match(Command('ps -ef | grep foo', '', ''), None) - assert not match(Command('', '', ''), None) + """The character before 'grep' is Alt+Space, which happens frequently + on the Mac when typing the pipe character (Alt+7), and holding the Alt + key pressed for longer than necessary. + + """ + assert match(Command(u'ps -ef | grep foo', + stderr=u'-bash:  grep: command not found'), None) + assert not match(Command('ps -ef | grep foo'), None) + assert not match(Command(), None) def test_get_new_command(): """ Replace the Alt+Space character by a simple space """ - assert get_new_command(Command(u'ps -ef | grep foo', '', ''), None) == 'ps -ef | grep foo' + assert get_new_command(Command(u'ps -ef | grep foo'), None)\ + == 'ps -ef | grep foo' diff --git a/tests/rules/test_git_not_command.py b/tests/rules/test_git_not_command.py index 91df959..d93d133 100644 --- a/tests/rules/test_git_not_command.py +++ b/tests/rules/test_git_not_command.py @@ -1,6 +1,6 @@ import pytest -from thefuck.types import Command from thefuck.rules.git_not_command import match, get_new_command +from tests.utils import Command @pytest.fixture @@ -31,14 +31,14 @@ def git_command(): def test_match(git_not_command, git_command, git_not_command_one_of_this): - assert match(Command('git brnch', '', git_not_command), None) - assert match(Command('git st', '', git_not_command_one_of_this), None) - assert not match(Command('ls brnch', '', git_not_command), None) - assert not match(Command('git branch', '', git_command), None) + assert match(Command('git brnch', stderr=git_not_command), None) + assert match(Command('git st', stderr=git_not_command_one_of_this), None) + assert not match(Command('ls brnch', stderr=git_not_command), None) + assert not match(Command('git branch', stderr=git_command), None) def test_get_new_command(git_not_command, git_not_command_one_of_this): - assert get_new_command(Command('git brnch', '', git_not_command), None)\ + assert get_new_command(Command('git brnch', stderr=git_not_command), None)\ == 'git branch' - assert get_new_command( - Command('git st', '', git_not_command_one_of_this), None) == 'git status' + assert get_new_command(Command('git st', stderr=git_not_command_one_of_this), + None) == 'git status' diff --git a/tests/rules/test_git_push.py b/tests/rules/test_git_push.py index 57f6359..7a2b512 100644 --- a/tests/rules/test_git_push.py +++ b/tests/rules/test_git_push.py @@ -1,6 +1,6 @@ import pytest -from thefuck.types import Command from thefuck.rules.git_push import match, get_new_command +from tests.utils import Command @pytest.fixture @@ -14,11 +14,11 @@ To push the current branch and set the remote as upstream, use def test_match(stderr): - assert match(Command('git push master', '', stderr), None) - assert not match(Command('git push master', '', ''), None) - assert not match(Command('ls', '', stderr), None) + assert match(Command('git push master', stderr=stderr), None) + assert not match(Command('git push master'), None) + assert not match(Command('ls', stderr=stderr), None) def test_get_new_command(stderr): - assert get_new_command(Command('', '', stderr), None)\ + assert get_new_command(Command(stderr=stderr), None)\ == "git push --set-upstream origin master" diff --git a/tests/rules/test_mkdir_p.py b/tests/rules/test_mkdir_p.py index dae196f..60b6b65 100644 --- a/tests/rules/test_mkdir_p.py +++ b/tests/rules/test_mkdir_p.py @@ -1,13 +1,16 @@ -from thefuck.types import Command from thefuck.rules.mkdir_p import match, get_new_command +from tests.utils import 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) + assert match(Command('mkdir foo/bar/baz', + stderr='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', stderr='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' + assert get_new_command(Command('mkdir foo/bar/baz'), None)\ + == 'mkdir -p foo/bar/baz' diff --git a/tests/rules/test_pip_unknown_command.py b/tests/rules/test_pip_unknown_command.py index 61b5c99..58ba2bb 100644 --- a/tests/rules/test_pip_unknown_command.py +++ b/tests/rules/test_pip_unknown_command.py @@ -1,6 +1,6 @@ import pytest -from thefuck.types import Command from thefuck.rules.pip_unknown_command import match, get_new_command +from tests.utils import Command @pytest.fixture @@ -14,11 +14,12 @@ def pip_unknown_cmd_without_recommend(): def test_match(pip_unknown_cmd, pip_unknown_cmd_without_recommend): - assert match(Command('pip instatl', '', pip_unknown_cmd), None) - assert not match(Command('pip i', '', pip_unknown_cmd_without_recommend), + assert match(Command('pip instatl', stderr=pip_unknown_cmd), None) + assert not match(Command('pip i', + stderr=pip_unknown_cmd_without_recommend), None) def test_get_new_command(pip_unknown_cmd): - assert get_new_command(Command('pip instatl', '', pip_unknown_cmd), None)\ - == 'pip install' + assert get_new_command(Command('pip instatl', stderr=pip_unknown_cmd), + None) == 'pip install' diff --git a/tests/rules/test_python_command.py b/tests/rules/test_python_command.py index 56d9b3f..54be39a 100644 --- a/tests/rules/test_python_command.py +++ b/tests/rules/test_python_command.py @@ -1,11 +1,12 @@ -from thefuck.types import Command from thefuck.rules.python_command import match, get_new_command +from tests.utils import Command def test_match(): - assert match(Command('temp.py', '', 'Permission denied'), None) - assert not match(Command('', '', ''), None) + assert match(Command('temp.py', stderr='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' + 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 index 1608047..573bbdc 100644 --- a/tests/rules/test_rm_dir.py +++ b/tests/rules/test_rm_dir.py @@ -1,13 +1,13 @@ -from thefuck.types import Command from thefuck.rules.rm_dir import match, get_new_command +from tests.utils import Command def test_match(): - assert match(Command('rm foo', '', 'rm: foo: is a directory'), None) - 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) + assert match(Command('rm foo', stderr='rm: foo: is a directory'), None) + assert match(Command('rm foo', stderr='rm: foo: Is a directory'), None) + assert not match(Command('rm foo'), None) + assert not match(Command('rm foo', stderr='foo bar baz'), None) + assert not match(Command(), None) def test_get_new_command(): diff --git a/tests/rules/test_sl_ls.py b/tests/rules/test_sl_ls.py index fd83b04..4ed7499 100644 --- a/tests/rules/test_sl_ls.py +++ b/tests/rules/test_sl_ls.py @@ -1,12 +1,12 @@ -from thefuck.types import Command from thefuck.rules.sl_ls import match, get_new_command +from tests.utils import Command def test_match(): - assert match(Command('sl', '', ''), None) - assert not match(Command('ls', '', ''), None) + assert match(Command('sl'), None) + assert not match(Command('ls'), None) def test_get_new_command(): - assert get_new_command(Command('sl', '', ''), None) == 'ls' + assert get_new_command(Command('sl'), None) == 'ls' diff --git a/tests/rules/test_ssh_known_host.py b/tests/rules/test_ssh_known_host.py index 252cac1..8875bd4 100644 --- a/tests/rules/test_ssh_known_host.py +++ b/tests/rules/test_ssh_known_host.py @@ -1,8 +1,9 @@ import os import pytest from mock import Mock -from thefuck.types import Command -from thefuck.rules.ssh_known_hosts import match, get_new_command, remove_offending_keys +from thefuck.rules.ssh_known_hosts import match, get_new_command,\ + remove_offending_keys +from tests.utils import Command @pytest.fixture @@ -43,18 +44,18 @@ Host key verification failed.""".format(path, '98.765.432.321') 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) + assert match(Command('ssh', stderr=errormsg), None) + assert match(Command('ssh', stderr=errormsg), None) + assert match(Command('scp something something', stderr=errormsg), None) + assert match(Command('scp something something', stderr=errormsg), None) + assert not match(Command(stderr=errormsg), None) + assert not match(Command('notssh', stderr=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) + command = Command('ssh user@host', stderr=errormsg) remove_offending_keys(command, None) expected = ['123.234.567.890 asdjkasjdakjsd\n', '111.222.333.444 qwepoiwqepoiss\n'] assert known_hosts(path) == expected @@ -65,5 +66,5 @@ def test_get_new_command(ssh_error, monkeypatch): 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 get_new_command(Command('ssh user@host', stderr=errormsg), None) == 'ssh user@host' assert method.call_count diff --git a/tests/rules/test_sudo.py b/tests/rules/test_sudo.py index 4f3b3f5..47542c8 100644 --- a/tests/rules/test_sudo.py +++ b/tests/rules/test_sudo.py @@ -1,13 +1,13 @@ -from thefuck.types import Command from thefuck.rules.sudo import match, get_new_command +from tests.utils import Command def test_match(): - assert match(Command('', '', 'Permission denied'), None) - assert match(Command('', '', 'permission denied'), None) - assert match(Command('', '', "npm ERR! Error: EACCES, unlink"), None) - assert not match(Command('', '', ''), None) + assert match(Command(stderr='Permission denied'), None) + assert match(Command(stderr='permission denied'), None) + assert match(Command(stderr="npm ERR! Error: EACCES, unlink"), None) + assert not match(Command(), None) def test_get_new_command(): - assert get_new_command(Command('ls', '', ''), None) == 'sudo ls' + assert get_new_command(Command('ls'), None) == 'sudo ls' diff --git a/tests/test_conf.py b/tests/test_conf.py index 32e57b0..608a49e 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -1,13 +1,13 @@ import six from mock import patch, Mock -from thefuck.types import Rule from thefuck import conf +from tests.utils import Rule def test_default(): - assert Rule('test', None, None, True) in conf.DEFAULT_RULES - assert Rule('test', None, None, False) not in conf.DEFAULT_RULES - assert Rule('test', None, None, False) in (conf.DEFAULT_RULES + ['test']) + assert Rule('test', enabled_by_default=True) in conf.DEFAULT_RULES + assert Rule('test', enabled_by_default=False) not in conf.DEFAULT_RULES + assert Rule('test', enabled_by_default=False) in (conf.DEFAULT_RULES + ['test']) def test_settings_defaults(): diff --git a/tests/test_main.py b/tests/test_main.py index 078f1a8..cc5bf79 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,7 @@ from subprocess import PIPE from pathlib import PosixPath, Path from mock import patch, Mock from thefuck import main, conf, types +from tests.utils import Rule, Command def test_load_rule(): @@ -13,7 +14,7 @@ def test_load_rule(): get_new_command=get_new_command, enabled_by_default=True)) as load_source: assert main.load_rule(Path('/rules/bash.py')) \ - == types.Rule('bash', match, get_new_command, True) + == Rule('bash', match, get_new_command) load_source.assert_called_once_with('bash', '/rules/bash.py') @@ -26,15 +27,15 @@ def test_get_rules(): assert list(main.get_rules( Path('~'), Mock(rules=conf.DEFAULT_RULES))) \ - == [types.Rule('bash', 'bash', 'bash', True), - types.Rule('lisp', 'lisp', 'lisp', True), - types.Rule('bash', 'bash', 'bash', True), - types.Rule('lisp', 'lisp', 'lisp', True)] + == [Rule('bash', 'bash', 'bash'), + Rule('lisp', 'lisp', 'lisp'), + Rule('bash', 'bash', 'bash'), + Rule('lisp', 'lisp', 'lisp')] assert list(main.get_rules( Path('~'), Mock(rules=types.RulesNamesList(['bash'])))) \ - == [types.Rule('bash', 'bash', 'bash', True), - types.Rule('bash', 'bash', 'bash', True)] + == [Rule('bash', 'bash', 'bash'), + Rule('bash', 'bash', 'bash')] def test_get_command(): @@ -47,7 +48,7 @@ def test_get_command(): Popen.return_value.stderr.read.return_value = b'stderr' assert main.get_command(Mock(), ['thefuck', 'apt-get', 'search', 'vim']) \ - == types.Command('apt-get search vim', 'stdout', 'stderr') + == Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', shell=True, stdout=PIPE, @@ -57,12 +58,12 @@ def test_get_command(): def test_get_matched_rule(capsys): - rules = [types.Rule('', lambda x, _: x.script == 'cd ..', None, True), - types.Rule('', lambda *_: False, None, True), - types.Rule('rule', Mock(side_effect=OSError('Denied')), None, True)] - assert main.get_matched_rule(types.Command('ls', '', ''), + rules = [Rule('', lambda x, _: x.script == 'cd ..'), + Rule('', lambda *_: False), + Rule('rule', Mock(side_effect=OSError('Denied')))] + assert main.get_matched_rule(Command('ls'), rules, Mock(no_colors=True)) is None - assert main.get_matched_rule(types.Command('cd ..', '', ''), + assert main.get_matched_rule(Command('cd ..'), rules, Mock(no_colors=True)) == rules[0] assert capsys.readouterr()[1].split('\n')[0] \ == '[WARN] Rule rule:' @@ -70,11 +71,11 @@ def test_get_matched_rule(capsys): def test_run_rule(capsys): with patch('thefuck.main.confirm', return_value=True): - main.run_rule(types.Rule('', None, lambda *_: 'new-command', True), + main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), None, None) assert capsys.readouterr() == ('new-command\n', '') with patch('thefuck.main.confirm', return_value=False): - main.run_rule(types.Rule('', None, lambda *_: 'new-command', True), + main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), None, None) assert capsys.readouterr() == ('', '') diff --git a/tests/test_utils.py b/tests/test_utils.py index 995614d..08766a9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,7 @@ from mock import Mock from thefuck.utils import sudo_support, wrap_settings -from thefuck.types import Command, Settings +from thefuck.types import Settings +from tests.utils import Command def test_wrap_settings(): @@ -13,13 +14,13 @@ def test_wrap_settings(): 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) + assert sudo_support(fn)(Command('sudo ls'), None) + fn.assert_called_once_with(Command('ls'), None) fn.return_value = False - assert not sudo_support(fn)(Command('sudo ls', 'out', 'err'), None) + assert not sudo_support(fn)(Command('sudo ls'), None) fn.return_value = 'pwd' - assert sudo_support(fn)(Command('sudo ls', 'out', 'err'), None) == 'sudo pwd' + assert sudo_support(fn)(Command('sudo ls'), None) == 'sudo pwd' - assert sudo_support(fn)(Command('ls', 'out', 'err'), None) == 'pwd' + assert sudo_support(fn)(Command('ls'), None) == 'pwd' diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..5191d2c --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,11 @@ +from thefuck import types + + +def Command(script='', stdout='', stderr=''): + return types.Command(script, stdout, stderr) + + +def Rule(name='', match=lambda *_: True, + get_new_command=lambda *_: '', + enabled_by_default=True): + return types.Rule(name, match, get_new_command, enabled_by_default) From 698451f65dd35c3dc0a150118f421548b71a0d51 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 25 Apr 2015 02:54:39 +0200 Subject: [PATCH 044/107] Use parametrized tests where it possible --- tests/rules/test_cd_mkdir.py | 34 +++++++++++++++++------------ tests/rules/test_mkdir_p.py | 12 ++++++++--- tests/rules/test_rm_dir.py | 19 +++++++++++------ tests/rules/test_rm_root.py | 25 ++++++++++++---------- tests/rules/test_sudo.py | 13 +++++++---- tests/rules/test_switch_lang.py | 38 +++++++++++++++++---------------- 6 files changed, 85 insertions(+), 56 deletions(-) diff --git a/tests/rules/test_cd_mkdir.py b/tests/rules/test_cd_mkdir.py index b3db292..ae5449f 100644 --- a/tests/rules/test_cd_mkdir.py +++ b/tests/rules/test_cd_mkdir.py @@ -1,19 +1,25 @@ -from mock import Mock +import pytest from thefuck.rules.cd_mkdir import match, get_new_command +from tests.utils import Command -def test_match(): - assert match(Mock(script='cd foo', stderr='cd: foo: No such file or directory'), - None) - assert match(Mock(script='cd foo/bar/baz', stderr='cd: foo: No such file or directory'), - None) - assert match(Mock(script='cd foo/bar/baz', stderr='cd: can\'t cd to foo/bar/baz'), - None) - assert not match(Mock(script='cd foo', - stderr=''), None) - assert not match(Mock(script='', stderr=''), None) +@pytest.mark.parametrize('command', [ + Command(script='cd foo', stderr='cd: foo: No such file or directory'), + Command(script='cd foo/bar/baz', + stderr='cd: foo: No such file or directory'), + Command(script='cd foo/bar/baz', stderr='cd: can\'t cd to foo/bar/baz')]) +def test_match(command): + assert match(command, None) -def test_get_new_command(): - assert get_new_command(Mock(script='cd foo'), None) == 'mkdir -p foo && cd foo' - assert get_new_command(Mock(script='cd foo/bar/baz'), None) == 'mkdir -p foo/bar/baz && cd foo/bar/baz' +@pytest.mark.parametrize('command', [ + Command(script='cd foo', stderr=''), Command()]) +def test_not_match(command): + assert not match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('cd foo'), 'mkdir -p foo && cd foo'), + (Command('cd foo/bar/baz'), 'mkdir -p foo/bar/baz && cd foo/bar/baz')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command diff --git a/tests/rules/test_mkdir_p.py b/tests/rules/test_mkdir_p.py index 60b6b65..d1b91a7 100644 --- a/tests/rules/test_mkdir_p.py +++ b/tests/rules/test_mkdir_p.py @@ -1,3 +1,4 @@ +import pytest from thefuck.rules.mkdir_p import match, get_new_command from tests.utils import Command @@ -6,9 +7,14 @@ def test_match(): assert match(Command('mkdir foo/bar/baz', stderr='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', stderr='foo bar baz'), None) - assert not match(Command(), None) + + +@pytest.mark.parametrize('command', [ + Command('mkdir foo/bar/baz'), + Command('mkdir foo/bar/baz', stderr='foo bar baz'), + Command()]) +def test_not_match(command): + assert not match(command, None) def test_get_new_command(): diff --git a/tests/rules/test_rm_dir.py b/tests/rules/test_rm_dir.py index 573bbdc..c334d2f 100644 --- a/tests/rules/test_rm_dir.py +++ b/tests/rules/test_rm_dir.py @@ -1,13 +1,20 @@ +import pytest from thefuck.rules.rm_dir import match, get_new_command from tests.utils import Command -def test_match(): - assert match(Command('rm foo', stderr='rm: foo: is a directory'), None) - assert match(Command('rm foo', stderr='rm: foo: Is a directory'), None) - assert not match(Command('rm foo'), None) - assert not match(Command('rm foo', stderr='foo bar baz'), None) - assert not match(Command(), None) +@pytest.mark.parametrize('command', [ + Command('rm foo', stderr='rm: foo: is a directory'), + Command('rm foo', stderr='rm: foo: Is a directory')]) +def test_match(command): + assert match(command, None) + assert match(command, None) + + +@pytest.mark.parametrize('command', [ + Command('rm foo'), Command('rm foo'), Command()]) +def test_not_match(command): + assert not match(command, None) def test_get_new_command(): diff --git a/tests/rules/test_rm_root.py b/tests/rules/test_rm_root.py index 003cb80..f56595f 100644 --- a/tests/rules/test_rm_root.py +++ b/tests/rules/test_rm_root.py @@ -1,18 +1,21 @@ -from mock import Mock +import pytest from thefuck.rules.rm_root import match, get_new_command +from tests.utils import 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) + assert match(Command(script='rm -rf /', + stderr='add --no-preserve-root'), None) + + +@pytest.mark.parametrize('command', [ + Command(script='ls', stderr='add --no-preserve-root'), + Command(script='rm --no-preserve-root /', stderr='add --no-preserve-root'), + Command(script='rm -rf /', stderr='')]) +def test_not_match(command): + assert not match(command, None) def test_get_new_command(): - assert get_new_command(Mock(script='rm -rf /'), None) \ - == 'rm -rf / --no-preserve-root' + assert get_new_command(Command(script='rm -rf /'), None) \ + == 'rm -rf / --no-preserve-root' diff --git a/tests/rules/test_sudo.py b/tests/rules/test_sudo.py index 47542c8..6bac882 100644 --- a/tests/rules/test_sudo.py +++ b/tests/rules/test_sudo.py @@ -1,11 +1,16 @@ +import pytest from thefuck.rules.sudo import match, get_new_command from tests.utils import Command -def test_match(): - assert match(Command(stderr='Permission denied'), None) - assert match(Command(stderr='permission denied'), None) - assert match(Command(stderr="npm ERR! Error: EACCES, unlink"), None) +@pytest.mark.parametrize('stderr', ['Permission denied', + 'permission denied', + "npm ERR! Error: EACCES, unlink"]) +def test_match(stderr): + assert match(Command(stderr=stderr), None) + + +def test_not_match(): assert not match(Command(), None) diff --git a/tests/rules/test_switch_lang.py b/tests/rules/test_switch_lang.py index 991f398..12163d4 100644 --- a/tests/rules/test_switch_lang.py +++ b/tests/rules/test_switch_lang.py @@ -1,25 +1,27 @@ # -*- encoding: utf-8 -*- -from mock import Mock +import pytest from thefuck.rules import switch_lang +from tests.utils import Command -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) +@pytest.mark.parametrize('command', [ + Command(stderr='command not found: фзе-пуе', script=u'фзе-пуе'), + Command(stderr='command not found: λσ', script=u'λσ')]) +def test_match(command): + assert switch_lang.match(command, 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' +@pytest.mark.parametrize('command', [ + Command(stderr='command not found: pat-get', script=u'pat-get'), + Command(stderr='command not found: ls', script=u'ls'), + Command(stderr='some info', script=u'фзе-пуе')]) +def test_not_match(command): + assert not switch_lang.match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command(u'фзе-пуе штыефдд мшь'), 'apt-get install vim'), + (Command(u'λσ -λα'), 'ls -la')]) +def test_get_new_command(command, new_command): + assert switch_lang.get_new_command(command, None) == new_command From 2f04a953baa2e1c3f4b37bc46e804d92185ceb50 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 25 Apr 2015 03:13:57 +0200 Subject: [PATCH 045/107] Fix tests with python 2 --- tests/__init__.py | 0 tests/rules/test_fix_alt_space.py | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/rules/test_fix_alt_space.py b/tests/rules/test_fix_alt_space.py index 2e8eea9..c27d3d2 100644 --- a/tests/rules/test_fix_alt_space.py +++ b/tests/rules/test_fix_alt_space.py @@ -1,3 +1,5 @@ +# -*- encoding: utf-8 -*- + from thefuck.rules.fix_alt_space import match, get_new_command from tests.utils import Command From 419878f526c35a9caf5f4665693d9170b638081e Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 25 Apr 2015 03:42:36 +0200 Subject: [PATCH 046/107] #118 Make `ls_lah` disabled by default --- README.md | 2 +- tests/rules/test_ls_lah.py | 10 +++++----- thefuck/rules/ls_lah.py | 6 +++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 85cbdbb..9f55b53 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,6 @@ using matched rule and run it. Rules enabled by default: * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `has_exists_script` – prepends `./` when script/binary exists; -* `ls_lah` – adds -lah to ls; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; @@ -170,6 +169,7 @@ using matched rule and run it. Rules enabled by default: Bundled, but not enabled by default: +* `ls_lah` – adds -lah to ls; * `rm_root` – adds `--no-preserve-root` to `rm -rf /` command. ## Creating your own rules diff --git a/tests/rules/test_ls_lah.py b/tests/rules/test_ls_lah.py index f7eb3ed..c453f6e 100644 --- a/tests/rules/test_ls_lah.py +++ b/tests/rules/test_ls_lah.py @@ -3,11 +3,11 @@ from thefuck.rules.ls_lah import match, get_new_command def test_match(): - assert match(Mock(script='ls file.py'), None) - assert match(Mock(script='ls /opt'), None) - assert not match(Mock(script='ls -lah /opt'), None) + assert match(Mock(script='ls file.py'), None) + assert match(Mock(script='ls /opt'), None) + assert not match(Mock(script='ls -lah /opt'), None) def test_get_new_command(): - assert get_new_command( Mock(script='ls file.py'), None) == 'ls -lah file.py' - assert get_new_command( Mock(script='ls'), None) == 'ls -lah' + assert get_new_command(Mock(script='ls file.py'), None) == 'ls -lah file.py' + assert get_new_command(Mock(script='ls'), None) == 'ls -lah' diff --git a/thefuck/rules/ls_lah.py b/thefuck/rules/ls_lah.py index 1de0283..50fe9f5 100644 --- a/thefuck/rules/ls_lah.py +++ b/thefuck/rules/ls_lah.py @@ -1,5 +1,9 @@ +enabled_by_default = False + + def match(command, settings): - return ('ls' in command.script and not ('ls -' in command.script)) + return 'ls' in command.script and not ('ls -' in command.script) + def get_new_command(command, settings): command = command.script.split(' ') From 077de17d6c39d0bdc51dc7d733e27a71723f8cfd Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Sun, 26 Apr 2015 20:35:04 -0300 Subject: [PATCH 047/107] Fix typo: unoffical -> unofficial --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f55b53..1c01c8b 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Install `The Fuck` with `pip`: sudo pip install thefuck ``` -**Unoffical**: Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)): +**Unofficial**: Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)): ```bash brew install thefuck From 49ed98c8a4bb78d67bd3784561e83abbbef64bb3 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 27 Apr 2015 07:46:15 +0200 Subject: [PATCH 048/107] Move os specific installation to wiki --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 1c01c8b..1b3fe9a 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,7 @@ Install `The Fuck` with `pip`: sudo pip install thefuck ``` -**Unofficial**: Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)): - -```bash -brew install thefuck -``` +[Or using an OS package manager (OS X, Ubuntu, Arch).](/wiki/Installation) And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX): From cbf6507e1e45c0dff07cd90a5bb331afd24ac052 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 27 Apr 2015 07:54:40 +0200 Subject: [PATCH 049/107] Move aliases for specific shells to wiki --- README.md | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/README.md b/README.md index 1b3fe9a..a100c2e 100644 --- a/README.md +++ b/README.md @@ -110,25 +110,7 @@ alias fuck='eval $(thefuck $(fc -ln -1))' alias FUCK='fuck' ``` -Or in `config.fish`: - -```fish -function fuck - eval (thefuck $history[1]) -end -``` - -Or in your Powershell `$PROFILE` on Windows: - -```powershell -function fuck { - $fuck = $(thefuck (get-history -count 1).commandline) - if($fuck.startswith("echo")) { - $fuck.substring(5) - } - else { iex "$fuck" } -} -``` +[On in your shell config (Bash, Zsh, Fish, Powershell).](/wiki/Shell-aliases) Changes will be available only in a new shell session. From 4cf631fa4794ea03da192ff4c9e100a9daf2183f Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 27 Apr 2015 07:56:17 +0200 Subject: [PATCH 050/107] Fix links to wiki --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a100c2e..5cf2097 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Install `The Fuck` with `pip`: sudo pip install thefuck ``` -[Or using an OS package manager (OS X, Ubuntu, Arch).](/wiki/Installation) +[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation) And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX): @@ -110,7 +110,7 @@ alias fuck='eval $(thefuck $(fc -ln -1))' alias FUCK='fuck' ``` -[On in your shell config (Bash, Zsh, Fish, Powershell).](/wiki/Shell-aliases) +[On in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases) Changes will be available only in a new shell session. From a2ac15da56e6d08c734f91f97e13e09e59f8c5a2 Mon Sep 17 00:00:00 2001 From: Trace Date: Mon, 27 Apr 2015 17:45:59 -0500 Subject: [PATCH 051/107] Added dpkg rule For example, when using ```dpkg -i some-pkg.deb``` --- 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 2588ee5..26d87ad 100644 --- a/thefuck/rules/sudo.py +++ b/thefuck/rules/sudo.py @@ -7,7 +7,8 @@ patterns = ['permission denied', 'root privilege', 'This command has to be run under the root user.', 'This operation requires root.', - 'You need to be root to perform this command.'] + 'You need to be root to perform this command.', + 'requested operation requires superuser privilege'] def match(command, settings): From bc2c87e8fe2c1ac19dc9e58e8a9003b17aa784bc Mon Sep 17 00:00:00 2001 From: Namwoo Kim Date: Tue, 28 Apr 2015 15:09:38 +0900 Subject: [PATCH 052/107] Add a support for pip install with sudo - Fixes #136 --- tests/rules/test_pip_install_sudo.py | 58 ++++++++++++++++++++++++++++ thefuck/rules/pip_install_sudo.py | 13 +++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/rules/test_pip_install_sudo.py create mode 100644 thefuck/rules/pip_install_sudo.py diff --git a/tests/rules/test_pip_install_sudo.py b/tests/rules/test_pip_install_sudo.py new file mode 100644 index 0000000..db4220d --- /dev/null +++ b/tests/rules/test_pip_install_sudo.py @@ -0,0 +1,58 @@ +import pytest +from thefuck.rules.pip_install_sudo import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stdout_success(): + return ''' + Collecting thefuck + Downloading thefuck-1.30.tar.gz + Requirement already satisfied (use --upgrade to upgrade): pathlib in /usr/local/lib/python2.7/site-packages/pathlib-1.0.1-py2.7.egg (from thefuck) + Requirement already satisfied (use --upgrade to upgrade): psutil in /usr/local/lib/python2.7/site-packages/psutil-2.2.1-py2.7-macosx-10.10-x86_64.egg (from thefuck) + Requirement already satisfied (use --upgrade to upgrade): colorama in /usr/local/lib/python2.7/site-packages/colorama-0.3.3-py2.7.egg (from thefuck) + Requirement already satisfied (use --upgrade to upgrade): six in /usr/local/lib/python2.7/site-packages (from thefuck) + Installing collected packages: thefuck + Running setup.py install for thefuck + Successfully installed thefuck-1.30 + ''' + + +@pytest.fixture +def stdout(): + return ''' + Collecting ipaddr + Using cached ipaddr-2.1.11.tar.gz + Installing collected packages: ipaddr + Running setup.py install for ipaddr + Complete output from command /usr/bin/python -c "import setuptools, tokenize;__file__='/tmp/pip-build-usOyBh/ipaddr/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-ghPfAW-record/install-record.txt --single-version-externally-managed --compile: + running install + running build + running build_py + creating build + creating build/lib.linux-x86_64-2.7 + copying ipaddr.py -> build/lib.linux-x86_64-2.7 + running install_lib + copying build/lib.linux-x86_64-2.7/ipaddr.py -> /usr/local/lib/python2.7/dist-packages + error: [Errno 13] Permission denied: '/usr/local/lib/python2.7/dist-packages/ipaddr.py' + ''' + + +@pytest.fixture +def stderr(): + return ''' + Command "/usr/bin/python -c "import setuptools, tokenize;__file__='/tmp/pip-build-usOyBh/ipaddr/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-ghPfAW-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-usOyBh/ipaddr + ''' + + +def test_match(stdout, stdout_success, stderr): + assert match(Command('pip install ipaddr', stdout=stdout, stderr=stderr), + None) + assert not match(Command('pip install thefuck', stdout=stdout_success), + None) + + +def test_get_new_command(stdout, stdout_success, stderr): + assert get_new_command(Command('pip install ipaddr', stdout=stdout, + stderr=stderr), None)\ + == 'sudo pip install ipaddr' diff --git a/thefuck/rules/pip_install_sudo.py b/thefuck/rules/pip_install_sudo.py new file mode 100644 index 0000000..0d1e37d --- /dev/null +++ b/thefuck/rules/pip_install_sudo.py @@ -0,0 +1,13 @@ +import re +from thefuck.utils import sudo_support + + +def match(command, settings): + return (('pip' in command.script and 'install' in command.script) and + 'failed with error code 1' in command.stderr and + ('Errno 13' in command.stdout or + 'Permission denied' in command.stdout)) + + +def get_new_command(command, settings): + return u'sudo {}'.format(command.script) From d3295e6a4e18b6e0be45f08fa95d413f4d1c871c Mon Sep 17 00:00:00 2001 From: Trace Date: Tue, 28 Apr 2015 11:59:15 -0500 Subject: [PATCH 053/107] Hoping this fixes the issue of it not working. Still new to this project. My problem of it not working (I admit to being at fault) was that I had not added the same rule to the test case. --- tests/rules/test_sudo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rules/test_sudo.py b/tests/rules/test_sudo.py index 6bac882..9d77392 100644 --- a/tests/rules/test_sudo.py +++ b/tests/rules/test_sudo.py @@ -5,7 +5,8 @@ from tests.utils import Command @pytest.mark.parametrize('stderr', ['Permission denied', 'permission denied', - "npm ERR! Error: EACCES, unlink"]) + "npm ERR! Error: EACCES, unlink", + 'requested operation requires superuser privilege']) def test_match(stderr): assert match(Command(stderr=stderr), None) From 7652884df6396c64568d88ebe28f968fb12985df Mon Sep 17 00:00:00 2001 From: SanketDG Date: Wed, 29 Apr 2015 00:14:23 +0530 Subject: [PATCH 054/107] change requirements --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cf2097..2e7be32 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ Reading package lists... Done ## Requirements +- python (2.7+ or 3.3+) - pip -- python - python-dev ## Installation From c463fea8a0935c604f521abbb7d66e2de2ffe18a Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Wed, 29 Apr 2015 04:47:15 +0200 Subject: [PATCH 055/107] #145 add config examples --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 5cf2097..8f7b254 100644 --- a/README.md +++ b/README.md @@ -185,12 +185,30 @@ The Fuck has a few settings parameters, they can be changed in `~/.thefuck/setti * `wait_command` – max amount of time in seconds for getting previous command output; * `no_colors` – disable colored output. +Example of `settings.py`: + +```python +rules = ['sudo', 'no_command'] +require_confirmation = True +wait_command = 10 +no_colors = False +``` + Or via environment variables: * `THEFUCK_RULES` – list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`; * `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`; * `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output; * `THEFUCK_NO_COLORS` – disable colored output, `true/false`. + +For example: + +```bash +export THEFUCK_RULES='sudo:no_command' +export THEFUCK_REQUIRE_CONFIRMATION='true' +export THEFUCK_WAIT_COMMAND=10 +export THEFUCK_NO_COLORS='false' +``` ## Developing From 99d9c9aff7fde129279a6a4ee58cb1e6df109e34 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Apr 2015 05:01:30 +0200 Subject: [PATCH 056/107] #132 Merge `pip_install_sudo` rule with `sudo` rule --- tests/rules/test_pip_install_sudo.py | 58 ---------------------------- tests/rules/test_sudo.py | 14 ++++--- thefuck/rules/pip_install_sudo.py | 13 ------- thefuck/rules/sudo.py | 3 +- 4 files changed, 10 insertions(+), 78 deletions(-) delete mode 100644 tests/rules/test_pip_install_sudo.py delete mode 100644 thefuck/rules/pip_install_sudo.py diff --git a/tests/rules/test_pip_install_sudo.py b/tests/rules/test_pip_install_sudo.py deleted file mode 100644 index db4220d..0000000 --- a/tests/rules/test_pip_install_sudo.py +++ /dev/null @@ -1,58 +0,0 @@ -import pytest -from thefuck.rules.pip_install_sudo import match, get_new_command -from tests.utils import Command - - -@pytest.fixture -def stdout_success(): - return ''' - Collecting thefuck - Downloading thefuck-1.30.tar.gz - Requirement already satisfied (use --upgrade to upgrade): pathlib in /usr/local/lib/python2.7/site-packages/pathlib-1.0.1-py2.7.egg (from thefuck) - Requirement already satisfied (use --upgrade to upgrade): psutil in /usr/local/lib/python2.7/site-packages/psutil-2.2.1-py2.7-macosx-10.10-x86_64.egg (from thefuck) - Requirement already satisfied (use --upgrade to upgrade): colorama in /usr/local/lib/python2.7/site-packages/colorama-0.3.3-py2.7.egg (from thefuck) - Requirement already satisfied (use --upgrade to upgrade): six in /usr/local/lib/python2.7/site-packages (from thefuck) - Installing collected packages: thefuck - Running setup.py install for thefuck - Successfully installed thefuck-1.30 - ''' - - -@pytest.fixture -def stdout(): - return ''' - Collecting ipaddr - Using cached ipaddr-2.1.11.tar.gz - Installing collected packages: ipaddr - Running setup.py install for ipaddr - Complete output from command /usr/bin/python -c "import setuptools, tokenize;__file__='/tmp/pip-build-usOyBh/ipaddr/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-ghPfAW-record/install-record.txt --single-version-externally-managed --compile: - running install - running build - running build_py - creating build - creating build/lib.linux-x86_64-2.7 - copying ipaddr.py -> build/lib.linux-x86_64-2.7 - running install_lib - copying build/lib.linux-x86_64-2.7/ipaddr.py -> /usr/local/lib/python2.7/dist-packages - error: [Errno 13] Permission denied: '/usr/local/lib/python2.7/dist-packages/ipaddr.py' - ''' - - -@pytest.fixture -def stderr(): - return ''' - Command "/usr/bin/python -c "import setuptools, tokenize;__file__='/tmp/pip-build-usOyBh/ipaddr/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-ghPfAW-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-usOyBh/ipaddr - ''' - - -def test_match(stdout, stdout_success, stderr): - assert match(Command('pip install ipaddr', stdout=stdout, stderr=stderr), - None) - assert not match(Command('pip install thefuck', stdout=stdout_success), - None) - - -def test_get_new_command(stdout, stdout_success, stderr): - assert get_new_command(Command('pip install ipaddr', stdout=stdout, - stderr=stderr), None)\ - == 'sudo pip install ipaddr' diff --git a/tests/rules/test_sudo.py b/tests/rules/test_sudo.py index 9d77392..7c48d91 100644 --- a/tests/rules/test_sudo.py +++ b/tests/rules/test_sudo.py @@ -3,12 +3,14 @@ from thefuck.rules.sudo import match, get_new_command from tests.utils import Command -@pytest.mark.parametrize('stderr', ['Permission denied', - 'permission denied', - "npm ERR! Error: EACCES, unlink", - 'requested operation requires superuser privilege']) -def test_match(stderr): - assert match(Command(stderr=stderr), None) +@pytest.mark.parametrize('stderr, stdout', [ + ('Permission denied', ''), + ('permission denied', ''), + ("npm ERR! Error: EACCES, unlink", ''), + ('requested operation requires superuser privilege', ''), + ('', "error: [Errno 13] Permission denied: '/usr/local/lib/python2.7/dist-packages/ipaddr.py'")]) +def test_match(stderr, stdout): + assert match(Command(stderr=stderr, stdout=stdout), None) def test_not_match(): diff --git a/thefuck/rules/pip_install_sudo.py b/thefuck/rules/pip_install_sudo.py deleted file mode 100644 index 0d1e37d..0000000 --- a/thefuck/rules/pip_install_sudo.py +++ /dev/null @@ -1,13 +0,0 @@ -import re -from thefuck.utils import sudo_support - - -def match(command, settings): - return (('pip' in command.script and 'install' in command.script) and - 'failed with error code 1' in command.stderr and - ('Errno 13' in command.stdout or - 'Permission denied' in command.stdout)) - - -def get_new_command(command, settings): - return u'sudo {}'.format(command.script) diff --git a/thefuck/rules/sudo.py b/thefuck/rules/sudo.py index 26d87ad..fb0226a 100644 --- a/thefuck/rules/sudo.py +++ b/thefuck/rules/sudo.py @@ -13,7 +13,8 @@ patterns = ['permission denied', def match(command, settings): for pattern in patterns: - if pattern.lower() in command.stderr.lower(): + if pattern.lower() in command.stderr.lower()\ + or pattern.lower() in command.stdout.lower(): return True return False From b80f3ea6e4c77cee3fc02dcbbd0ff5a34ac0ee0f Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Apr 2015 05:02:32 +0200 Subject: [PATCH 057/107] Bump to 1.31 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 79e1c60..28f87e3 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.30' +VERSION = '1.31' setup(name='thefuck', From 7e76ab1dc6912c92dbbcf01b2f0a6c746e15729d Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Wed, 29 Apr 2015 05:06:30 +0200 Subject: [PATCH 058/107] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1843ef4..502a6be 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ alias fuck='eval $(thefuck $(fc -ln -1))' alias FUCK='fuck' ``` -[On in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases) +[Or in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases) Changes will be available only in a new shell session. From 9bf910a2ddc86dee90876651e668f18c46259ec3 Mon Sep 17 00:00:00 2001 From: Namwoo Kim Date: Wed, 29 Apr 2015 15:18:48 +0900 Subject: [PATCH 059/107] Improve a logic to get recommended command based on local environment --- tests/rules/test_brew_unknown_command.py | 10 ++-- thefuck/rules/brew_unknown_command.py | 74 ++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/tests/rules/test_brew_unknown_command.py b/tests/rules/test_brew_unknown_command.py index f7bdcdd..d9a79c3 100644 --- a/tests/rules/test_brew_unknown_command.py +++ b/tests/rules/test_brew_unknown_command.py @@ -10,7 +10,7 @@ def brew_unknown_cmd(): @pytest.fixture -def brew_unknown_cmd_instaa(): +def brew_unknown_cmd2(): return '''Error: Unknown command: instaa''' @@ -20,9 +20,9 @@ def test_match(brew_unknown_cmd): assert not match(Command('brew ' + command), None) -def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd_instaa): - assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd), None)\ - == 'brew list' +def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd2): + assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd), + None) == 'brew list' - assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd_instaa), + assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd2), None) == 'brew install' diff --git a/thefuck/rules/brew_unknown_command.py b/thefuck/rules/brew_unknown_command.py index 447b3c1..1259e3d 100644 --- a/thefuck/rules/brew_unknown_command.py +++ b/thefuck/rules/brew_unknown_command.py @@ -1,11 +1,77 @@ import difflib +import os import re +import subprocess import thefuck.logs -# This commands are based on Homebrew 0.9.5 -brew_commands = ['info', 'home', 'options', 'install', 'uninstall', 'search', - 'list', 'update', 'upgrade', 'pin', 'unpin', 'doctor', - 'create', 'edit'] +BREW_CMD_PATH = '/Library/Homebrew/cmd' +TAP_PATH = '/Library/Taps' +TAP_CMD_PATH = '/%s/%s/cmd' + + +def _get_brew_path_prefix(): + '''To get brew path''' + try: + return subprocess.check_output(['brew', '--prefix']).strip() + except: + return None + + +def _get_brew_commands(brew_path_prefix): + '''To get brew default commands on local environment''' + brew_cmd_path = brew_path_prefix + BREW_CMD_PATH + + commands = (name.replace('.rb', '') for name in os.listdir(brew_cmd_path) + if name.endswith('.rb')) + + return commands + + +def _get_brew_tap_specific_commands(brew_path_prefix): + '''To get tap's specific commands + https://github.com/Homebrew/homebrew/blob/master/Library/brew.rb#L115''' + commands = [] + brew_taps_path = brew_path_prefix + TAP_PATH + + for user in _get_directory_names_only(brew_taps_path): + taps = _get_directory_names_only(brew_taps_path + '/%s' % user) + + # Brew Taps's naming rule + # https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/brew-tap.md#naming-conventions-and-limitations + taps = (tap for tap in taps if tap.startswith('homebrew-')) + for tap in taps: + tap_cmd_path = brew_taps_path + TAP_CMD_PATH % (user, tap) + + if os.path.isdir(tap_cmd_path): + commands += (name.replace('brew-', '').replace('.rb', '') + for name in os.listdir(tap_cmd_path) + if _is_brew_tap_cmd_naming(name)) + + return commands + + +def _is_brew_tap_cmd_naming(name): + if name.startswith('brew-') and name.endswith('.rb'): + return True + + return False + + +def _get_directory_names_only(path): + return [d for d in os.listdir(path) + if os.path.isdir(os.path.join(path, d))] + +brew_commands = [] +brew_path_prefix = _get_brew_path_prefix() + +if brew_path_prefix: + brew_commands += _get_brew_commands(brew_path_prefix) + brew_commands += _get_brew_tap_specific_commands(brew_path_prefix) +else: + # Failback commands for testing (Based on Homebrew 0.9.5) + brew_commands = ['info', 'home', 'options', 'install', 'uninstall', + 'search', 'list', 'update', 'upgrade', 'pin', 'unpin', + 'doctor', 'create', 'edit'] def _get_similar_commands(command): From be9446635be7efa24431c7859357533cd26f9fe5 Mon Sep 17 00:00:00 2001 From: Daniele Date: Thu, 30 Apr 2015 13:54:02 +0100 Subject: [PATCH 060/107] Add more patterns to sudo.py These patterns cover commands like `reboot` or `dpkg-reconfigure something` --- thefuck/rules/sudo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/thefuck/rules/sudo.py b/thefuck/rules/sudo.py index fb0226a..9e16767 100644 --- a/thefuck/rules/sudo.py +++ b/thefuck/rules/sudo.py @@ -8,7 +8,9 @@ patterns = ['permission denied', 'This command has to be run under the root user.', 'This operation requires root.', 'You need to be root to perform this command.', - 'requested operation requires superuser privilege'] + 'requested operation requires superuser privilege', + 'must be run as root', + 'must be superuser'] def match(command, settings): From 55cfdda203a639727b5b73f7f9c068802e29b134 Mon Sep 17 00:00:00 2001 From: SanketDG Date: Thu, 30 Apr 2015 19:50:37 +0530 Subject: [PATCH 061/107] add rule for shutdown command --- 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 fb0226a..630f918 100644 --- a/thefuck/rules/sudo.py +++ b/thefuck/rules/sudo.py @@ -8,7 +8,8 @@ patterns = ['permission denied', 'This command has to be run under the root user.', 'This operation requires root.', 'You need to be root to perform this command.', - 'requested operation requires superuser privilege'] + 'requested operation requires superuser privilege', + 'Need to be root'] def match(command, settings): From 4276e1b991ea923a2a3bdd227bb3d98ced1fd4e2 Mon Sep 17 00:00:00 2001 From: SanketDG Date: Fri, 1 May 2015 00:12:30 +0530 Subject: [PATCH 062/107] add alias function --- thefuck/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/thefuck/main.py b/thefuck/main.py index 2ce1dd3..524fe0d 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -112,6 +112,10 @@ def is_second_run(command): return command.script.startswith('fuck') +def alias(): + print("\nalias fuck='eval $(thefuck $(fc -ln -1))'\n") + + def main(): colorama.init() user_dir = setup_user_dir() From 8a49b40f6a7dea32f72ccf733eca7b6e84c03686 Mon Sep 17 00:00:00 2001 From: SanketDG Date: Fri, 1 May 2015 00:12:43 +0530 Subject: [PATCH 063/107] add entry point --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 28f87e3..90a5852 100644 --- a/setup.py +++ b/setup.py @@ -17,4 +17,4 @@ setup(name='thefuck', zip_safe=False, install_requires=['pathlib', 'psutil', 'colorama', 'six'], entry_points={'console_scripts': [ - 'thefuck = thefuck.main:main']}) + 'thefuck = thefuck.main:main', 'thefuck-alias = thefuck.main:alias']}) From 32fd929e481ccd4f277df1599e94104667de5931 Mon Sep 17 00:00:00 2001 From: SanketDG Date: Fri, 1 May 2015 00:13:08 +0530 Subject: [PATCH 064/107] add instructions to use thefuck-alias --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 502a6be..9a13e72 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,12 @@ alias fuck='eval $(thefuck $(fc -ln -1))' alias FUCK='fuck' ``` +Alternatively, you can redirect the output of `thefuck-alias`: + +```bash +thefuck-alias >> ~/.bashrc +``` + [Or in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases) Changes will be available only in a new shell session. @@ -200,7 +206,7 @@ Or via environment variables: * `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`; * `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output; * `THEFUCK_NO_COLORS` – disable colored output, `true/false`. - + For example: ```bash From b985dfbffc51e21078c1fceb96abfcd71ea22d0e Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 1 May 2015 04:39:37 +0200 Subject: [PATCH 065/107] #102 Add support of rules with side effects --- tests/test_main.py | 29 ++++++++++++++++++++++++----- tests/test_types.py | 7 ++++--- tests/utils.py | 6 ++++-- thefuck/logs.py | 11 +++++++---- thefuck/main.py | 13 ++++++++----- thefuck/types.py | 4 ++-- 6 files changed, 49 insertions(+), 21 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index cc5bf79..776f5c9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -74,6 +74,15 @@ def test_run_rule(capsys): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), None, None) assert capsys.readouterr() == ('new-command\n', '') + # With side effect: + side_effect = Mock() + settings = Mock() + command = Mock() + main.run_rule(Rule(get_new_command=lambda *_: 'new-command', + side_effect=side_effect), + command, settings) + assert capsys.readouterr() == ('new-command\n', '') + side_effect.assert_called_once_with(command, settings) with patch('thefuck.main.confirm', return_value=False): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), None, None) @@ -82,15 +91,25 @@ def test_run_rule(capsys): def test_confirm(capsys): # When confirmation not required: - assert main.confirm('command', Mock(require_confirmation=False)) + assert main.confirm('command', None, Mock(require_confirmation=False)) assert capsys.readouterr() == ('', 'command\n') + # With side effect and without confirmation: + assert main.confirm('command', Mock(), Mock(require_confirmation=False)) + 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, - no_colors=True)) + assert main.confirm( + 'command', None, Mock(require_confirmation=True, + no_colors=True)) assert capsys.readouterr() == ('', 'command [enter/ctrl+c]') + # With side effect: + assert main.confirm( + 'command', Mock(), 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, - no_colors=True)) + assert not main.confirm('command', None, + Mock(require_confirmation=True, + no_colors=True)) assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n') diff --git a/tests/test_types.py b/tests/test_types.py index 0df4a16..9c587ea 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,11 +1,12 @@ -from thefuck.types import Rule, RulesNamesList, Settings +from thefuck.types import RulesNamesList, Settings +from tests.utils import Rule def test_rules_names_list(): assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp'] assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp']) - assert Rule('lisp', None, None, False) in RulesNamesList(['lisp']) - assert Rule('bash', None, None, False) not in RulesNamesList(['lisp']) + assert Rule('lisp') in RulesNamesList(['lisp']) + assert Rule('bash') not in RulesNamesList(['lisp']) def test_update_settings(): diff --git a/tests/utils.py b/tests/utils.py index 5191d2c..02e19e7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,5 +7,7 @@ def Command(script='', stdout='', stderr=''): def Rule(name='', match=lambda *_: True, get_new_command=lambda *_: '', - enabled_by_default=True): - return types.Rule(name, match, get_new_command, enabled_by_default) + enabled_by_default=True, + side_effect=None): + return types.Rule(name, match, get_new_command, + enabled_by_default, side_effect) diff --git a/thefuck/logs.py b/thefuck/logs.py index b21b245..3d765f6 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -26,17 +26,20 @@ def rule_failed(rule, exc_info, settings): exception('Rule {}'.format(rule.name), exc_info, settings) -def show_command(new_command, settings): - sys.stderr.write('{bold}{command}{reset}\n'.format( +def show_command(new_command, side_effect, settings): + sys.stderr.write('{bold}{command}{side_effect}{reset}\n'.format( command=new_command, + side_effect='*' if side_effect else '', bold=color(colorama.Style.BRIGHT, settings), reset=color(colorama.Style.RESET_ALL, settings))) -def confirm_command(new_command, settings): +def confirm_command(new_command, side_effect, settings): sys.stderr.write( - '{bold}{command}{reset} [{green}enter{reset}/{red}ctrl+c{reset}]'.format( + '{bold}{command}{side_effect}{reset} ' + '[{green}enter{reset}/{red}ctrl+c{reset}]'.format( command=new_command, + side_effect='*' if side_effect else '', bold=color(colorama.Style.BRIGHT, settings), green=color(colorama.Fore.GREEN, settings), red=color(colorama.Fore.RED, settings), diff --git a/thefuck/main.py b/thefuck/main.py index 524fe0d..235784b 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -24,7 +24,8 @@ def load_rule(rule): rule_module = load_source(rule.name[:-3], str(rule)) return types.Rule(rule.name[:-3], rule_module.match, rule_module.get_new_command, - getattr(rule_module, 'enabled_by_default', True)) + getattr(rule_module, 'enabled_by_default', True), + getattr(rule_module, 'side_effect', None)) def get_rules(user_dir, settings): @@ -85,13 +86,13 @@ def get_matched_rule(command, rules, settings): logs.rule_failed(rule, sys.exc_info(), settings) -def confirm(new_command, settings): +def confirm(new_command, side_effect, settings): """Returns `True` when running of new command confirmed.""" if not settings.require_confirmation: - logs.show_command(new_command, settings) + logs.show_command(new_command, side_effect, settings) return True - logs.confirm_command(new_command, settings) + logs.confirm_command(new_command, side_effect, settings) try: sys.stdin.read(1) return True @@ -103,7 +104,9 @@ def confirm(new_command, settings): def run_rule(rule, command, settings): """Runs command from rule for passed command.""" new_command = rule.get_new_command(command, settings) - if confirm(new_command, settings): + if confirm(new_command, rule.side_effect, settings): + if rule.side_effect: + rule.side_effect(command, settings) print(new_command) diff --git a/thefuck/types.py b/thefuck/types.py index b426788..221b0e9 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -4,11 +4,11 @@ from collections import namedtuple Command = namedtuple('Command', ('script', 'stdout', 'stderr')) Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', - 'enabled_by_default')) + 'enabled_by_default', 'side_effect')) class RulesNamesList(list): - """Wrapper a top of list for string rules names.""" + """Wrapper a top of list for storing rules names.""" def __contains__(self, item): return super(RulesNamesList, self).__contains__(item.name) From 5eeb9d704ca285198cdbf61121532f3054fed535 Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 1 May 2015 04:41:33 +0200 Subject: [PATCH 066/107] #102 Use `side_effect` in `ssh_known_host` rule --- tests/rules/test_ssh_known_host.py | 10 +++------- thefuck/rules/ssh_known_hosts.py | 11 +++++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/rules/test_ssh_known_host.py b/tests/rules/test_ssh_known_host.py index 8875bd4..ed414bc 100644 --- a/tests/rules/test_ssh_known_host.py +++ b/tests/rules/test_ssh_known_host.py @@ -2,7 +2,7 @@ import os import pytest from mock import Mock from thefuck.rules.ssh_known_hosts import match, get_new_command,\ - remove_offending_keys + side_effect from tests.utils import Command @@ -53,18 +53,14 @@ def test_match(ssh_error): assert not match(Command('ssh'), None) -def test_remove_offending_keys(ssh_error): +def test_side_effect(ssh_error): errormsg, path, reset, known_hosts = ssh_error command = Command('ssh user@host', stderr=errormsg) - remove_offending_keys(command, None) + side_effect(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', stderr=errormsg), None) == 'ssh user@host' - assert method.call_count diff --git a/thefuck/rules/ssh_known_hosts.py b/thefuck/rules/ssh_known_hosts.py index ab73c42..4e88969 100644 --- a/thefuck/rules/ssh_known_hosts.py +++ b/thefuck/rules/ssh_known_hosts.py @@ -22,7 +22,11 @@ def match(command, settings): return True -def remove_offending_keys(command, settings): +def get_new_command(command, settings): + return command.script + + +def side_effect(command, settings): offending = offending_pattern.findall(command.stderr) for filepath, lineno in offending: with open(filepath, 'r') as fh: @@ -30,8 +34,3 @@ def remove_offending_keys(command, settings): 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 From 9a43ba6e24c6515ea5d53d04b75d28021cc6d3d7 Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 1 May 2015 04:43:55 +0200 Subject: [PATCH 067/107] #102 Update readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 9a13e72..a25d601 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,9 @@ For adding your own rule you should create `your-rule-name.py` in `~/.thefuck/rules`. Rule should contain two functions: `match(command: Command, settings: Settings) -> bool` and `get_new_command(command: Command, settings: Settings) -> str`. +Also the rule can contain optional function +`side_effect(command: Command, settings: Settings) -> None` and +optional boolean `enabled_by_default` `Command` has three attributes: `script`, `stdout` and `stderr`. @@ -177,6 +180,12 @@ def match(command, settings): def get_new_command(command, settings): return 'sudo {}'.format(command.script) + +# Optional: +enabled_by_default = True + +def side_effect(command, settings): + subprocess.call('chmod 777 .', shell=True) ``` [More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules), From ee5c40d4277dec136afecc0dbe2f46b5c5e62be8 Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 1 May 2015 04:46:58 +0200 Subject: [PATCH 068/107] Update rules list in readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a25d601..63abba4 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,10 @@ using matched rule and run it. Rules enabled by default: * `rm_dir` – adds `-rf` when you trying to remove directory; * `ssh_known_hosts` – removes host from `known_hosts` on warning; * `sudo` – prepends `sudo` to previous command if it failed because of permissions; -* `switch_layout` – switches command from your local layout to en. +* `switch_layout` – switches command from your local layout to en; +* `apt_get` – installs app from apt if it not installed; +* `brew_install` – fixes formula name for `brew install`; +* `composer_not_command` – fixes composer command name. Bundled, but not enabled by default: From fb7376f5a56e04be5dcaa81bcb19599ee650abbe Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 1 May 2015 04:47:25 +0200 Subject: [PATCH 069/107] Bump to 1.32 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 90a5852..684f3f3 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.31' +VERSION = '1.32' setup(name='thefuck', From ba601644d697ef5f54198ae4d041eabb8792ad8d Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 1 May 2015 08:38:38 +0200 Subject: [PATCH 070/107] #1 Add history of last commands, allow fuck more than once --- tests/test_history.py | 44 +++++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 33 +++++++++++++++++++++++++------- thefuck/history.py | 27 ++++++++++++++++++++++++++ thefuck/main.py | 26 ++++++++++++------------- 4 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 tests/test_history.py create mode 100644 thefuck/history.py diff --git a/tests/test_history.py b/tests/test_history.py new file mode 100644 index 0000000..3768760 --- /dev/null +++ b/tests/test_history.py @@ -0,0 +1,44 @@ +import pytest +from mock import patch, Mock +from thefuck.history import History + + +@pytest.fixture +def process(monkeypatch): + Process = Mock() + Process.return_value.parent.return_value.pid = 1 + monkeypatch.setattr('thefuck.history.Process', Process) + + +@pytest.fixture +def db(monkeypatch): + class DBMock(dict): + def __init__(self): + super(DBMock, self).__init__() + self.sync = Mock() + + def __call__(self, *args, **kwargs): + return self + + db = DBMock() + monkeypatch.setattr('thefuck.history.shelve.open', db) + return db + + +@pytest.mark.usefixtures('process') +class TestHistory(object): + def test_set(self, db): + history = History() + history.update(last_script='ls', + last_fixed_script=None) + assert db == {'1-last_script': 'ls', + '1-last_fixed_script': None} + + def test_get(self, db): + history = History() + db['1-last_script'] = 'cd ..' + assert history.last_script == 'cd ..' + + def test_get_without_value(self, db): + history = History() + assert history.last_script is None diff --git a/tests/test_main.py b/tests/test_main.py index 776f5c9..bb3710e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -46,15 +46,30 @@ def test_get_command(): return_value=True): Popen.return_value.stdout.read.return_value = b'stdout' Popen.return_value.stderr.read.return_value = b'stderr' - assert main.get_command(Mock(), ['thefuck', 'apt-get', - 'search', 'vim']) \ + assert main.get_command(Mock(), Mock(), + ['thefuck', 'apt-get', 'search', 'vim']) \ == Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', shell=True, stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) - assert main.get_command(Mock(), ['']) is None + assert main.get_command(Mock(), Mock(), ['']) is None + # When command is `fuck`: + assert main.get_command( + Mock(), + Mock(last_script='ls', last_fixed_script='ls -la'), + ['thefuck', 'fuck']).script == 'ls -la' + # When command equals to last command: + assert main.get_command( + Mock(), + Mock(last_script='ls', last_fixed_script='ls -la'), + ['thefuck', 'ls']).script == 'ls -la' + # When last command is `fuck` and no last fixed script: + assert main.get_command( + Mock(), + Mock(last_script='ls', last_fixed_script=''), + ['thefuck', 'ls']).script == 'ls' def test_get_matched_rule(capsys): @@ -72,20 +87,24 @@ def test_get_matched_rule(capsys): def test_run_rule(capsys): with patch('thefuck.main.confirm', return_value=True): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - None, None) + Command(), Mock(), None) assert capsys.readouterr() == ('new-command\n', '') # With side effect: side_effect = Mock() settings = Mock() - command = Mock() + command = Mock(script='ls') + history = Mock() main.run_rule(Rule(get_new_command=lambda *_: 'new-command', side_effect=side_effect), - command, settings) + command, history, settings) assert capsys.readouterr() == ('new-command\n', '') side_effect.assert_called_once_with(command, settings) + # Ensure that history updated: + history.update.assert_called_once_with(last_script='ls', + last_fixed_script='new-command') with patch('thefuck.main.confirm', return_value=False): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - None, None) + Command(), Mock(), None) assert capsys.readouterr() == ('', '') diff --git a/thefuck/history.py b/thefuck/history.py new file mode 100644 index 0000000..86a2441 --- /dev/null +++ b/thefuck/history.py @@ -0,0 +1,27 @@ +import os +import shelve +from tempfile import gettempdir +from psutil import Process + + +class History(object): + """Temporary history of commands/fixed-commands dependent on + current shell instance. + + """ + + def __init__(self): + self._path = os.path.join(gettempdir(), '.thefuck_history') + self._pid = Process(os.getpid()).parent().pid + self._db = shelve.open(self._path) + + def _prepare_key(self, key): + return '{}-{}'.format(self._pid, key) + + def update(self, **kwargs): + self._db.update({self._prepare_key(k): v for k,v in kwargs.items()}) + self._db.sync() + return self + + def __getattr__(self, item): + return self._db.get(self._prepare_key(item)) diff --git a/thefuck/main.py b/thefuck/main.py index 235784b..d75a43e 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -6,6 +6,7 @@ import os import sys from psutil import Process, TimeoutExpired import colorama +from .history import History from . import logs, conf, types @@ -59,16 +60,21 @@ def wait_output(settings, popen): return False -def get_command(settings, args): +def get_command(settings, history, args): """Creates command from `args` and executes it.""" if sys.version_info[0] < 3: script = ' '.join(arg.decode('utf-8') for arg in args[1:]) else: script = ' '.join(args[1:]) + if script == 'fuck' or script == history.last_script: + script = history.last_fixed_script or history.last_script + if not script: return + history.update(last_script=script, + last_fixed_script=None) result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=dict(os.environ, LANG='C')) if wait_output(settings, result): @@ -101,20 +107,17 @@ def confirm(new_command, side_effect, settings): return False -def run_rule(rule, command, settings): +def run_rule(rule, command, history, settings): """Runs command from rule for passed command.""" new_command = rule.get_new_command(command, settings) if confirm(new_command, rule.side_effect, settings): if rule.side_effect: rule.side_effect(command, settings) + history.update(last_script=command.script, + last_fixed_script=new_command) print(new_command) -def is_second_run(command): - """Is it the second run of `fuck`?""" - return command.script.startswith('fuck') - - def alias(): print("\nalias fuck='eval $(thefuck $(fc -ln -1))'\n") @@ -123,17 +126,14 @@ def main(): colorama.init() user_dir = setup_user_dir() settings = conf.get_settings(user_dir) + history = History() - command = get_command(settings, sys.argv) + command = get_command(settings, history, sys.argv) if command: - if is_second_run(command): - logs.failed("Can't fuck twice", settings) - return - rules = get_rules(user_dir, settings) matched_rule = get_matched_rule(command, rules, settings) if matched_rule: - run_rule(matched_rule, command, settings) + run_rule(matched_rule, command, history, settings) return logs.failed('No fuck given', settings) From dd1861955cdaa18ac05d83810c1a9e67707396c4 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 2 May 2015 04:29:55 +0200 Subject: [PATCH 071/107] Refine tests --- tests/test_conf.py | 129 +++++++++++++------------ tests/test_history.py | 48 +++++----- tests/test_main.py | 217 ++++++++++++++++++++++++------------------ tests/test_utils.py | 34 +++---- 4 files changed, 237 insertions(+), 191 deletions(-) diff --git a/tests/test_conf.py b/tests/test_conf.py index 608a49e..c5a8333 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -1,89 +1,100 @@ +import pytest import six -from mock import patch, Mock +from mock import Mock from thefuck import conf from tests.utils import Rule -def test_default(): - assert Rule('test', enabled_by_default=True) in conf.DEFAULT_RULES - assert Rule('test', enabled_by_default=False) not in conf.DEFAULT_RULES - assert Rule('test', enabled_by_default=False) in (conf.DEFAULT_RULES + ['test']) +@pytest.mark.parametrize('enabled, rules, result', [ + (True, conf.DEFAULT_RULES, True), + (False, conf.DEFAULT_RULES, False), + (False, conf.DEFAULT_RULES + ['test'], True)]) +def test_default(enabled, rules, result): + assert (Rule('test', enabled_by_default=enabled) in rules) == result -def test_settings_defaults(): - with patch('thefuck.conf.load_source', return_value=object()), \ - patch('thefuck.conf.os.environ', new_callable=lambda: {}): - for key, val in conf.DEFAULT_SETTINGS.items(): - assert getattr(conf.get_settings(Mock()), key) == val +@pytest.fixture +def load_source(monkeypatch): + mock = Mock() + monkeypatch.setattr('thefuck.conf.load_source', mock) + return mock -def test_settings_from_file(): - with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'], - wait_command=10, - require_confirmation=True, - no_colors=True)), \ - patch('thefuck.conf.os.environ', new_callable=lambda: {}): +@pytest.fixture +def environ(monkeypatch): + data = {} + monkeypatch.setattr('thefuck.conf.os.environ', data) + return data + + +@pytest.mark.usefixture('environ') +def test_settings_defaults(load_source): + load_source.return_value = object() + for key, val in conf.DEFAULT_SETTINGS.items(): + assert getattr(conf.get_settings(Mock()), key) == val + + +@pytest.mark.usefixture('environ') +class TestSettingsFromFile(object): + def test_from_file(self, load_source): + load_source.return_value = Mock(rules=['test'], + wait_command=10, + require_confirmation=True, + no_colors=True) settings = conf.get_settings(Mock()) assert settings.rules == ['test'] assert settings.wait_command == 10 assert settings.require_confirmation is True assert settings.no_colors is True - -def test_settings_from_file_with_DEFAULT(): - with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT_RULES + ['test'], - wait_command=10, - require_confirmation=True, - no_colors=True)), \ - patch('thefuck.conf.os.environ', new_callable=lambda: {}): + def test_from_file_with_DEFAULT(self, load_source): + load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'], + wait_command=10, + require_confirmation=True, + no_colors=True) settings = conf.get_settings(Mock()) assert settings.rules == conf.DEFAULT_RULES + ['test'] -def test_settings_from_env(): - with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'], - wait_command=10)), \ - patch('thefuck.conf.os.environ', - new_callable=lambda: {'THEFUCK_RULES': 'bash:lisp', - 'THEFUCK_WAIT_COMMAND': '55', - 'THEFUCK_REQUIRE_CONFIRMATION': 'true', - 'THEFUCK_NO_COLORS': 'false'}): +@pytest.mark.usefixture('load_source') +class TestSettingsFromEnv(object): + def test_from_env(self, environ): + environ.update({'THEFUCK_RULES': 'bash:lisp', + 'THEFUCK_WAIT_COMMAND': '55', + 'THEFUCK_REQUIRE_CONFIRMATION': 'true', + 'THEFUCK_NO_COLORS': 'false'}) settings = conf.get_settings(Mock()) assert settings.rules == ['bash', 'lisp'] assert settings.wait_command == 55 assert settings.require_confirmation is True assert settings.no_colors is False - -def test_settings_from_env_with_DEFAULT(): - with patch('thefuck.conf.load_source', return_value=Mock()), \ - patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}): + def test_from_env_with_DEFAULT(self, environ): + environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) settings = conf.get_settings(Mock()) assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp'] -def test_initialize_settings_file_ignore_if_exists(): - settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock()) - user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) - conf.initialize_settings_file(user_dir_mock) - assert settings_path_mock.is_file.call_count == 1 - assert not settings_path_mock.open.called +class TestInitializeSettingsFile(object): + def test_ignore_if_exists(self): + settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock()) + user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) + conf.initialize_settings_file(user_dir_mock) + assert settings_path_mock.is_file.call_count == 1 + assert not settings_path_mock.open.called - -def test_initialize_settings_file_create_if_exists_not(): - settings_file = six.StringIO() - settings_path_mock = Mock( - is_file=Mock(return_value=False), - open=Mock(return_value=Mock( - __exit__=lambda *args: None, __enter__=lambda *args: settings_file - )), - ) - user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) - conf.initialize_settings_file(user_dir_mock) - settings_file_contents = settings_file.getvalue() - assert settings_path_mock.is_file.call_count == 1 - assert settings_path_mock.open.call_count == 1 - assert conf.SETTINGS_HEADER in settings_file_contents - for setting in conf.DEFAULT_SETTINGS.items(): - assert '# {} = {}\n'.format(*setting) in settings_file_contents - settings_file.close() + def test_create_if_doesnt_exists(self): + settings_file = six.StringIO() + settings_path_mock = Mock( + is_file=Mock(return_value=False), + open=Mock(return_value=Mock( + __exit__=lambda *args: None, __enter__=lambda *args: settings_file))) + user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) + conf.initialize_settings_file(user_dir_mock) + settings_file_contents = settings_file.getvalue() + assert settings_path_mock.is_file.call_count == 1 + assert settings_path_mock.open.call_count == 1 + assert conf.SETTINGS_HEADER in settings_file_contents + for setting in conf.DEFAULT_SETTINGS.items(): + assert '# {} = {}\n'.format(*setting) in settings_file_contents + settings_file.close() diff --git a/tests/test_history.py b/tests/test_history.py index 3768760..5e76c60 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -1,32 +1,30 @@ import pytest -from mock import patch, Mock +from mock import Mock from thefuck.history import History -@pytest.fixture -def process(monkeypatch): - Process = Mock() - Process.return_value.parent.return_value.pid = 1 - monkeypatch.setattr('thefuck.history.Process', Process) - - -@pytest.fixture -def db(monkeypatch): - class DBMock(dict): - def __init__(self): - super(DBMock, self).__init__() - self.sync = Mock() - - def __call__(self, *args, **kwargs): - return self - - db = DBMock() - monkeypatch.setattr('thefuck.history.shelve.open', db) - return db - - -@pytest.mark.usefixtures('process') class TestHistory(object): + @pytest.fixture(autouse=True) + def process(self, monkeypatch): + Process = Mock() + Process.return_value.parent.return_value.pid = 1 + monkeypatch.setattr('thefuck.history.Process', Process) + return Process + + @pytest.fixture(autouse=True) + def db(self, monkeypatch): + class DBMock(dict): + def __init__(self): + super(DBMock, self).__init__() + self.sync = Mock() + + def __call__(self, *args, **kwargs): + return self + + db = DBMock() + monkeypatch.setattr('thefuck.history.shelve.open', db) + return db + def test_set(self, db): history = History() history.update(last_script='ls', @@ -39,6 +37,6 @@ class TestHistory(object): db['1-last_script'] = 'cd ..' assert history.last_script == 'cd ..' - def test_get_without_value(self, db): + def test_get_without_value(self): history = History() assert history.last_script is None diff --git a/tests/test_main.py b/tests/test_main.py index bb3710e..8e730f0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,51 +1,56 @@ +import pytest from subprocess import PIPE from pathlib import PosixPath, Path -from mock import patch, Mock +from mock import Mock from thefuck import main, conf, types from tests.utils import Rule, Command -def test_load_rule(): +def test_load_rule(monkeypatch): match = object() get_new_command = object() - with patch('thefuck.main.load_source', - return_value=Mock( - match=match, - get_new_command=get_new_command, - enabled_by_default=True)) as load_source: - assert main.load_rule(Path('/rules/bash.py')) \ - == Rule('bash', match, get_new_command) - load_source.assert_called_once_with('bash', '/rules/bash.py') + load_source = Mock() + load_source.return_value = Mock(match=match, + get_new_command=get_new_command, + enabled_by_default=True) + monkeypatch.setattr('thefuck.main.load_source', load_source) + assert main.load_rule(Path('/rules/bash.py')) \ + == Rule('bash', match, get_new_command) + 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, - enabled_by_default=True)): - glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] - assert list(main.get_rules( - Path('~'), - Mock(rules=conf.DEFAULT_RULES))) \ - == [Rule('bash', 'bash', 'bash'), - Rule('lisp', 'lisp', 'lisp'), - Rule('bash', 'bash', 'bash'), - Rule('lisp', 'lisp', 'lisp')] - assert list(main.get_rules( - Path('~'), - Mock(rules=types.RulesNamesList(['bash'])))) \ - == [Rule('bash', 'bash', 'bash'), - Rule('bash', 'bash', 'bash')] +@pytest.mark.parametrize('conf_rules, rules', [ + (conf.DEFAULT_RULES, [Rule('bash', 'bash', 'bash'), + Rule('lisp', 'lisp', 'lisp'), + Rule('bash', 'bash', 'bash'), + Rule('lisp', 'lisp', 'lisp')]), + (types.RulesNamesList(['bash']), [Rule('bash', 'bash', 'bash'), + Rule('bash', 'bash', 'bash')])]) +def test_get_rules(monkeypatch, conf_rules, rules): + monkeypatch.setattr( + 'thefuck.main.Path.glob', + lambda *_: [PosixPath('bash.py'), PosixPath('lisp.py')]) + monkeypatch.setattr('thefuck.main.load_source', + lambda x, _: Mock(match=x, get_new_command=x, + enabled_by_default=True)) + assert list(main.get_rules(Path('~'), Mock(rules=conf_rules))) == rules -def test_get_command(): - with patch('thefuck.main.Popen') as Popen, \ - patch('thefuck.main.os.environ', - new_callable=lambda: {}), \ - patch('thefuck.main.wait_output', - return_value=True): +class TestGetCommand(object): + @pytest.fixture(autouse=True) + def Popen(self, monkeypatch): + Popen = Mock() Popen.return_value.stdout.read.return_value = b'stdout' Popen.return_value.stderr.read.return_value = b'stderr' + monkeypatch.setattr('thefuck.main.Popen', Popen) + return Popen + + @pytest.fixture(autouse=True) + def prepare(self, monkeypatch): + monkeypatch.setattr('thefuck.main.os.environ', {}) + monkeypatch.setattr('thefuck.main.wait_output', lambda *_: True) + + def test_get_command_calls(self, Popen): assert main.get_command(Mock(), Mock(), ['thefuck', 'apt-get', 'search', 'vim']) \ == Command('apt-get search vim', 'stdout', 'stderr') @@ -54,81 +59,113 @@ def test_get_command(): stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) - assert main.get_command(Mock(), Mock(), ['']) is None - # When command is `fuck`: - assert main.get_command( - Mock(), - Mock(last_script='ls', last_fixed_script='ls -la'), - ['thefuck', 'fuck']).script == 'ls -la' - # When command equals to last command: - assert main.get_command( - Mock(), - Mock(last_script='ls', last_fixed_script='ls -la'), - ['thefuck', 'ls']).script == 'ls -la' - # When last command is `fuck` and no last fixed script: - assert main.get_command( - Mock(), - Mock(last_script='ls', last_fixed_script=''), - ['thefuck', 'ls']).script == 'ls' + + @pytest.mark.parametrize('history, args, result', [ + (Mock(), [''], None), + (Mock(last_script='ls', last_fixed_script='ls -la'), + ['thefuck', 'fuck'], 'ls -la'), + (Mock(last_script='ls', last_fixed_script='ls -la'), + ['thefuck', 'ls'], 'ls -la'), + (Mock(last_script='ls', last_fixed_script=''), + ['thefuck', 'ls'], 'ls'), + (Mock(last_script='ls', last_fixed_script=''), + ['thefuck', 'fuck'], 'ls')]) + def test_get_command_script(self, history, args, result): + if result: + assert main.get_command(Mock(), history, args).script == result + else: + assert main.get_command(Mock(), history, args) is None -def test_get_matched_rule(capsys): - rules = [Rule('', lambda x, _: x.script == 'cd ..'), - Rule('', lambda *_: False), - Rule('rule', Mock(side_effect=OSError('Denied')))] - assert main.get_matched_rule(Command('ls'), - rules, Mock(no_colors=True)) is None - assert main.get_matched_rule(Command('cd ..'), - rules, Mock(no_colors=True)) == rules[0] - assert capsys.readouterr()[1].split('\n')[0] \ - == '[WARN] Rule rule:' +class TestGetMatchedRule(object): + @pytest.fixture + def rules(self): + return [Rule('', lambda x, _: x.script == 'cd ..'), + Rule('', lambda *_: False), + Rule('rule', Mock(side_effect=OSError('Denied')))] + + def test_no_match(self): + assert main.get_matched_rule( + Command('ls'), [Rule('', lambda *_: False)], + Mock(no_colors=True)) is None + + def test_match(self): + rule = Rule('', lambda x, _: x.script == 'cd ..') + assert main.get_matched_rule( + Command('cd ..'), [rule], Mock(no_colors=True)) == rule + + def test_when_rule_failed(self, capsys): + main.get_matched_rule( + Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))], + Mock(no_colors=True)) + assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:' -def test_run_rule(capsys): - with patch('thefuck.main.confirm', return_value=True): +class TestRunRule(object): + @pytest.fixture(autouse=True) + def confirm(self, monkeypatch): + mock = Mock(return_value=True) + monkeypatch.setattr('thefuck.main.confirm', mock) + return mock + + def test_run_rule(self, capsys): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), Command(), Mock(), None) assert capsys.readouterr() == ('new-command\n', '') - # With side effect: + + def test_run_rule_with_side_effect(self, capsys): side_effect = Mock() settings = Mock() - command = Mock(script='ls') - history = Mock() + command = Command() main.run_rule(Rule(get_new_command=lambda *_: 'new-command', side_effect=side_effect), - command, history, settings) + command, Mock(), settings) assert capsys.readouterr() == ('new-command\n', '') side_effect.assert_called_once_with(command, settings) - # Ensure that history updated: + + def test_hisotry_updated(self): + history = Mock() + main.run_rule(Rule(get_new_command=lambda *_: 'ls -lah'), + Command('ls'), history, None) history.update.assert_called_once_with(last_script='ls', - last_fixed_script='new-command') - with patch('thefuck.main.confirm', return_value=False): + last_fixed_script='ls -lah') + + def test_when_not_comfirmed(self, capsys, confirm): + confirm.return_value = False main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), Command(), Mock(), None) assert capsys.readouterr() == ('', '') -def test_confirm(capsys): - # When confirmation not required: - assert main.confirm('command', None, Mock(require_confirmation=False)) - assert capsys.readouterr() == ('', 'command\n') - # With side effect and without confirmation: - assert main.confirm('command', Mock(), Mock(require_confirmation=False)) - assert capsys.readouterr() == ('', 'command*\n') - # When confirmation required and confirmed: - with patch('thefuck.main.sys.stdin.read', return_value='\n'): - assert main.confirm( - 'command', None, Mock(require_confirmation=True, - no_colors=True)) +class TestConfirm(object): + @pytest.fixture + def stdin(self, monkeypatch): + mock = Mock(return_value='\n') + monkeypatch.setattr('sys.stdin.read', mock) + return mock + + def test_when_not_required(self, capsys): + assert main.confirm('command', None, Mock(require_confirmation=False)) + assert capsys.readouterr() == ('', 'command\n') + + def test_with_side_effect_and_without_confirmation(self, capsys): + assert main.confirm('command', Mock(), Mock(require_confirmation=False)) + assert capsys.readouterr() == ('', 'command*\n') + + # `stdin` fixture should be applied after `capsys` + def test_when_confirmation_required_and_confirmed(self, capsys, stdin): + assert main.confirm('command', None, Mock(require_confirmation=True, + no_colors=True)) assert capsys.readouterr() == ('', 'command [enter/ctrl+c]') - # With side effect: - assert main.confirm( - 'command', Mock(), Mock(require_confirmation=True, - no_colors=True)) + + # `stdin` fixture should be applied after `capsys` + def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin): + assert main.confirm('command', Mock(), 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', None, - Mock(require_confirmation=True, - no_colors=True)) + + def test_when_confirmation_required_and_aborted(self, capsys, stdin): + stdin.side_effect = KeyboardInterrupt + assert not main.confirm('command', None, Mock(require_confirmation=True, + no_colors=True)) assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n') diff --git a/tests/test_utils.py b/tests/test_utils.py index 08766a9..24d6b19 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,26 +1,26 @@ +import pytest from mock import Mock from thefuck.utils import sudo_support, wrap_settings from thefuck.types import Settings from tests.utils import Command -def test_wrap_settings(): +@pytest.mark.parametrize('override, old, new', [ + ({'key': 'val'}, {}, {'key': 'val'}), + ({'key': 'new-val'}, {'key': 'val'}, {'key': 'new-val'})]) +def test_wrap_settings(override, old, new): fn = lambda _, settings: settings - assert wrap_settings({'key': 'val'})(fn)(None, Settings({})) \ - == {'key': 'val'} - assert wrap_settings({'key': 'new-val'})(fn)( - None, Settings({'key': 'val'})) == {'key': 'new-val'} + assert wrap_settings(override)(fn)(None, Settings(old)) == new -def test_sudo_support(): - fn = Mock(return_value=True, __name__='') - assert sudo_support(fn)(Command('sudo ls'), None) - fn.assert_called_once_with(Command('ls'), None) - - fn.return_value = False - assert not sudo_support(fn)(Command('sudo ls'), None) - - fn.return_value = 'pwd' - assert sudo_support(fn)(Command('sudo ls'), None) == 'sudo pwd' - - assert sudo_support(fn)(Command('ls'), None) == 'pwd' +@pytest.mark.parametrize('return_value, command, called, result', [ + ('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'), + ('ls -lah', 'ls', 'ls', 'ls -lah'), + (True, 'sudo ls', 'ls', True), + (True, 'ls', 'ls', True), + (False, 'sudo ls', 'ls', False), + (False, 'ls', 'ls', False)]) +def test_sudo_support(return_value, command, called, result): + fn = Mock(return_value=return_value, __name__='') + assert sudo_support(fn)(Command(command), None) == result + fn.assert_called_once_with(Command(called), None) From 2acfea3350101b77e8bb5a638d96d4ffc9f52749 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 2 May 2015 04:32:07 +0200 Subject: [PATCH 072/107] #1 s/last_script/last_command/, s/last_fixed_script/last_fixed_command/ --- tests/test_history.py | 14 +++++++------- tests/test_main.py | 12 ++++++------ thefuck/main.py | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/test_history.py b/tests/test_history.py index 5e76c60..12af890 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -27,16 +27,16 @@ class TestHistory(object): def test_set(self, db): history = History() - history.update(last_script='ls', - last_fixed_script=None) - assert db == {'1-last_script': 'ls', - '1-last_fixed_script': None} + history.update(last_command='ls', + last_fixed_command=None) + assert db == {'1-last_command': 'ls', + '1-last_fixed_command': None} def test_get(self, db): history = History() - db['1-last_script'] = 'cd ..' - assert history.last_script == 'cd ..' + db['1-last_command'] = 'cd ..' + assert history.last_command == 'cd ..' def test_get_without_value(self): history = History() - assert history.last_script is None + assert history.last_command is None diff --git a/tests/test_main.py b/tests/test_main.py index 8e730f0..45e4880 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -62,13 +62,13 @@ class TestGetCommand(object): @pytest.mark.parametrize('history, args, result', [ (Mock(), [''], None), - (Mock(last_script='ls', last_fixed_script='ls -la'), + (Mock(last_command='ls', last_fixed_command='ls -la'), ['thefuck', 'fuck'], 'ls -la'), - (Mock(last_script='ls', last_fixed_script='ls -la'), + (Mock(last_command='ls', last_fixed_command='ls -la'), ['thefuck', 'ls'], 'ls -la'), - (Mock(last_script='ls', last_fixed_script=''), + (Mock(last_command='ls', last_fixed_command=''), ['thefuck', 'ls'], 'ls'), - (Mock(last_script='ls', last_fixed_script=''), + (Mock(last_command='ls', last_fixed_command=''), ['thefuck', 'fuck'], 'ls')]) def test_get_command_script(self, history, args, result): if result: @@ -127,8 +127,8 @@ class TestRunRule(object): history = Mock() main.run_rule(Rule(get_new_command=lambda *_: 'ls -lah'), Command('ls'), history, None) - history.update.assert_called_once_with(last_script='ls', - last_fixed_script='ls -lah') + history.update.assert_called_once_with(last_command='ls', + last_fixed_command='ls -lah') def test_when_not_comfirmed(self, capsys, confirm): confirm.return_value = False diff --git a/thefuck/main.py b/thefuck/main.py index d75a43e..de1c425 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -67,14 +67,14 @@ def get_command(settings, history, args): else: script = ' '.join(args[1:]) - if script == 'fuck' or script == history.last_script: - script = history.last_fixed_script or history.last_script + if script == 'fuck' or script == history.last_command: + script = history.last_fixed_command or history.last_command if not script: return - history.update(last_script=script, - last_fixed_script=None) + history.update(last_command=script, + last_fixed_command=None) result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=dict(os.environ, LANG='C')) if wait_output(settings, result): @@ -113,8 +113,8 @@ def run_rule(rule, command, history, settings): if confirm(new_command, rule.side_effect, settings): if rule.side_effect: rule.side_effect(command, settings) - history.update(last_script=command.script, - last_fixed_script=new_command) + history.update(last_command=command.script, + last_fixed_command=new_command) print(new_command) From 938f1df0355a0364d82c8a2d9e2ccb929be999b7 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 2 May 2015 04:56:23 +0200 Subject: [PATCH 073/107] Remove not used fixture --- tests/test_main.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 45e4880..68912cf 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -78,12 +78,6 @@ class TestGetCommand(object): class TestGetMatchedRule(object): - @pytest.fixture - def rules(self): - return [Rule('', lambda x, _: x.script == 'cd ..'), - Rule('', lambda *_: False), - Rule('rule', Mock(side_effect=OSError('Denied')))] - def test_no_match(self): assert main.get_matched_rule( Command('ls'), [Rule('', lambda *_: False)], From fcc2a1a40a6e07a76cc653352c052e810df0546f Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 3 May 2015 12:46:01 +0200 Subject: [PATCH 074/107] #128 #69 add support of shell specific actions, add alias expansion for bash and zsh --- tests/test_main.py | 5 +++ tests/test_shells.py | 53 +++++++++++++++++++++++++++++ thefuck/main.py | 5 +-- thefuck/shells.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 tests/test_shells.py create mode 100644 thefuck/shells.py diff --git a/tests/test_main.py b/tests/test_main.py index 68912cf..a871c76 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -50,6 +50,11 @@ class TestGetCommand(object): monkeypatch.setattr('thefuck.main.os.environ', {}) monkeypatch.setattr('thefuck.main.wait_output', lambda *_: True) + @pytest.fixture(autouse=True) + def generic_shell(self, monkeypatch): + monkeypatch.setattr('thefuck.shells.from_shell', lambda x: x) + monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x) + def test_get_command_calls(self, Popen): assert main.get_command(Mock(), Mock(), ['thefuck', 'apt-get', 'search', 'vim']) \ diff --git a/tests/test_shells.py b/tests/test_shells.py new file mode 100644 index 0000000..c24112b --- /dev/null +++ b/tests/test_shells.py @@ -0,0 +1,53 @@ +import pytest +from mock import Mock +from thefuck import shells + + +class TestGeneric(object): + def test_from_shell(self): + assert shells.Generic().from_shell('pwd') == 'pwd' + + def test_to_shell(self): + assert shells.Bash().to_shell('pwd') == 'pwd' + + +class TestBash(object): + @pytest.fixture(autouse=True) + def Popen(self, monkeypatch): + mock = Mock() + monkeypatch.setattr('thefuck.shells.Popen', mock) + mock.return_value.stdout.read.return_value = ( + b'alias l=\'ls -CF\'\n' + b'alias la=\'ls -A\'\n' + b'alias ll=\'ls -alF\'') + return mock + + @pytest.mark.parametrize('before, after', [ + ('pwd', 'pwd'), + ('ll', 'ls -alF')]) + def test_from_shell(self, before, after): + assert shells.Bash().from_shell(before) == after + + def test_to_shell(self): + assert shells.Bash().to_shell('pwd') == 'pwd' + + +class TestZsh(object): + @pytest.fixture(autouse=True) + def Popen(self, monkeypatch): + mock = Mock() + monkeypatch.setattr('thefuck.shells.Popen', mock) + mock.return_value.stdout.read.return_value = ( + b'l=\'ls -CF\'\n' + b'la=\'ls -A\'\n' + b'll=\'ls -alF\'') + return mock + + @pytest.mark.parametrize('before, after', [ + ('pwd', 'pwd'), + ('ll', 'ls -alF')]) + def test_from_shell(self, before, after): + assert shells.Zsh().from_shell(before) == after + + def test_to_shell(self): + assert shells.Zsh().to_shell('pwd') == 'pwd' \ No newline at end of file diff --git a/thefuck/main.py b/thefuck/main.py index de1c425..8a2fd3a 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -7,7 +7,7 @@ import sys from psutil import Process, TimeoutExpired import colorama from .history import History -from . import logs, conf, types +from . import logs, conf, types, shells def setup_user_dir(): @@ -73,6 +73,7 @@ def get_command(settings, history, args): if not script: return + script = shells.from_shell(script) history.update(last_command=script, last_fixed_command=None) result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, @@ -109,7 +110,7 @@ def confirm(new_command, side_effect, settings): def run_rule(rule, command, history, settings): """Runs command from rule for passed command.""" - new_command = rule.get_new_command(command, settings) + new_command = shells.to_shell(rule.get_new_command(command, settings)) if confirm(new_command, rule.side_effect, settings): if rule.side_effect: rule.side_effect(command, settings) diff --git a/thefuck/shells.py b/thefuck/shells.py new file mode 100644 index 0000000..b0fdc7a --- /dev/null +++ b/thefuck/shells.py @@ -0,0 +1,80 @@ +"""Module with shell specific actions, each shell class should +implement `from_shell` and `to_shell` methods. + +""" +from collections import defaultdict +from subprocess import Popen, PIPE +import os +from psutil import Process + + +FNULL = open(os.devnull, 'w') + + +class Generic(object): + def _get_aliases(self): + return {} + + def _expand_aliases(self, command_script): + aliases = self._get_aliases() + binary = command_script.split(' ')[0] + if binary in aliases: + return command_script.replace(binary, aliases[binary], 1) + else: + return command_script + + def from_shell(self, command_script): + """Prepares command before running in app.""" + return self._expand_aliases(command_script) + + def to_shell(self, command_script): + """Prepares command for running in shell.""" + return command_script + + +class Bash(Generic): + def _parse_alias(self, alias): + name, value = alias.replace('alias ', '', 1).split('=', 1) + if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": + value = value[1:-1] + return name, value + + def _get_aliases(self): + proc = Popen('bash -ic alias', stdout=PIPE, stderr=FNULL, shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias) + + +class Zsh(Generic): + def _parse_alias(self, alias): + name, value = alias.split('=', 1) + if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": + value = value[1:-1] + return name, value + + def _get_aliases(self): + proc = Popen('zsh -ic alias', stdout=PIPE, stderr=FNULL, shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias) + + +shells = defaultdict(lambda: Generic(), { + 'bash': Bash(), + 'zsh': Zsh()}) + + +def _get_shell(): + shell = Process(os.getpid()).parent().cmdline()[0] + return shells[shell] + + +def from_shell(command): + return _get_shell().from_shell(command) + + +def to_shell(command): + return _get_shell().to_shell(command) From bcd3154121bfe334a0713af9b12ad9aede3ae642 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 3 May 2015 12:59:37 +0200 Subject: [PATCH 075/107] Bump to 1.33 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 684f3f3..dbd09fc 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.32' +VERSION = '1.33' setup(name='thefuck', From 3ce8c1187c09ebe7cc7ebd82a37f89b28c71c29a Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 3 May 2015 13:04:33 +0200 Subject: [PATCH 076/107] Make `thefuck-alias` depends on current shell --- thefuck/main.py | 2 +- thefuck/shells.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/thefuck/main.py b/thefuck/main.py index 8a2fd3a..0a62d45 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -120,7 +120,7 @@ def run_rule(rule, command, history, settings): def alias(): - print("\nalias fuck='eval $(thefuck $(fc -ln -1))'\n") + print(shells.app_alias()) def main(): diff --git a/thefuck/shells.py b/thefuck/shells.py index b0fdc7a..a9bc7cb 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -31,6 +31,9 @@ class Generic(object): """Prepares command for running in shell.""" return command_script + def app_alias(self): + return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" + class Bash(Generic): def _parse_alias(self, alias): @@ -78,3 +81,7 @@ def from_shell(command): def to_shell(command): return _get_shell().to_shell(command) + + +def app_alias(): + return _get_shell().app_alias() From 93c90d575868caff09f37cc11c22b2a06732b2e5 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 3 May 2015 13:24:33 +0200 Subject: [PATCH 077/107] #157 Don't fail if can't get brew commands --- thefuck/rules/brew_unknown_command.py | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/thefuck/rules/brew_unknown_command.py b/thefuck/rules/brew_unknown_command.py index 1259e3d..6664d8e 100644 --- a/thefuck/rules/brew_unknown_command.py +++ b/thefuck/rules/brew_unknown_command.py @@ -2,7 +2,7 @@ import difflib import os import re import subprocess -import thefuck.logs + BREW_CMD_PATH = '/Library/Homebrew/cmd' TAP_PATH = '/Library/Taps' @@ -10,7 +10,7 @@ TAP_CMD_PATH = '/%s/%s/cmd' def _get_brew_path_prefix(): - '''To get brew path''' + """To get brew path""" try: return subprocess.check_output(['brew', '--prefix']).strip() except: @@ -18,18 +18,18 @@ def _get_brew_path_prefix(): def _get_brew_commands(brew_path_prefix): - '''To get brew default commands on local environment''' + """To get brew default commands on local environment""" brew_cmd_path = brew_path_prefix + BREW_CMD_PATH - commands = (name.replace('.rb', '') for name in os.listdir(brew_cmd_path) - if name.endswith('.rb')) + commands = [name.replace('.rb', '') for name in os.listdir(brew_cmd_path) + if name.endswith('.rb')] return commands def _get_brew_tap_specific_commands(brew_path_prefix): - '''To get tap's specific commands - https://github.com/Homebrew/homebrew/blob/master/Library/brew.rb#L115''' + """To get tap's specific commands + https://github.com/Homebrew/homebrew/blob/master/Library/brew.rb#L115""" commands = [] brew_taps_path = brew_path_prefix + TAP_PATH @@ -61,17 +61,20 @@ def _get_directory_names_only(path): return [d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))] -brew_commands = [] + brew_path_prefix = _get_brew_path_prefix() +# Failback commands for testing (Based on Homebrew 0.9.5) +brew_commands = ['info', 'home', 'options', 'install', 'uninstall', + 'search', 'list', 'update', 'upgrade', 'pin', 'unpin', + 'doctor', 'create', 'edit'] + if brew_path_prefix: - brew_commands += _get_brew_commands(brew_path_prefix) - brew_commands += _get_brew_tap_specific_commands(brew_path_prefix) -else: - # Failback commands for testing (Based on Homebrew 0.9.5) - brew_commands = ['info', 'home', 'options', 'install', 'uninstall', - 'search', 'list', 'update', 'upgrade', 'pin', 'unpin', - 'doctor', 'create', 'edit'] + try: + brew_commands = _get_brew_commands(brew_path_prefix) \ + + _get_brew_tap_specific_commands(brew_path_prefix) + except OSError: + pass def _get_similar_commands(command): From 72ac9650f944746fca2b5a48c031579c205b730f Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 3 May 2015 13:25:01 +0200 Subject: [PATCH 078/107] Bump to 1.34 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dbd09fc..57e3009 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.33' +VERSION = '1.34' setup(name='thefuck', From 3f2fe0d275e38d9825721b5d1c70bb5369462b89 Mon Sep 17 00:00:00 2001 From: nvbn Date: Mon, 4 May 2015 04:44:16 +0200 Subject: [PATCH 079/107] #89 #152 Use shell history --- README.md | 10 ++++++++-- tests/test_history.py | 42 ------------------------------------------ tests/test_main.py | 35 ++++++++++------------------------- tests/test_shells.py | 38 +++++++++++++++++++++++++++++++++++--- thefuck/history.py | 27 --------------------------- thefuck/main.py | 21 +++++++-------------- thefuck/shells.py | 35 ++++++++++++++++++++++++++++++++++- 7 files changed, 94 insertions(+), 114 deletions(-) delete mode 100644 tests/test_history.py delete mode 100644 thefuck/history.py diff --git a/README.md b/README.md index 63abba4..a2630bf 100644 --- a/README.md +++ b/README.md @@ -102,14 +102,20 @@ sudo pip install thefuck [Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation) -And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX): +And add to `.bashrc` or `.bash_profile`(for OSX): ```bash -alias fuck='eval $(thefuck $(fc -ln -1))' +alias fuck='eval $(thefuck $(fc -ln -1)); history -r' # You can use whatever you want as an alias, like for Mondays: alias FUCK='fuck' ``` +Or in your `.zshrc`: + +```bash +alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R' +``` + Alternatively, you can redirect the output of `thefuck-alias`: ```bash diff --git a/tests/test_history.py b/tests/test_history.py deleted file mode 100644 index 12af890..0000000 --- a/tests/test_history.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from mock import Mock -from thefuck.history import History - - -class TestHistory(object): - @pytest.fixture(autouse=True) - def process(self, monkeypatch): - Process = Mock() - Process.return_value.parent.return_value.pid = 1 - monkeypatch.setattr('thefuck.history.Process', Process) - return Process - - @pytest.fixture(autouse=True) - def db(self, monkeypatch): - class DBMock(dict): - def __init__(self): - super(DBMock, self).__init__() - self.sync = Mock() - - def __call__(self, *args, **kwargs): - return self - - db = DBMock() - monkeypatch.setattr('thefuck.history.shelve.open', db) - return db - - def test_set(self, db): - history = History() - history.update(last_command='ls', - last_fixed_command=None) - assert db == {'1-last_command': 'ls', - '1-last_fixed_command': None} - - def test_get(self, db): - history = History() - db['1-last_command'] = 'cd ..' - assert history.last_command == 'cd ..' - - def test_get_without_value(self): - history = History() - assert history.last_command is None diff --git a/tests/test_main.py b/tests/test_main.py index a871c76..681b616 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -56,7 +56,7 @@ class TestGetCommand(object): monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x) def test_get_command_calls(self, Popen): - assert main.get_command(Mock(), Mock(), + assert main.get_command(Mock(), ['thefuck', 'apt-get', 'search', 'vim']) \ == Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', @@ -64,22 +64,14 @@ class TestGetCommand(object): stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) - - @pytest.mark.parametrize('history, args, result', [ - (Mock(), [''], None), - (Mock(last_command='ls', last_fixed_command='ls -la'), - ['thefuck', 'fuck'], 'ls -la'), - (Mock(last_command='ls', last_fixed_command='ls -la'), - ['thefuck', 'ls'], 'ls -la'), - (Mock(last_command='ls', last_fixed_command=''), - ['thefuck', 'ls'], 'ls'), - (Mock(last_command='ls', last_fixed_command=''), - ['thefuck', 'fuck'], 'ls')]) - def test_get_command_script(self, history, args, result): + @pytest.mark.parametrize('args, result', [ + (['thefuck', 'ls', '-la'], 'ls -la'), + (['thefuck', 'ls'], 'ls')]) + def test_get_command_script(self, args, result): if result: - assert main.get_command(Mock(), history, args).script == result + assert main.get_command(Mock(), args).script == result else: - assert main.get_command(Mock(), history, args) is None + assert main.get_command(Mock(), args) is None class TestGetMatchedRule(object): @@ -109,7 +101,7 @@ class TestRunRule(object): def test_run_rule(self, capsys): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - Command(), Mock(), None) + Command(), None) assert capsys.readouterr() == ('new-command\n', '') def test_run_rule_with_side_effect(self, capsys): @@ -118,21 +110,14 @@ class TestRunRule(object): command = Command() main.run_rule(Rule(get_new_command=lambda *_: 'new-command', side_effect=side_effect), - command, Mock(), settings) + command, settings) assert capsys.readouterr() == ('new-command\n', '') side_effect.assert_called_once_with(command, settings) - def test_hisotry_updated(self): - history = Mock() - main.run_rule(Rule(get_new_command=lambda *_: 'ls -lah'), - Command('ls'), history, None) - history.update.assert_called_once_with(last_command='ls', - last_fixed_command='ls -lah') - def test_when_not_comfirmed(self, capsys, confirm): confirm.return_value = False main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - Command(), Mock(), None) + Command(), None) assert capsys.readouterr() == ('', '') diff --git a/tests/test_shells.py b/tests/test_shells.py index c24112b..5b2748e 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -1,16 +1,35 @@ import pytest -from mock import Mock +from mock import Mock, MagicMock from thefuck import shells +@pytest.fixture +def builtins_open(monkeypatch): + mock = MagicMock() + monkeypatch.setattr('six.moves.builtins.open', mock) + return mock + + +@pytest.fixture +def isfile(monkeypatch): + mock = Mock(return_value=True) + monkeypatch.setattr('os.path.isfile', mock) + return mock + + class TestGeneric(object): def test_from_shell(self): assert shells.Generic().from_shell('pwd') == 'pwd' def test_to_shell(self): - assert shells.Bash().to_shell('pwd') == 'pwd' + assert shells.Generic().to_shell('pwd') == 'pwd' + + def test_put_to_history(self, builtins_open): + assert shells.Generic().put_to_history('ls') is None + assert builtins_open.call_count == 0 +@pytest.mark.usefixtures('isfile') class TestBash(object): @pytest.fixture(autouse=True) def Popen(self, monkeypatch): @@ -31,7 +50,13 @@ class TestBash(object): def test_to_shell(self): assert shells.Bash().to_shell('pwd') == 'pwd' + def test_put_to_history(self, builtins_open): + shells.Bash().put_to_history('ls') + builtins_open.return_value.__enter__.return_value.\ + write.assert_called_once_with('ls\n') + +@pytest.mark.usefixtures('isfile') class TestZsh(object): @pytest.fixture(autouse=True) def Popen(self, monkeypatch): @@ -50,4 +75,11 @@ class TestZsh(object): assert shells.Zsh().from_shell(before) == after def test_to_shell(self): - assert shells.Zsh().to_shell('pwd') == 'pwd' \ No newline at end of file + assert shells.Zsh().to_shell('pwd') == 'pwd' + + def test_put_to_history(self, builtins_open, monkeypatch): + monkeypatch.setattr('thefuck.shells.time', + lambda: 1430707243.3517463) + shells.Zsh().put_to_history('ls') + builtins_open.return_value.__enter__.return_value. \ + write.assert_called_once_with(': 1430707243:0;ls\n') \ No newline at end of file diff --git a/thefuck/history.py b/thefuck/history.py deleted file mode 100644 index 86a2441..0000000 --- a/thefuck/history.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import shelve -from tempfile import gettempdir -from psutil import Process - - -class History(object): - """Temporary history of commands/fixed-commands dependent on - current shell instance. - - """ - - def __init__(self): - self._path = os.path.join(gettempdir(), '.thefuck_history') - self._pid = Process(os.getpid()).parent().pid - self._db = shelve.open(self._path) - - def _prepare_key(self, key): - return '{}-{}'.format(self._pid, key) - - def update(self, **kwargs): - self._db.update({self._prepare_key(k): v for k,v in kwargs.items()}) - self._db.sync() - return self - - def __getattr__(self, item): - return self._db.get(self._prepare_key(item)) diff --git a/thefuck/main.py b/thefuck/main.py index 0a62d45..56e28d1 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -6,7 +6,7 @@ import os import sys from psutil import Process, TimeoutExpired import colorama -from .history import History +import six from . import logs, conf, types, shells @@ -60,22 +60,17 @@ def wait_output(settings, popen): return False -def get_command(settings, history, args): +def get_command(settings, args): """Creates command from `args` and executes it.""" - if sys.version_info[0] < 3: + if six.PY2: script = ' '.join(arg.decode('utf-8') for arg in args[1:]) else: script = ' '.join(args[1:]) - if script == 'fuck' or script == history.last_command: - script = history.last_fixed_command or history.last_command - if not script: return script = shells.from_shell(script) - history.update(last_command=script, - last_fixed_command=None) result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=dict(os.environ, LANG='C')) if wait_output(settings, result): @@ -108,14 +103,13 @@ def confirm(new_command, side_effect, settings): return False -def run_rule(rule, command, history, settings): +def run_rule(rule, command, settings): """Runs command from rule for passed command.""" new_command = shells.to_shell(rule.get_new_command(command, settings)) if confirm(new_command, rule.side_effect, settings): if rule.side_effect: rule.side_effect(command, settings) - history.update(last_command=command.script, - last_fixed_command=new_command) + shells.put_to_history(new_command) print(new_command) @@ -127,14 +121,13 @@ def main(): colorama.init() user_dir = setup_user_dir() settings = conf.get_settings(user_dir) - history = History() - command = get_command(settings, history, sys.argv) + command = get_command(settings, sys.argv) if command: rules = get_rules(user_dir, settings) matched_rule = get_matched_rule(command, rules, settings) if matched_rule: - run_rule(matched_rule, command, history, settings) + run_rule(matched_rule, command, settings) return logs.failed('No fuck given', settings) diff --git a/thefuck/shells.py b/thefuck/shells.py index a9bc7cb..c0bb089 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -1,9 +1,11 @@ """Module with shell specific actions, each shell class should -implement `from_shell` and `to_shell` methods. +implement `from_shell`, `to_shell`, `app_alias` and `put_to_history` +methods. """ from collections import defaultdict from subprocess import Popen, PIPE +from time import time import os from psutil import Process @@ -34,6 +36,19 @@ class Generic(object): def app_alias(self): return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" + def _get_history_file_name(self): + return '' + + def _get_history_line(self, command_script): + return '' + + def put_to_history(self, command_script): + """Puts command script to shell history.""" + history_file_name = self._get_history_file_name() + if os.path.isfile(history_file_name): + with open(history_file_name, 'a') as history: + history.write(self._get_history_line(command_script)) + class Bash(Generic): def _parse_alias(self, alias): @@ -49,6 +64,13 @@ class Bash(Generic): for alias in proc.stdout.read().decode('utf-8').split('\n') if alias) + def _get_history_file_name(self): + return os.environ.get("HISTFILE", + os.path.expanduser('~/.bash_history')) + + def _get_history_line(self, command_script): + return u'{}\n'.format(command_script) + class Zsh(Generic): def _parse_alias(self, alias): @@ -64,6 +86,13 @@ class Zsh(Generic): for alias in proc.stdout.read().decode('utf-8').split('\n') if alias) + def _get_history_file_name(self): + return os.environ.get("HISTFILE", + os.path.expanduser('~/.zsh_history')) + + def _get_history_line(self, command_script): + return u': {}:0;{}\n'.format(int(time()), command_script) + shells = defaultdict(lambda: Generic(), { 'bash': Bash(), @@ -85,3 +114,7 @@ def to_shell(command): def app_alias(): return _get_shell().app_alias() + + +def put_to_history(command): + return _get_shell().put_to_history(command) From 80bfbec4220fb3bc5a5c13be3dd237d159f77539 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 4 May 2015 05:00:11 +0200 Subject: [PATCH 080/107] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a2630bf..b0830ff 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck) +**Aliases changed in 1.34.** + Magnificent app which corrects your previous console command, inspired by a [@liamosaur](https://twitter.com/liamosaur/) [tweet](https://twitter.com/liamosaur/status/506975850596536320). From a8ff2375c0b4fd99ed1bcb79edd33377461ce79e Mon Sep 17 00:00:00 2001 From: nvbn Date: Mon, 4 May 2015 05:01:56 +0200 Subject: [PATCH 081/107] Bump to 1.35 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 57e3009..5c39e93 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.34' +VERSION = '1.35' setup(name='thefuck', From 6624ecb3b85e82e9f1a08823f6e41ee805d35a9e Mon Sep 17 00:00:00 2001 From: mcarton Date: Tue, 5 May 2015 11:13:29 +0200 Subject: [PATCH 082/107] Add a rule for pacman --- README.md | 1 + thefuck/rules/pacman.py | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 thefuck/rules/pacman.py diff --git a/README.md b/README.md index b0830ff..54dd882 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ using matched rule and run it. Rules enabled by default: * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; +* `pacman` – installs app with `pacman` or `yaourt` if it is not installed; * `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `sl_ls` – changes `sl` to `ls`; diff --git a/thefuck/rules/pacman.py b/thefuck/rules/pacman.py new file mode 100644 index 0000000..f709895 --- /dev/null +++ b/thefuck/rules/pacman.py @@ -0,0 +1,42 @@ +import subprocess + + +def __command_available(command): + try: + subprocess.check_output([command], stderr=subprocess.DEVNULL) + return True + except subprocess.CalledProcessError: + # command exists but is not happy to be called without any argument + return True + except FileNotFoundError: + return False + + +def __get_pkgfile(command): + try: + return subprocess.check_output( + ['pkgfile', '-b', '-v', command.script.split(" ")[0]], + universal_newlines=True, stderr=subprocess.DEVNULL + ).split() + except subprocess.CalledProcessError: + return None + + +def match(command, settings): + return 'not found' in command.stderr and __get_pkgfile(command) + + +def get_new_command(command, settings): + package = __get_pkgfile(command)[0] + + return '{} -S {} && {}'.format(pacman, package, command.script) + + +if not __command_available('pkgfile'): + enabled_by_default = False +elif __command_available('yaourt'): + pacman = 'yaourt' +elif __command_available('pacman'): + pacman = 'sudo pacman' +else: + enabled_by_default = False From 9380eb1f56fda20547c88afd7a5b82f82fa120d9 Mon Sep 17 00:00:00 2001 From: mcarton Date: Wed, 6 May 2015 11:31:31 +0200 Subject: [PATCH 083/107] Add a git_add rule --- README.md | 1 + thefuck/rules/git_add.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 thefuck/rules/git_add.py diff --git a/README.md b/README.md index 54dd882..d7ed591 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ using matched rule and run it. Rules enabled by default: * `cd_mkdir` – creates directories before cd'ing into them; * `cp_omitting_directory` – adds `-a` when you `cp` directory; * `fix_alt_space` – replaces Alt+Space with Space character; +* `git_add` – fix *"Did you forget to 'git add'?"*; * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `has_exists_script` – prepends `./` when script/binary exists; diff --git a/thefuck/rules/git_add.py b/thefuck/rules/git_add.py new file mode 100644 index 0000000..66c7f1d --- /dev/null +++ b/thefuck/rules/git_add.py @@ -0,0 +1,15 @@ +import re + + +def match(command, settings): + return ('git' in command.script + and 'did not match any file(s) known to git.' in command.stderr + and "Did you forget to 'git add'?" in command.stderr) + + +def get_new_command(command, settings): + missing_file = re.findall( + r"error: pathspec '([^']*)' " + "did not match any file\(s\) known to git.", command.stderr)[0] + + return 'git add -- {} && {}'.format(missing_file, command.script) From 5864faadeff34bad0b2b15d904802707dd68fbfd Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 6 May 2015 13:17:14 +0200 Subject: [PATCH 084/107] #165 fix python 2 support --- thefuck/rules/pacman.py | 5 +++-- thefuck/shells.py | 8 +++----- thefuck/utils.py | 3 +++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/thefuck/rules/pacman.py b/thefuck/rules/pacman.py index f709895..4115798 100644 --- a/thefuck/rules/pacman.py +++ b/thefuck/rules/pacman.py @@ -1,14 +1,15 @@ import subprocess +from thefuck.utils import DEVNULL def __command_available(command): try: - subprocess.check_output([command], stderr=subprocess.DEVNULL) + subprocess.check_output([command], stderr=DEVNULL) return True except subprocess.CalledProcessError: # command exists but is not happy to be called without any argument return True - except FileNotFoundError: + except OSError: return False diff --git a/thefuck/shells.py b/thefuck/shells.py index c0bb089..1d128cd 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -8,9 +8,7 @@ from subprocess import Popen, PIPE from time import time import os from psutil import Process - - -FNULL = open(os.devnull, 'w') +from .utils import DEVNULL class Generic(object): @@ -58,7 +56,7 @@ class Bash(Generic): return name, value def _get_aliases(self): - proc = Popen('bash -ic alias', stdout=PIPE, stderr=FNULL, shell=True) + proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) return dict( self._parse_alias(alias) for alias in proc.stdout.read().decode('utf-8').split('\n') @@ -80,7 +78,7 @@ class Zsh(Generic): return name, value def _get_aliases(self): - proc = Popen('zsh -ic alias', stdout=PIPE, stderr=FNULL, shell=True) + proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) return dict( self._parse_alias(alias) for alias in proc.stdout.read().decode('utf-8').split('\n') diff --git a/thefuck/utils.py b/thefuck/utils.py index 7ee66c2..3247111 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -4,6 +4,9 @@ import six from .types import Command +DEVNULL = open(os.devnull, 'w') + + def which(program): """Returns `program` path or `None`.""" From fc3fcf028ac480bab2ab6a9f58f4accce5faf084 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 6 May 2015 13:57:09 +0200 Subject: [PATCH 085/107] #154 Add priority to rules --- README.md | 2 ++ tests/test_main.py | 50 ++++++++++++++++++++++++++++++---------------- tests/utils.py | 7 +++++-- thefuck/conf.py | 1 + thefuck/main.py | 19 ++++++++++++------ thefuck/types.py | 3 ++- 6 files changed, 56 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index d7ed591..07ec8bb 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ enabled_by_default = True def side_effect(command, settings): subprocess.call('chmod 777 .', shell=True) + +priority = 1000 # Lower first ``` [More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules), diff --git a/tests/test_main.py b/tests/test_main.py index 681b616..de75051 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -12,28 +12,43 @@ def test_load_rule(monkeypatch): load_source = Mock() load_source.return_value = Mock(match=match, get_new_command=get_new_command, - enabled_by_default=True) + enabled_by_default=True, + priority=900) monkeypatch.setattr('thefuck.main.load_source', load_source) assert main.load_rule(Path('/rules/bash.py')) \ - == Rule('bash', match, get_new_command) + == Rule('bash', match, get_new_command, priority=900) load_source.assert_called_once_with('bash', '/rules/bash.py') -@pytest.mark.parametrize('conf_rules, rules', [ - (conf.DEFAULT_RULES, [Rule('bash', 'bash', 'bash'), - Rule('lisp', 'lisp', 'lisp'), - Rule('bash', 'bash', 'bash'), - Rule('lisp', 'lisp', 'lisp')]), - (types.RulesNamesList(['bash']), [Rule('bash', 'bash', 'bash'), - Rule('bash', 'bash', 'bash')])]) -def test_get_rules(monkeypatch, conf_rules, rules): - monkeypatch.setattr( - 'thefuck.main.Path.glob', - lambda *_: [PosixPath('bash.py'), PosixPath('lisp.py')]) - monkeypatch.setattr('thefuck.main.load_source', - lambda x, _: Mock(match=x, get_new_command=x, - enabled_by_default=True)) - assert list(main.get_rules(Path('~'), Mock(rules=conf_rules))) == rules +class TestGetRules(object): + @pytest.fixture(autouse=True) + def glob(self, monkeypatch): + mock = Mock(return_value=[]) + monkeypatch.setattr('thefuck.main.Path.glob', mock) + return mock + + def _compare_names(self, rules, names): + return [r.name for r in rules] == names + + @pytest.mark.parametrize('conf_rules, rules', [ + (conf.DEFAULT_RULES, ['bash', 'lisp', 'bash', 'lisp']), + (types.RulesNamesList(['bash']), ['bash', 'bash'])]) + def test_get(self, monkeypatch, glob, conf_rules, rules): + glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] + monkeypatch.setattr('thefuck.main.load_source', + lambda x, _: Rule(x)) + assert self._compare_names( + main.get_rules(Path('~'), Mock(rules=conf_rules)), rules) + + @pytest.mark.parametrize('unordered, ordered', [ + ([Rule('bash', priority=100), Rule('python', priority=5)], + ['python', 'bash']), + ([Rule('lisp', priority=9999), Rule('c', priority=conf.DEFAULT_PRIORITY)], + ['c', 'lisp'])]) + def test_ordered_by_priority(self, monkeypatch, unordered, ordered): + monkeypatch.setattr('thefuck.main._get_loaded_rules', + lambda *_: unordered) + assert self._compare_names(main.get_rules(Path('~'), Mock()), ordered) class TestGetCommand(object): @@ -64,6 +79,7 @@ class TestGetCommand(object): stdout=PIPE, stderr=PIPE, env={'LANG': 'C'}) + @pytest.mark.parametrize('args, result', [ (['thefuck', 'ls', '-la'], 'ls -la'), (['thefuck', 'ls'], 'ls')]) diff --git a/tests/utils.py b/tests/utils.py index 02e19e7..4641971 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,5 @@ from thefuck import types +from thefuck.conf import DEFAULT_PRIORITY def Command(script='', stdout='', stderr=''): @@ -8,6 +9,8 @@ def Command(script='', stdout='', stderr=''): def Rule(name='', match=lambda *_: True, get_new_command=lambda *_: '', enabled_by_default=True, - side_effect=None): + side_effect=None, + priority=DEFAULT_PRIORITY): return types.Rule(name, match, get_new_command, - enabled_by_default, side_effect) + enabled_by_default, side_effect, + priority) diff --git a/thefuck/conf.py b/thefuck/conf.py index d33b1e3..4bc01e7 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -22,6 +22,7 @@ class _DefaultRulesNames(types.RulesNamesList): DEFAULT_RULES = _DefaultRulesNames([]) +DEFAULT_PRIORITY = 1000 DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, diff --git a/thefuck/main.py b/thefuck/main.py index 56e28d1..434d7eb 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -26,7 +26,17 @@ def load_rule(rule): return types.Rule(rule.name[:-3], rule_module.match, rule_module.get_new_command, getattr(rule_module, 'enabled_by_default', True), - getattr(rule_module, 'side_effect', None)) + getattr(rule_module, 'side_effect', None), + getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY)) + + +def _get_loaded_rules(rules, settings): + """Yields all available rules.""" + for rule in rules: + if rule.name != '__init__.py': + loaded_rule = load_rule(rule) + if loaded_rule in settings.rules: + yield loaded_rule def get_rules(user_dir, settings): @@ -35,11 +45,8 @@ def get_rules(user_dir, settings): .joinpath('rules') \ .glob('*.py') user = user_dir.joinpath('rules').glob('*.py') - for rule in sorted(list(bundled)) + list(user): - if rule.name != '__init__.py': - loaded_rule = load_rule(rule) - if loaded_rule in settings.rules: - yield loaded_rule + rules = _get_loaded_rules(sorted(bundled) + sorted(user), settings) + return sorted(rules, key=lambda rule: rule.priority) def wait_output(settings, popen): diff --git a/thefuck/types.py b/thefuck/types.py index 221b0e9..3ca2cf8 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -4,7 +4,8 @@ from collections import namedtuple Command = namedtuple('Command', ('script', 'stdout', 'stderr')) Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', - 'enabled_by_default', 'side_effect')) + 'enabled_by_default', 'side_effect', + 'priority')) class RulesNamesList(list): From 5bf14246138d03c7c044e7a5720964223597cad3 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 12:57:43 +0200 Subject: [PATCH 086/107] #164 Decrease priority of `no_command` --- thefuck/rules/no_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/thefuck/rules/no_command.py b/thefuck/rules/no_command.py index 917e1fc..1a152c9 100644 --- a/thefuck/rules/no_command.py +++ b/thefuck/rules/no_command.py @@ -31,3 +31,6 @@ def get_new_command(command, settings): new_command = get_close_matches(old_command, _get_all_bins())[0] return ' '.join([new_command] + command.script.split(' ')[1:]) + + +priority = 3000 From 05f594b918d2bf6b718754218af652286da35ebb Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 13:11:45 +0200 Subject: [PATCH 087/107] #154 Add ability to override priority in settings --- README.md | 9 +++++++-- tests/test_conf.py | 8 ++++++-- tests/test_main.py | 22 +++++++++++++++------- thefuck/conf.py | 27 +++++++++++++++++++++------ thefuck/main.py | 3 ++- 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 07ec8bb..ee52858 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,8 @@ The Fuck has a few settings parameters, they can be changed in `~/.thefuck/setti * `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`; * `require_confirmation` – require confirmation before running new command, by default `False`; * `wait_command` – max amount of time in seconds for getting previous command output; -* `no_colors` – disable colored output. +* `no_colors` – disable colored output; +* `priority` – dict with rules priorities, rule with lower `priority` will be matched first. Example of `settings.py`: @@ -222,6 +223,7 @@ rules = ['sudo', 'no_command'] require_confirmation = True wait_command = 10 no_colors = False +priority = {'sudo': 100, 'no_command': 9999} ``` Or via environment variables: @@ -229,7 +231,9 @@ Or via environment variables: * `THEFUCK_RULES` – list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`; * `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`; * `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output; -* `THEFUCK_NO_COLORS` – disable colored output, `true/false`. +* `THEFUCK_NO_COLORS` – disable colored output, `true/false`; +* `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`, +rule with lower `priority` will be matched first. For example: @@ -238,6 +242,7 @@ export THEFUCK_RULES='sudo:no_command' export THEFUCK_REQUIRE_CONFIRMATION='true' export THEFUCK_WAIT_COMMAND=10 export THEFUCK_NO_COLORS='false' +export THEFUCK_PRIORITY='no_command=9999:apt_get=100' ``` ## Developing diff --git a/tests/test_conf.py b/tests/test_conf.py index c5a8333..3363a5e 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -40,12 +40,14 @@ class TestSettingsFromFile(object): load_source.return_value = Mock(rules=['test'], wait_command=10, require_confirmation=True, - no_colors=True) + no_colors=True, + priority={'vim': 100}) settings = conf.get_settings(Mock()) assert settings.rules == ['test'] assert settings.wait_command == 10 assert settings.require_confirmation is True assert settings.no_colors is True + assert settings.priority == {'vim': 100} def test_from_file_with_DEFAULT(self, load_source): load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'], @@ -62,12 +64,14 @@ class TestSettingsFromEnv(object): environ.update({'THEFUCK_RULES': 'bash:lisp', 'THEFUCK_WAIT_COMMAND': '55', 'THEFUCK_REQUIRE_CONFIRMATION': 'true', - 'THEFUCK_NO_COLORS': 'false'}) + 'THEFUCK_NO_COLORS': 'false', + 'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15'}) settings = conf.get_settings(Mock()) assert settings.rules == ['bash', 'lisp'] assert settings.wait_command == 55 assert settings.require_confirmation is True assert settings.no_colors is False + assert settings.priority == {'bash': 10, 'vim': 15} def test_from_env_with_DEFAULT(self, environ): environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) diff --git a/tests/test_main.py b/tests/test_main.py index de75051..44b4bee 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -38,17 +38,25 @@ class TestGetRules(object): monkeypatch.setattr('thefuck.main.load_source', lambda x, _: Rule(x)) assert self._compare_names( - main.get_rules(Path('~'), Mock(rules=conf_rules)), rules) + main.get_rules(Path('~'), Mock(rules=conf_rules, priority={})), + rules) - @pytest.mark.parametrize('unordered, ordered', [ - ([Rule('bash', priority=100), Rule('python', priority=5)], + @pytest.mark.parametrize('priority, unordered, ordered', [ + ({}, + [Rule('bash', priority=100), Rule('python', priority=5)], ['python', 'bash']), - ([Rule('lisp', priority=9999), Rule('c', priority=conf.DEFAULT_PRIORITY)], - ['c', 'lisp'])]) - def test_ordered_by_priority(self, monkeypatch, unordered, ordered): + ({}, + [Rule('lisp', priority=9999), Rule('c', priority=conf.DEFAULT_PRIORITY)], + ['c', 'lisp']), + ({'python': 9999}, + [Rule('bash', priority=100), Rule('python', priority=5)], + ['bash', 'python'])]) + def test_ordered_by_priority(self, monkeypatch, priority, unordered, ordered): monkeypatch.setattr('thefuck.main._get_loaded_rules', lambda *_: unordered) - assert self._compare_names(main.get_rules(Path('~'), Mock()), ordered) + assert self._compare_names( + main.get_rules(Path('~'), Mock(priority=priority)), + ordered) class TestGetCommand(object): diff --git a/thefuck/conf.py b/thefuck/conf.py index 4bc01e7..916d216 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -28,12 +28,14 @@ DEFAULT_PRIORITY = 1000 DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'wait_command': 3, 'require_confirmation': False, - 'no_colors': False} + 'no_colors': False, + 'priority': {}} ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', 'THEFUCK_WAIT_COMMAND': 'wait_command', 'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation', - 'THEFUCK_NO_COLORS': 'no_colors'} + 'THEFUCK_NO_COLORS': 'no_colors', + 'THEFUCK_PRIORITY': 'priority'} SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file @@ -66,16 +68,29 @@ def _rules_from_env(val): return val +def _priority_from_env(val): + """Gets priority pairs from env.""" + for part in val.split(':'): + try: + rule, priority = part.split('=') + yield rule, int(priority) + except ValueError: + continue + + def _val_from_env(env, attr): """Transforms env-strings to python.""" val = os.environ[env] if attr == 'rules': - val = _rules_from_env(val) + return _rules_from_env(val) + elif attr == 'priority': + return dict(_priority_from_env(val)) elif attr == 'wait_command': - val = int(val) + return int(val) elif attr in ('require_confirmation', 'no_colors'): - val = val.lower() == 'true' - return val + return val.lower() == 'true' + else: + return val def _settings_from_env(): diff --git a/thefuck/main.py b/thefuck/main.py index 434d7eb..27a0095 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -46,7 +46,8 @@ def get_rules(user_dir, settings): .glob('*.py') user = user_dir.joinpath('rules').glob('*.py') rules = _get_loaded_rules(sorted(bundled) + sorted(user), settings) - return sorted(rules, key=lambda rule: rule.priority) + return sorted(rules, key=lambda rule: settings.priority.get( + rule.name, rule.priority)) def wait_output(settings, popen): From f3d377114e8110d445a0bcbbbf38f2606008ddb9 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 13:12:25 +0200 Subject: [PATCH 088/107] Bump to 1.36 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5c39e93..8b7216c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.35' +VERSION = '1.36' setup(name='thefuck', From 91c1fe414a9ecfe26768211da62c2999611145fe Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 13:32:23 +0200 Subject: [PATCH 089/107] Update `thefuck-alias` entry point --- setup.py | 3 ++- thefuck/main.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 8b7216c..b5ae687 100644 --- a/setup.py +++ b/setup.py @@ -17,4 +17,5 @@ setup(name='thefuck', zip_safe=False, install_requires=['pathlib', 'psutil', 'colorama', 'six'], entry_points={'console_scripts': [ - 'thefuck = thefuck.main:main', 'thefuck-alias = thefuck.main:alias']}) + 'thefuck = thefuck.main:main', + 'thefuck-alias = thefuck.shells:app_alias']}) diff --git a/thefuck/main.py b/thefuck/main.py index 27a0095..5772d4e 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -121,10 +121,6 @@ def run_rule(rule, command, settings): print(new_command) -def alias(): - print(shells.app_alias()) - - def main(): colorama.init() user_dir = setup_user_dir() From 2b12b4bfced02d4f646e8dcdc046bf18e854e434 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 13:42:52 +0200 Subject: [PATCH 090/107] Improve tests with `mocker` --- requirements.txt | 1 + tests/test_conf.py | 6 ++---- tests/test_main.py | 32 +++++++++++++------------------- tests/test_shells.py | 31 ++++++++++++------------------- 4 files changed, 28 insertions(+), 42 deletions(-) diff --git a/requirements.txt b/requirements.txt index 625bed6..1a6161b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pytest mock +pytest-mock diff --git a/tests/test_conf.py b/tests/test_conf.py index 3363a5e..1fde6a6 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -14,10 +14,8 @@ def test_default(enabled, rules, result): @pytest.fixture -def load_source(monkeypatch): - mock = Mock() - monkeypatch.setattr('thefuck.conf.load_source', mock) - return mock +def load_source(mocker): + return mocker.patch('thefuck.conf.load_source') @pytest.fixture diff --git a/tests/test_main.py b/tests/test_main.py index 44b4bee..fe82cfa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,15 +6,15 @@ from thefuck import main, conf, types from tests.utils import Rule, Command -def test_load_rule(monkeypatch): +def test_load_rule(mocker): match = object() get_new_command = object() - load_source = Mock() - load_source.return_value = Mock(match=match, - get_new_command=get_new_command, - enabled_by_default=True, - priority=900) - monkeypatch.setattr('thefuck.main.load_source', load_source) + load_source = mocker.patch( + 'thefuck.main.load_source', + return_value=Mock(match=match, + get_new_command=get_new_command, + enabled_by_default=True, + priority=900)) assert main.load_rule(Path('/rules/bash.py')) \ == Rule('bash', match, get_new_command, priority=900) load_source.assert_called_once_with('bash', '/rules/bash.py') @@ -22,10 +22,8 @@ def test_load_rule(monkeypatch): class TestGetRules(object): @pytest.fixture(autouse=True) - def glob(self, monkeypatch): - mock = Mock(return_value=[]) - monkeypatch.setattr('thefuck.main.Path.glob', mock) - return mock + def glob(self, mocker): + return mocker.patch('thefuck.main.Path.glob', return_value=[]) def _compare_names(self, rules, names): return [r.name for r in rules] == names @@ -118,10 +116,8 @@ class TestGetMatchedRule(object): class TestRunRule(object): @pytest.fixture(autouse=True) - def confirm(self, monkeypatch): - mock = Mock(return_value=True) - monkeypatch.setattr('thefuck.main.confirm', mock) - return mock + def confirm(self, mocker): + return mocker.patch('thefuck.main.confirm', return_value=True) def test_run_rule(self, capsys): main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), @@ -147,10 +143,8 @@ class TestRunRule(object): class TestConfirm(object): @pytest.fixture - def stdin(self, monkeypatch): - mock = Mock(return_value='\n') - monkeypatch.setattr('sys.stdin.read', mock) - return mock + def stdin(self, mocker): + return mocker.patch('sys.stdin.read', return_value='\n') def test_when_not_required(self, capsys): assert main.confirm('command', None, Mock(require_confirmation=False)) diff --git a/tests/test_shells.py b/tests/test_shells.py index 5b2748e..b00cfab 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -1,20 +1,15 @@ import pytest -from mock import Mock, MagicMock from thefuck import shells @pytest.fixture -def builtins_open(monkeypatch): - mock = MagicMock() - monkeypatch.setattr('six.moves.builtins.open', mock) - return mock +def builtins_open(mocker): + return mocker.patch('six.moves.builtins.open') @pytest.fixture -def isfile(monkeypatch): - mock = Mock(return_value=True) - monkeypatch.setattr('os.path.isfile', mock) - return mock +def isfile(mocker): + return mocker.patch('os.path.isfile', return_value=True) class TestGeneric(object): @@ -32,9 +27,8 @@ class TestGeneric(object): @pytest.mark.usefixtures('isfile') class TestBash(object): @pytest.fixture(autouse=True) - def Popen(self, monkeypatch): - mock = Mock() - monkeypatch.setattr('thefuck.shells.Popen', mock) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.Popen') mock.return_value.stdout.read.return_value = ( b'alias l=\'ls -CF\'\n' b'alias la=\'ls -A\'\n' @@ -52,16 +46,15 @@ class TestBash(object): def test_put_to_history(self, builtins_open): shells.Bash().put_to_history('ls') - builtins_open.return_value.__enter__.return_value.\ + builtins_open.return_value.__enter__.return_value. \ write.assert_called_once_with('ls\n') @pytest.mark.usefixtures('isfile') class TestZsh(object): @pytest.fixture(autouse=True) - def Popen(self, monkeypatch): - mock = Mock() - monkeypatch.setattr('thefuck.shells.Popen', mock) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.Popen') mock.return_value.stdout.read.return_value = ( b'l=\'ls -CF\'\n' b'la=\'ls -A\'\n' @@ -77,9 +70,9 @@ class TestZsh(object): def test_to_shell(self): assert shells.Zsh().to_shell('pwd') == 'pwd' - def test_put_to_history(self, builtins_open, monkeypatch): - monkeypatch.setattr('thefuck.shells.time', - lambda: 1430707243.3517463) + def test_put_to_history(self, builtins_open, mocker): + mocker.patch('thefuck.shells.time', + return_value=1430707243.3517463) shells.Zsh().put_to_history('ls') builtins_open.return_value.__enter__.return_value. \ write.assert_called_once_with(': 1430707243:0;ls\n') \ No newline at end of file From 36d80859a4d3e02c161db7d1600734ea9d1eacc1 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 13:51:27 +0200 Subject: [PATCH 091/107] Add tox config --- tests/test_shells.py | 2 +- tox.ini | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 tox.ini diff --git a/tests/test_shells.py b/tests/test_shells.py index b00cfab..449496c 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -75,4 +75,4 @@ class TestZsh(object): return_value=1430707243.3517463) shells.Zsh().put_to_history('ls') builtins_open.return_value.__enter__.return_value. \ - write.assert_called_once_with(': 1430707243:0;ls\n') \ No newline at end of file + write.assert_called_once_with(': 1430707243:0;ls\n') diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..46cf1c1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py27,py33,py34 + +[testenv] +deps = -rrequirements.txt +commands = py.test From 0cdd23edcffe4b49f72ce7bdd8df695c946ea99c Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 14:16:07 +0200 Subject: [PATCH 092/107] Use wheel --- release.py | 2 +- requirements.txt | 1 + setup.cfg | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 setup.cfg diff --git a/release.py b/release.py index e22209b..85af928 100755 --- a/release.py +++ b/release.py @@ -28,4 +28,4 @@ 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) +call('python setup.py sdist bdist_wheel upload', shell=True) diff --git a/requirements.txt b/requirements.txt index 1a6161b..78b43ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pytest mock pytest-mock +wheel diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 From 29e70e14a0e08907e79ebee1a90bf0fcbfb6b1ec Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 7 May 2015 14:16:17 +0200 Subject: [PATCH 093/107] Bump to 1.37 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b5ae687..ab09bf9 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.36' +VERSION = '1.37' setup(name='thefuck', From a395ac568c7d5ccc0627e5cb7b43306359a9a612 Mon Sep 17 00:00:00 2001 From: mcarton Date: Thu, 7 May 2015 20:32:04 +0200 Subject: [PATCH 094/107] Add the git_checkout rule It creates a branch before checking-out to it if the branch does not exist. --- README.md | 1 + thefuck/rules/git_checkout.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 thefuck/rules/git_checkout.py diff --git a/README.md b/README.md index ee52858..252ff7b 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ using matched rule and run it. Rules enabled by default: * `cp_omitting_directory` – adds `-a` when you `cp` directory; * `fix_alt_space` – replaces Alt+Space with Space character; * `git_add` – fix *"Did you forget to 'git add'?"*; +* `git_checkout` – creates the branch before checking-out; * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `has_exists_script` – prepends `./` when script/binary exists; diff --git a/thefuck/rules/git_checkout.py b/thefuck/rules/git_checkout.py new file mode 100644 index 0000000..271562b --- /dev/null +++ b/thefuck/rules/git_checkout.py @@ -0,0 +1,15 @@ +import re + + +def match(command, settings): + return ('git' in command.script + and 'did not match any file(s) known to git.' in command.stderr + and "Did you forget to 'git add'?" not in command.stderr) + + +def get_new_command(command, settings): + missing_file = re.findall( + r"error: pathspec '([^']*)' " + "did not match any file\(s\) known to git.", command.stderr)[0] + + return 'git branch {} && {}'.format(missing_file, command.script) From 793e8830739f719351bb6da808916eac86014afe Mon Sep 17 00:00:00 2001 From: SanketDG Date: Fri, 8 May 2015 00:15:32 +0530 Subject: [PATCH 095/107] add man_no_space command --- thefuck/rules/man_no_space.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 thefuck/rules/man_no_space.py diff --git a/thefuck/rules/man_no_space.py b/thefuck/rules/man_no_space.py new file mode 100644 index 0000000..1751352 --- /dev/null +++ b/thefuck/rules/man_no_space.py @@ -0,0 +1,9 @@ +def match(command, settings): + return (command.script.startswith(u'man') + and u'command not found' in command.stderr.lower()) + + +def get_new_command(command, settings): + return u'man {}'.format(command.script[3:]) + +priority = 2000 From 65aeea857ef3e4934e25f5eb2675abe4520a7d77 Mon Sep 17 00:00:00 2001 From: SanketDG Date: Fri, 8 May 2015 00:15:57 +0530 Subject: [PATCH 096/107] add tests for man_no_space --- tests/rules/test_man_no_space.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/rules/test_man_no_space.py diff --git a/tests/rules/test_man_no_space.py b/tests/rules/test_man_no_space.py new file mode 100644 index 0000000..669ebb8 --- /dev/null +++ b/tests/rules/test_man_no_space.py @@ -0,0 +1,12 @@ +from thefuck.rules.man_no_space import match, get_new_command +from tests.utils import Command + + +def test_match(): + assert match(Command('mandiff', stderr='mandiff: command not found'), None) + assert not match(Command(), None) + + +def test_get_new_command(): + assert get_new_command( + Command('mandiff'), None) == 'man diff' From 045959ec47d8124e04a9e49f52cd79f109622968 Mon Sep 17 00:00:00 2001 From: SanketDG Date: Fri, 8 May 2015 00:16:50 +0530 Subject: [PATCH 097/107] add man_no_space --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee52858..4fb1bad 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ using matched rule and run it. Rules enabled by default: * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; +* `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `pacman` – installs app with `pacman` or `yaourt` if it is not installed; * `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; @@ -193,13 +194,13 @@ def match(command, settings): def get_new_command(command, settings): return 'sudo {}'.format(command.script) - + # Optional: enabled_by_default = True def side_effect(command, settings): subprocess.call('chmod 777 .', shell=True) - + priority = 1000 # Lower first ``` From 932a7c5db5e63735ba5e3dcc77d24a95284810bd Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 8 May 2015 01:46:00 +0200 Subject: [PATCH 098/107] Add a don't repeat yourself rule --- README.md | 1 + thefuck/rules/dry.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 thefuck/rules/dry.py diff --git a/README.md b/README.md index ee52858..e903d76 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ using matched rule and run it. Rules enabled by default: * `cd_parent` – changes `cd..` to `cd ..`; * `cd_mkdir` – creates directories before cd'ing into them; * `cp_omitting_directory` – adds `-a` when you `cp` directory; +* `dry` – fix repetitions like "git git push"; * `fix_alt_space` – replaces Alt+Space with Space character; * `git_add` – fix *"Did you forget to 'git add'?"*; * `git_no_command` – fixes wrong git commands like `git brnch`; diff --git a/thefuck/rules/dry.py b/thefuck/rules/dry.py new file mode 100644 index 0000000..86759b3 --- /dev/null +++ b/thefuck/rules/dry.py @@ -0,0 +1,12 @@ +def match(command, settings): + split_command = command.script.split() + + return len(split_command) >= 2 and split_command[0] == split_command[1] + + +def get_new_command(command, settings): + return command.script[command.script.find(' '):] + +# it should be rare enough to actually have to type twice the same word, so +# this rule can have a higher priority to come before things like "cd cd foo" +priority = 900 From 56f636f3d8efbabb0942dd751f567484c67f3fbe Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 8 May 2015 11:41:26 +0200 Subject: [PATCH 099/107] Remove unnecessary space in the DRY rule --- thefuck/rules/dry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/rules/dry.py b/thefuck/rules/dry.py index 86759b3..f0954ea 100644 --- a/thefuck/rules/dry.py +++ b/thefuck/rules/dry.py @@ -5,7 +5,7 @@ def match(command, settings): def get_new_command(command, settings): - return command.script[command.script.find(' '):] + return command.script[command.script.find(' ')+1:] # it should be rare enough to actually have to type twice the same word, so # this rule can have a higher priority to come before things like "cd cd foo" From 95007220fbe4c5554a92b2b688fb9724ec949ebd Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 8 May 2015 11:42:00 +0200 Subject: [PATCH 100/107] Add a test for the DRY rule --- tests/rules/test_dry.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/rules/test_dry.py diff --git a/tests/rules/test_dry.py b/tests/rules/test_dry.py new file mode 100644 index 0000000..757866d --- /dev/null +++ b/tests/rules/test_dry.py @@ -0,0 +1,17 @@ +import pytest +from thefuck.rules.dry import match, get_new_command +from tests.utils import Command + + +@pytest.mark.parametrize('command', [ + Command(script='cd cd foo'), + Command(script='git git push origin/master')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('cd cd foo'), 'cd foo'), + (Command('git git push origin/master'), 'git push origin/master')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command From f082ba829fadd26e5528338a0ada15cd3ce14404 Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 8 May 2015 15:27:33 +0200 Subject: [PATCH 101/107] Bump to 1.38 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ab09bf9..494c523 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.37' +VERSION = '1.38' setup(name='thefuck', From 690729d5a15ebb105ca9fadb383549e3c8d0356f Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 9 May 2015 18:53:36 +0200 Subject: [PATCH 102/107] #176 Fix fails with wrong aliases --- thefuck/shells.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index 1d128cd..e1203f4 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -60,7 +60,7 @@ class Bash(Generic): return dict( self._parse_alias(alias) for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias) + if alias and '=' in alias) def _get_history_file_name(self): return os.environ.get("HISTFILE", @@ -82,7 +82,7 @@ class Zsh(Generic): return dict( self._parse_alias(alias) for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias) + if alias and '=' in alias) def _get_history_file_name(self): return os.environ.get("HISTFILE", From f477cd69c250a4d4a9f5fb714c719f0170aacac8 Mon Sep 17 00:00:00 2001 From: nvbn Date: Sat, 9 May 2015 18:53:49 +0200 Subject: [PATCH 103/107] Bump to 1.39 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 494c523..c79654f 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.38' +VERSION = '1.39' setup(name='thefuck', From 20f1c76d2716392f53159acf4f935dcbf356096d Mon Sep 17 00:00:00 2001 From: archilius777 Date: Sat, 9 May 2015 22:56:35 +0530 Subject: [PATCH 104/107] Fixed grammar in README.txt --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73509ca..13fec69 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ REPL-y 0.3.1 ... ``` -If you are scared to blindly run changed command, there's `require_confirmation` +If you are scared to blindly run the changed command, there is a `require_confirmation` [settings](#settings) option: ```bash @@ -104,7 +104,7 @@ sudo pip install thefuck [Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation) -And add to `.bashrc` or `.bash_profile`(for OSX): +And add to the `.bashrc` or `.bash_profile`(for OSX): ```bash alias fuck='eval $(thefuck $(fc -ln -1)); history -r' @@ -137,8 +137,8 @@ sudo pip install thefuck --upgrade ## How it works -The Fuck tries to match rule for the previous command, create new command -using matched rule and run it. Rules enabled by default: +The Fuck tries to match a rule for the previous command, creates a new command +using the matched rule and runs it. Rules enabled by default are as follows: * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `cd_parent` – changes `cd..` to `cd ..`; @@ -211,10 +211,10 @@ priority = 1000 # Lower first ## Settings -The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`: +The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`: * `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`; -* `require_confirmation` – require confirmation before running new command, by default `False`; +* `require_confirmation` – requires confirmation before running new command, by default `False`; * `wait_command` – max amount of time in seconds for getting previous command output; * `no_colors` – disable colored output; * `priority` – dict with rules priorities, rule with lower `priority` will be matched first. From c749615ad6184976e2bbdf537209a446e01c7eab Mon Sep 17 00:00:00 2001 From: mcarton Date: Sat, 9 May 2015 20:37:13 +0200 Subject: [PATCH 105/107] Add a `C++11` rule --- thefuck/rules/c++11.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 thefuck/rules/c++11.py diff --git a/thefuck/rules/c++11.py b/thefuck/rules/c++11.py new file mode 100644 index 0000000..f4d8ab5 --- /dev/null +++ b/thefuck/rules/c++11.py @@ -0,0 +1,11 @@ +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 + ) + ) + + +def get_new_command(command, settings): + return command.script + ' -std=c++11' From 8930d0160128bc224e6857d03b0775bf1c8d4de1 Mon Sep 17 00:00:00 2001 From: mcarton Date: Sat, 9 May 2015 20:42:18 +0200 Subject: [PATCH 106/107] Update README.md to add the C++11 rule --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e903d76..f173283 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ The Fuck tries to match rule for the previous command, create new command using matched rule and run it. Rules enabled by default: * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; +* `c++11` – add missing `-std=c++11` to `g++` or `clang++`; * `cd_parent` – changes `cd..` to `cd ..`; * `cd_mkdir` – creates directories before cd'ing into them; * `cp_omitting_directory` – adds `-a` when you `cp` directory; From 6cdc2c27fb98b2989ae34c9c508ca30a5e3459ef Mon Sep 17 00:00:00 2001 From: nvbn Date: Sun, 10 May 2015 09:35:02 +0200 Subject: [PATCH 107/107] #179 /c++1/cpp11/s --- README.md | 2 +- thefuck/rules/{c++11.py => cpp11.py} | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) rename thefuck/rules/{c++11.py => cpp11.py} (80%) diff --git a/README.md b/README.md index 24d9e76..75f06d9 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ The Fuck tries to match rule for the previous command, create new command using matched rule and run it. Rules enabled by default: * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; -* `c++11` – add missing `-std=c++11` to `g++` or `clang++`; +* `cpp11` – add missing `-std=c++11` to `g++` or `clang++`; * `cd_parent` – changes `cd..` to `cd ..`; * `cd_mkdir` – creates directories before cd'ing into them; * `cp_omitting_directory` – adds `-a` when you `cp` directory; diff --git a/thefuck/rules/c++11.py b/thefuck/rules/cpp11.py similarity index 80% rename from thefuck/rules/c++11.py rename to thefuck/rules/cpp11.py index f4d8ab5..154abab 100644 --- a/thefuck/rules/c++11.py +++ b/thefuck/rules/cpp11.py @@ -2,9 +2,7 @@ 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 - ) - ) + '-Wc++11-extensions' in command.stderr)) def get_new_command(command, settings):