From 2379573cf236f335112e299be997ae81ca6a19cd Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 13 Mar 2017 19:05:34 +0100 Subject: [PATCH] #591: Add `path_from_history` rule --- README.md | 1 + tests/rules/test_path_from_history.py | 43 ++++++++++++++++++++++ thefuck/rules/path_from_history.py | 53 +++++++++++++++++++++++++++ thefuck/utils.py | 4 +- 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 tests/rules/test_path_from_history.py create mode 100644 thefuck/rules/path_from_history.py diff --git a/README.md b/README.md index 564435be..52e4a90e 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `python_execute` – appends missing `.py` when executing Python files; * `quotation_marks` – fixes uneven usage of `'` and `"` when containing args'; +* `path_from_history` – replaces not found path with similar absolute path from history; * `react_native_command_unrecognized` – fixes unrecognized `react-native` commands; * `remove_trailing_cedilla` – remove trailling cedillas `รง`, a common typo for european keyboard layouts; * `rm_dir` – adds `-rf` when you trying to remove directory; diff --git a/tests/rules/test_path_from_history.py b/tests/rules/test_path_from_history.py new file mode 100644 index 00000000..9b14ca73 --- /dev/null +++ b/tests/rules/test_path_from_history.py @@ -0,0 +1,43 @@ +import pytest +from thefuck.rules.path_from_history import match, get_new_command +from tests.utils import Command + + +@pytest.fixture(autouse=True) +def history(mocker): + return mocker.patch( + 'thefuck.rules.path_from_history.get_valid_history_without_current', + return_value=['cd /opt/java', 'ls ~/work/project/']) + + +@pytest.fixture(autouse=True) +def path_exists(mocker): + path_mock = mocker.patch('thefuck.rules.path_from_history.Path') + exists_mock = path_mock.return_value.expanduser.return_value.exists + exists_mock.return_value = True + return exists_mock + + +@pytest.mark.parametrize('script, stderr', [ + ('ls project', 'no such file or directory: project'), + ('cd project', "can't cd to project"), +]) +def test_match(script, stderr): + assert match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, stderr', [ + ('myapp cats', 'no such file or directory: project'), + ('cd project', ""), +]) +def test_not_match(script, stderr): + assert not match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, stderr, result', [ + ('ls project', 'no such file or directory: project', 'ls ~/work/project'), + ('cd java', "can't cd to java", 'cd /opt/java'), +]) +def test_get_new_command(script, stderr, result): + new_command = get_new_command(Command(script, stderr=stderr)) + assert new_command[0] == result diff --git a/thefuck/rules/path_from_history.py b/thefuck/rules/path_from_history.py new file mode 100644 index 00000000..a5ea4adc --- /dev/null +++ b/thefuck/rules/path_from_history.py @@ -0,0 +1,53 @@ +from collections import Counter +import re +from thefuck.system import Path +from thefuck.utils import (get_valid_history_without_current, + memoize, replace_argument) +from thefuck.shells import shell + + +patterns = [r'no such file or directory: (.*)$', + r"cannot access '(.*)': No such file or directory", + r': (.*): No such file or directory', + r"can't cd to (.*)$"] + + +@memoize +def _get_destination(command): + for pattern in patterns: + found = re.findall(pattern, command.stderr) + if found: + if found[0] in command.script_parts: + return found[0] + + +def match(command): + return bool(_get_destination(command)) + + +def _get_all_absolute_paths_from_history(command): + counter = Counter() + + for line in get_valid_history_without_current(command): + splitted = shell.split_command(line) + + for param in splitted[1:]: + if param.startswith('/') or param.startswith('~'): + if param.endswith('/'): + param = param[:-1] + + counter[param] += 1 + + return (path for path, _ in counter.most_common(None)) + + +def get_new_command(command): + destination = _get_destination(command) + paths = _get_all_absolute_paths_from_history(command) + + return [replace_argument(command.script, destination, path) + for path in paths if path.endswith(destination) + and Path(path).expanduser().exists()] + + +priority = 800 diff --git a/thefuck/utils.py b/thefuck/utils.py index 29ea2486..1663f0d1 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -268,7 +268,9 @@ def get_valid_history_without_current(command): from thefuck.shells import shell history = shell.get_history() tf_alias = get_alias() - executables = set(get_all_executables()) + executables = set(get_all_executables())\ + .union(shell.get_builtin_commands()) + return [line for line in _not_corrected(history, tf_alias) if not line.startswith(tf_alias) and not line == command.script and line.split(' ')[0] in executables]