From e4b97af73e04c9a936ac8a43fbce874deb3adf70 Mon Sep 17 00:00:00 2001 From: mcarton Date: Wed, 29 Jul 2015 16:05:46 +0200 Subject: [PATCH 1/3] #320 Add the `fix_file` rule --- README.md | 1 + tests/rules/test_fix_file.py | 167 +++++++++++++++++++++++++++++++++++ thefuck/rules/fix_file.py | 61 +++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 tests/rules/test_fix_file.py create mode 100644 thefuck/rules/fix_file.py diff --git a/README.md b/README.md index 8a6c8c1a..c73b12f0 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `docker_not_command` – fixes wrong docker commands like `docker tags`; * `dry` – fixes repetitions like `git git push`; * `fix_alt_space` – replaces Alt+Space with Space character; +* `fix_file` – opens a file with an error in your `$EDITOR`; * `git_add` – fixes *"Did you forget to 'git add'?"*; * `git_branch_delete` – changes `git branch -d` to `git branch -D`; * `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch; diff --git a/tests/rules/test_fix_file.py b/tests/rules/test_fix_file.py new file mode 100644 index 00000000..c9411b42 --- /dev/null +++ b/tests/rules/test_fix_file.py @@ -0,0 +1,167 @@ +import pytest +from thefuck.rules.fix_file import match, get_new_command +from tests.utils import Command + + +# (script, file, line, col (or None), stderr) +tests = ( +('gcc a.c', 'a.c', 3, 1, +""" +a.c: In function 'main': +a.c:3:1: error: expected expression before '}' token + } + ^ +"""), + +('clang a.c', 'a.c', 3, 1, +""" +a.c:3:1: error: expected expression +} +^ +"""), + +('perl a.pl', 'a.pl', 3, None, +""" +syntax error at a.pl line 3, at EOF +Execution of a.pl aborted due to compilation errors. +"""), + +('perl a.pl', 'a.pl', 2, None, +""" +Search pattern not terminated at a.pl line 2. +"""), + +('sh a.sh', 'a.sh', 2, None, +""" +a.sh: line 2: foo: command not found +"""), + +('zsh a.sh', 'a.sh', 2, None, +""" +a.sh:2: command not found: foo +"""), + +('bash a.sh', 'a.sh', 2, None, +""" +a.sh: line 2: foo: command not found +"""), + +('rustc a.rs', 'a.rs', 2, 5, +""" +a.rs:2:5: 2:6 error: unexpected token: `+` +a.rs:2 + + ^ +"""), + +('cargo build', 'src/lib.rs', 3, 5, +""" + Compiling test v0.1.0 (file:///tmp/fix-error/test) + src/lib.rs:3:5: 3:6 error: unexpected token: `+` + src/lib.rs:3 + + ^ +Could not compile `test`. + +To learn more, run the command again with --verbose. +"""), + +('python a.py', 'a.py', 2, None, +""" + File "a.py", line 2 + + + ^ +SyntaxError: invalid syntax +"""), + +('python a.py', 'a.py', 8, None, +""" +Traceback (most recent call last): + File "a.py", line 8, in + match("foo") + File "a.py", line 5, in match + m = re.search(None, command) + File "/usr/lib/python3.4/re.py", line 170, in search + return _compile(pattern, flags).search(string) + File "/usr/lib/python3.4/re.py", line 293, in _compile + raise TypeError("first argument must be string or compiled pattern") +TypeError: first argument must be string or compiled pattern +""" +), + +('ruby a.rb', 'a.rb', 3, None, +""" +a.rb:3: syntax error, unexpected keyword_end +"""), + +('lua a.lua', 'a.lua', 2, None, +""" +lua: a.lua:2: unexpected symbol near '+' +"""), + +('fish a.sh', '/tmp/fix-error/a.sh', 2, None, +""" +fish: Unknown command 'foo' +/tmp/fix-error/a.sh (line 2): foo + ^ +"""), + +('./a', './a', 2, None, +""" +awk: ./a:2: BEGIN { print "Hello, world!" + } +awk: ./a:2: ^ syntax error +"""), + +('llc a.ll', 'a.ll', 1, None, +""" +llc: a.ll:1:1: error: expected top-level entity ++ +^ +"""), + +('go build a.go', 'a.go', 1, None, +""" +can't load package: +a.go:1:1: expected 'package', found '+' +"""), + +('make', 'Makefile', 2, None, +""" +bidule +make: bidule: Command not found +Makefile:2: recipe for target 'target' failed +make: *** [target] Error 127 +"""), + +('git st', '/home/martin/.config/git/config', 1, None, +""" +fatal: bad config file line 1 in /home/martin/.config/git/config +"""), + +('node fuck.js asdf qwer', '/Users/pablo/Workspace/barebones/fuck.js', '2', 5, +""" +/Users/pablo/Workspace/barebones/fuck.js:2 +conole.log(arg); // this should read console.log(arg); +^ +ReferenceError: conole is not defined + at /Users/pablo/Workspace/barebones/fuck.js:2:5 + at Array.forEach (native) + at Object. (/Users/pablo/Workspace/barebones/fuck.js:1:85) + at Module._compile (module.js:460:26) + at Object.Module._extensions..js (module.js:478:10) + at Module.load (module.js:355:32) + at Function.Module._load (module.js:310:12) + at Function.Module.runMain (module.js:501:10) + at startup (node.js:129:16) + at node.js:814:3 +"""), +) + + +@pytest.mark.parametrize('test', tests) +def test_match(test): + assert match(Command(stderr=test[4]), None) + + +@pytest.mark.parametrize('test', tests) +def test_get_new_command(monkeypatch, test): + assert (get_new_command(Command(script=test[0], stderr=test[4]), None) == + '$EDITOR {} +{} && {}'.format(test[1], test[2], test[0])) diff --git a/thefuck/rules/fix_file.py b/thefuck/rules/fix_file.py new file mode 100644 index 00000000..4b7626bc --- /dev/null +++ b/thefuck/rules/fix_file.py @@ -0,0 +1,61 @@ +import re +from thefuck.utils import memoize +from thefuck import shells + + +patterns = ( + # js, node: + '^ at {file}:{line}:{col}', + # cargo: + '^ {file}:{line}:{col}', + # python, thefuck: + '^ File "{file}", line {line}', + # awk: + '^awk: {file}:{line}:', + # git + '^fatal: bad config file line {line} in {file}', + # llc: + '^llc: {file}:{line}:{col}:', + # lua: + '^lua: {file}:{line}:', + # fish: + '^{file} \(line {line}\):', + # bash, sh, ssh: + '^{file}: line {line}: ', + # ghc, make, ruby, zsh: + '^{file}:{line}:', + # cargo, clang, gcc, go, rustc: + '^{file}:{line}:{col}', + # perl: + 'at {file} line {line}', + ) + + +# for the sake of readability do not use named groups above +def _make_pattern(pattern): + pattern = pattern.replace('{file}', '(?P[^:\n]+)') + pattern = pattern.replace('{line}', '(?P[0-9]+)') + pattern = pattern.replace('{col}', '(?P[0-9]+)') + return re.compile(pattern, re.MULTILINE) +patterns = [_make_pattern(p) for p in patterns] + + +@memoize +def _search(stderr): + for pattern in patterns: + m = re.search(pattern, stderr) + if m: + return m + + +def match(command, settings): + return _search(command.stderr) + + +def get_new_command(command, settings): + m = _search(command.stderr) + + # Note: there does not seem to be a standard for columns, so they are just + # ignored for now + return shells.and_('$EDITOR {} +{}'.format(m.group('file'), m.group('line')), + command.script) From de513cacb150049e3f95434f8d6d30b7ed1e0ea7 Mon Sep 17 00:00:00 2001 From: mcarton Date: Wed, 29 Jul 2015 18:35:59 +0200 Subject: [PATCH 2/3] Show user's $EDITOR in output It looks nicer with confirmation and also checks the user actually has an $EDITOR. --- tests/rules/test_fix_file.py | 15 +++++++++++++-- thefuck/rules/fix_file.py | 5 +++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/rules/test_fix_file.py b/tests/rules/test_fix_file.py index c9411b42..2eb42cab 100644 --- a/tests/rules/test_fix_file.py +++ b/tests/rules/test_fix_file.py @@ -1,4 +1,5 @@ import pytest +import os from thefuck.rules.fix_file import match, get_new_command from tests.utils import Command @@ -157,11 +158,21 @@ ReferenceError: conole is not defined @pytest.mark.parametrize('test', tests) -def test_match(test): +def test_match(monkeypatch, test): + monkeypatch.setenv('EDITOR', 'dummy_editor') assert match(Command(stderr=test[4]), None) +@pytest.mark.parametrize('test', tests) +def test_not_match(monkeypatch, test): + if 'EDITOR' in os.environ: + monkeypatch.delenv('EDITOR') + + assert not match(Command(stderr=test[4]), None) + + @pytest.mark.parametrize('test', tests) def test_get_new_command(monkeypatch, test): + monkeypatch.setenv('EDITOR', 'dummy_editor') assert (get_new_command(Command(script=test[0], stderr=test[4]), None) == - '$EDITOR {} +{} && {}'.format(test[1], test[2], test[0])) + 'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0])) diff --git a/thefuck/rules/fix_file.py b/thefuck/rules/fix_file.py index 4b7626bc..5f4aafb9 100644 --- a/thefuck/rules/fix_file.py +++ b/thefuck/rules/fix_file.py @@ -1,4 +1,5 @@ import re +import os from thefuck.utils import memoize from thefuck import shells @@ -49,7 +50,7 @@ def _search(stderr): def match(command, settings): - return _search(command.stderr) + return 'EDITOR' in os.environ and _search(command.stderr) def get_new_command(command, settings): @@ -57,5 +58,5 @@ def get_new_command(command, settings): # Note: there does not seem to be a standard for columns, so they are just # ignored for now - return shells.and_('$EDITOR {} +{}'.format(m.group('file'), m.group('line')), + return shells.and_('{} {} +{}'.format(os.environ['EDITOR'], m.group('file'), m.group('line')), command.script) From 43fead02d3a24fef71534116c5550def0f56830c Mon Sep 17 00:00:00 2001 From: mcarton Date: Wed, 29 Jul 2015 21:35:49 +0200 Subject: [PATCH 3/3] Test if the file exists in the `fix_file` rule This avoid false positives in `match`. --- tests/rules/test_fix_file.py | 14 ++++++++++++-- thefuck/rules/fix_file.py | 7 ++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/rules/test_fix_file.py b/tests/rules/test_fix_file.py index 2eb42cab..83ac2738 100644 --- a/tests/rules/test_fix_file.py +++ b/tests/rules/test_fix_file.py @@ -158,19 +158,29 @@ ReferenceError: conole is not defined @pytest.mark.parametrize('test', tests) -def test_match(monkeypatch, test): +def test_match(mocker, monkeypatch, test): + mocker.patch('os.path.isfile', return_value=True) monkeypatch.setenv('EDITOR', 'dummy_editor') assert match(Command(stderr=test[4]), None) @pytest.mark.parametrize('test', tests) -def test_not_match(monkeypatch, test): +def test_no_editor(mocker, monkeypatch, test): + mocker.patch('os.path.isfile', return_value=True) if 'EDITOR' in os.environ: monkeypatch.delenv('EDITOR') assert not match(Command(stderr=test[4]), None) +@pytest.mark.parametrize('test', tests) +def test_not_file(mocker, monkeypatch, test): + mocker.patch('os.path.isfile', return_value=False) + monkeypatch.setenv('EDITOR', 'dummy_editor') + + assert not match(Command(stderr=test[4]), None) + + @pytest.mark.parametrize('test', tests) def test_get_new_command(monkeypatch, test): monkeypatch.setenv('EDITOR', 'dummy_editor') diff --git a/thefuck/rules/fix_file.py b/thefuck/rules/fix_file.py index 5f4aafb9..28b17ba1 100644 --- a/thefuck/rules/fix_file.py +++ b/thefuck/rules/fix_file.py @@ -50,7 +50,12 @@ def _search(stderr): def match(command, settings): - return 'EDITOR' in os.environ and _search(command.stderr) + if 'EDITOR' not in os.environ: + return False + + m = _search(command.stderr) + + return m and os.path.isfile(m.group('file')) def get_new_command(command, settings):