diff --git a/README.md b/README.md index 5fa401e7..35fba969 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,12 @@ eval $(thefuck --alias) eval $(thefuck --alias FUCK) ``` +If you want to enable the experimental smart rule, place this command in your `.bash_profile`, `.bashrc`, `.zshrc` or other startup script: + +```bash +eval $(thefuck --enable-experimental-shell-history) +``` + [Or in your shell config (Bash, Zsh, Fish, Powershell, tcsh).](https://github.com/nvbn/thefuck/wiki/Shell-aliases) Changes are only available in a new shell session. To make changes immediately @@ -321,6 +327,7 @@ default: * `git_push_force` – adds `--force-with-lease` to a `git push` (may conflict with `git_push_pull`); * `rm_root` – adds `--no-preserve-root` to `rm -rf /` command. +* `smart_rule`; returns recommended commands based on user corrected commands. ## Creating your own rules diff --git a/tests/rules/test_smart_rule.py b/tests/rules/test_smart_rule.py new file mode 100644 index 00000000..b0fce2df --- /dev/null +++ b/tests/rules/test_smart_rule.py @@ -0,0 +1,34 @@ +import pytest +from mock import patch, MagicMock +from thefuck.rules.smart_rule import match, get_new_command +from thefuck.types import Command + + +def test_match_simple(): + assert match('') + + +@pytest.mark.parametrize('script, socket_response, new_command', [ + ('git push', [b'\x03', b'\x25', b'git push --set-upstream origin master', b'\x16', + b'git push origin master', b'\x17', b'git push origin develop'], + ['git push --set-upstream origin master', 'git push origin master', 'git push origin develop']), + ('ls', [b'\x01', b'\x06', b'ls -la'], ['ls -la']) +]) +@patch('thefuck.rules.smart_rule.socket') +def test_get_new_command(socket_mock, script, socket_response, new_command): + sock_mock = MagicMock() + recv_mock = MagicMock(side_effect=socket_response) + socket_mock.socket.return_value = sock_mock + sock_mock.recv = recv_mock + returned_commands = get_new_command(Command(script, None)) + assert returned_commands == new_command + + +@patch('thefuck.rules.smart_rule.socket') +def test_socket_open_close_connect(socket_mock): + sock_mock = MagicMock() + socket_mock.socket.return_value = sock_mock + get_new_command('') + socket_mock.socket.assert_called_once() + sock_mock.connect.assert_called_once() + sock_mock.close.assert_called_once() diff --git a/tests/test_argument_parser.py b/tests/test_argument_parser.py index e5f1f44b..1ce5d93d 100644 --- a/tests/test_argument_parser.py +++ b/tests/test_argument_parser.py @@ -8,6 +8,7 @@ def _args(**override): 'help': False, 'version': False, 'debug': False, 'force_command': None, 'repeat': False, 'enable_experimental_instant_mode': False, + 'enable_experimental_shell_history': False, 'shell_logger': None} args.update(override) return args @@ -18,6 +19,8 @@ def _args(**override): (['thefuck', '-a'], _args(alias='fuck')), (['thefuck', '--alias', '--enable-experimental-instant-mode'], _args(alias='fuck', enable_experimental_instant_mode=True)), + (['thefuck', '--enable-experimental-shell-history'], + _args(enable_experimental_shell_history=True)), (['thefuck', '-a', 'fix'], _args(alias='fix')), (['thefuck', 'git', 'branch', ARGUMENT_PLACEHOLDER, '-y'], _args(command=['git', 'branch'], yes=True)), diff --git a/thefuck/argument_parser.py b/thefuck/argument_parser.py index b7b97837..fdc1fc3c 100644 --- a/thefuck/argument_parser.py +++ b/thefuck/argument_parser.py @@ -33,6 +33,10 @@ class Parser(object): '--enable-experimental-instant-mode', action='store_true', help='enable experimental instant mode, use on your own risk') + self._parser.add_argument( + '--enable-experimental-shell-history', + action='store_true', + help='enable experimental shell history') self._parser.add_argument( '-h', '--help', action='store_true', diff --git a/thefuck/conf.py b/thefuck/conf.py index 1f69faf8..1e5ccfa3 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -19,6 +19,7 @@ class Settings(dict): from .logs import exception self._setup_user_dir() + self._setup_data_dir() self._init_settings_file() try: @@ -64,6 +65,18 @@ class Settings(dict): rules_dir.mkdir(parents=True) self.user_dir = user_dir + def _get_user_data_path(self): + """Return Path object representing the user local resource""" + xdg_config_home = os.environ.get('XDG_DATA_HOME', '~/.local/share') + user_dir = Path(xdg_config_home, 'thefuck').expanduser() + return user_dir + + def _setup_data_dir(self): + data_dir = self._get_user_data_path() + if not data_dir.is_dir(): + data_dir.mkdir(parents=True) + self.data_dir = data_dir + def _settings_from_file(self): """Loads settings from file.""" settings = load_source( diff --git a/thefuck/const.py b/thefuck/const.py index 2009d499..76136fa1 100644 --- a/thefuck/const.py +++ b/thefuck/const.py @@ -28,6 +28,12 @@ ALL_ENABLED = _GenConst('All rules enabled') DEFAULT_RULES = [ALL_ENABLED] DEFAULT_PRIORITY = 1000 +SHELL_LOGGER_SOCKET_ENV_VAR = '__SHELL_LOGGER_SOCKET' +SHELL_LOGGER_DB_ENV_VAR = '__SHELL_LOGGER_DB_PATH' +SHELL_LOGGER_DB_FILENAME = 'my.db' +SHELL_LOGGER_SOCKET_PATH = '/tmp/tf_socket' +SHELL_LOGGER_BINARY_FILENAME = 'shell_logger' + DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'exclude_rules': [], 'wait_command': 3, diff --git a/thefuck/entrypoints/alias.py b/thefuck/entrypoints/alias.py index 4bcda8b7..c13f4f21 100644 --- a/thefuck/entrypoints/alias.py +++ b/thefuck/entrypoints/alias.py @@ -1,5 +1,17 @@ +import subprocess +import urllib.request + +import os +import sys import six -from ..logs import warn +import psutil +from psutil import ZombieProcess + +from ..conf import settings +from ..system import get_shell_logger_bname_from_sys +from ..const import SHELL_LOGGER_SOCKET_ENV_VAR, SHELL_LOGGER_SOCKET_PATH, \ + SHELL_LOGGER_BINARY_FILENAME, SHELL_LOGGER_DB_FILENAME, SHELL_LOGGER_DB_ENV_VAR +from ..logs import warn, debug from ..shells import shell from ..utils import which @@ -24,3 +36,30 @@ def _get_alias(known_args): def print_alias(known_args): print(_get_alias(known_args)) + + +def print_experimental_shell_history(known_args): + settings.init(known_args) + + filename_suffix = get_shell_logger_bname_from_sys() + client_release = 'https://github.com/nvbn/shell_logger/releases/download/0.1.0a1/shell_logger_{}'\ + .format(filename_suffix) + binary_path = '{}/{}'.format(settings.data_dir, SHELL_LOGGER_BINARY_FILENAME) + db_path = '{}/{}'.format(settings.data_dir, SHELL_LOGGER_DB_FILENAME) + + debug('Downloading the shell_logger release and putting it in the path ... ') + urllib.request.urlretrieve(client_release, binary_path) + + subprocess.Popen(['chmod', '+x', binary_path]) + + my_env = os.environ.copy() + my_env[SHELL_LOGGER_DB_ENV_VAR] = db_path + proc = subprocess.Popen([binary_path, '-mode', 'configure'], stdout=subprocess.PIPE, + env=my_env) + print(''.join([line.decode() for line in proc.stdout.readlines()])) + # TODO seems like daemon returns something, so redirect stdout so eval doesn't hang + subprocess.Popen(["{} -mode daemon &".format(binary_path)], shell=True, + env={SHELL_LOGGER_SOCKET_ENV_VAR: SHELL_LOGGER_SOCKET_PATH, + SHELL_LOGGER_DB_ENV_VAR: db_path}, stdout=2) + + diff --git a/thefuck/entrypoints/main.py b/thefuck/entrypoints/main.py index 63f9d8c9..0b08a5c7 100644 --- a/thefuck/entrypoints/main.py +++ b/thefuck/entrypoints/main.py @@ -8,7 +8,7 @@ import sys # noqa: E402 from .. import logs # noqa: E402 from ..argument_parser import Parser # noqa: E402 from ..utils import get_installation_info # noqa: E402 -from .alias import print_alias # noqa: E402 +from .alias import print_alias, print_experimental_shell_history # noqa: E402 from .fix_command import fix_command # noqa: E402 @@ -32,5 +32,8 @@ def main(): logs.warn('Shell logger supports only Linux and macOS') else: shell_logger(known_args.shell_logger) + elif known_args.enable_experimental_shell_history: + print_experimental_shell_history(known_args) else: parser.print_usage() + diff --git a/thefuck/rules/smart_rule.py b/thefuck/rules/smart_rule.py new file mode 100644 index 00000000..093727ac --- /dev/null +++ b/thefuck/rules/smart_rule.py @@ -0,0 +1,28 @@ +import sys +import socket + +from thefuck.logs import debug +from thefuck.const import SHELL_LOGGER_SOCKET_PATH + + +def match(command): + return True + + +def get_new_command(command): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + list_of_commands = [] + try: + sock.connect(SHELL_LOGGER_SOCKET_PATH) + sock.send(command.script.encode()) + number_of_strings = int.from_bytes(sock.recv(1), sys.byteorder) + for i in range(number_of_strings): + length_of_string = int.from_bytes(sock.recv(1), sys.byteorder) + list_of_commands.append(sock.recv(length_of_string).decode()) + finally: + sock.close() + return list_of_commands + + +# Make last priority +priority = 10000 diff --git a/thefuck/system/__init__.py b/thefuck/system/__init__.py index bb13c30b..3764f8ca 100644 --- a/thefuck/system/__init__.py +++ b/thefuck/system/__init__.py @@ -5,3 +5,14 @@ if sys.platform == 'win32': from .win32 import * # noqa: F401,F403 else: from .unix import * # noqa: F401,F403 + + +def get_shell_logger_bname_from_sys(): + """Return the binary name associated with the current system""" + platform = sys.platform + if "darwin" in platform: + return "darwin64" + elif "linux" in platform: + return "linux64" + else: + return "windows64.exe"