From ec37998a1048dfb34d29bf90900d6f8ff5f88d8a Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Tue, 28 Mar 2017 11:31:06 +0200 Subject: [PATCH] #620: Add support of arguments to `fuck`, like `fuck -y` on zsh --- tests/shells/test_zsh.py | 12 ++++----- tests/test_argument_parser.py | 26 +++++++++++++++++++ thefuck/argument_parser.py | 48 +++++++++++++++++++++++++++++++++++ thefuck/conf.py | 11 +++++++- thefuck/const.py | 2 ++ thefuck/logs.py | 6 +++++ thefuck/main.py | 46 +++++++++++---------------------- thefuck/shells/zsh.py | 27 ++++++++++++-------- 8 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 tests/test_argument_parser.py create mode 100644 thefuck/argument_parser.py diff --git a/tests/shells/test_zsh.py b/tests/shells/test_zsh.py index 43893a09..378b8e22 100644 --- a/tests/shells/test_zsh.py +++ b/tests/shells/test_zsh.py @@ -40,17 +40,17 @@ class TestZsh(object): 'll': 'ls -alF'} def test_app_alias(self, shell): - assert 'alias fuck' in shell.app_alias('fuck') - assert 'alias FUCK' in shell.app_alias('FUCK') + assert 'fuck () {' in shell.app_alias('fuck') + assert 'FUCK () {' in shell.app_alias('FUCK') assert 'thefuck' in shell.app_alias('fuck') assert 'PYTHONIOENCODING' in shell.app_alias('fuck') def test_app_alias_variables_correctly_set(self, shell): alias = shell.app_alias('fuck') - assert "alias fuck='TF_CMD=$(TF_ALIAS" in alias - assert '$(TF_ALIAS=fuck PYTHONIOENCODING' in alias - assert 'PYTHONIOENCODING=utf-8 TF_SHELL_ALIASES' in alias - assert 'ALIASES=$(alias) thefuck' in alias + assert "fuck () {" in alias + assert "TF_ALIAS=fuck" in alias + assert 'PYTHONIOENCODING=utf-8' in alias + assert 'TF_SHELL_ALIASES=$(alias)' in alias def test_get_history(self, history_lines, shell): history_lines([': 1432613911:0;ls', ': 1432613916:0;rm']) diff --git a/tests/test_argument_parser.py b/tests/test_argument_parser.py new file mode 100644 index 00000000..35c754f6 --- /dev/null +++ b/tests/test_argument_parser.py @@ -0,0 +1,26 @@ +import pytest +from thefuck.argument_parser import Parser +from thefuck.const import ARGUMENT_PLACEHOLDER + + +@pytest.mark.parametrize('argv, result', [ + (['thefuck'], {'alias': None, 'command': [], 'yes': False, + 'help': False, 'version': False}), + (['thefuck', '-a'], {'alias': 'fuck', 'command': [], 'yes': False, + 'help': False, 'version': False}), + (['thefuck', '-a', 'fix'], {'alias': 'fix', 'command': [], 'yes': False, + 'help': False, 'version': False}), + (['thefuck', 'git', 'branch', ARGUMENT_PLACEHOLDER, '-y'], + {'alias': None, 'command': ['git', 'branch'], 'yes': True, + 'help': False, 'version': False}), + (['thefuck', 'git', 'branch', '-a', ARGUMENT_PLACEHOLDER, '-y'], + {'alias': None, 'command': ['git', 'branch', '-a'], 'yes': True, + 'help': False, 'version': False}), + (['thefuck', ARGUMENT_PLACEHOLDER, '-v'], + {'alias': None, 'command': [], 'yes': False, 'help': False, + 'version': True}), + (['thefuck', ARGUMENT_PLACEHOLDER, '--help'], + {'alias': None, 'command': [], 'yes': False, 'help': True, + 'version': False})]) +def test_parse(argv, result): + assert vars(Parser().parse(argv)) == result diff --git a/thefuck/argument_parser.py b/thefuck/argument_parser.py new file mode 100644 index 00000000..d751faed --- /dev/null +++ b/thefuck/argument_parser.py @@ -0,0 +1,48 @@ +import sys +from argparse import ArgumentParser +from .const import ARGUMENT_PLACEHOLDER +from .utils import get_alias + + +class Parser(object): + def __init__(self): + self._parser = ArgumentParser(prog='thefuck', add_help=False) + self._parser.add_argument( + '-v', '--version', + action='store_true', + help="show program's version number and exit") + self._parser.add_argument( + '-a', '--alias', + nargs='?', + const=get_alias(), + help='[custom-alias-name] prints alias for current shell') + self._parser.add_argument( + '-h', '--help', + action='store_true', + help='show this help message and exit') + self._parser.add_argument( + '-y', '--yes', + action='store_true', + help='execute fixed command without confirmation') + self._parser.add_argument('command', + nargs='*', + help='command that should be fixed') + + def _get_arguments(self, argv): + if ARGUMENT_PLACEHOLDER in argv: + index = argv.index(ARGUMENT_PLACEHOLDER) + return argv[index + 1:] + ['--'] + argv[:index] + elif argv and not argv[0].startswith('-') and argv[0] != '--': + return ['--'] + argv + else: + return argv + + def parse(self, argv): + arguments = self._get_arguments(argv[1:]) + return self._parser.parse_args(arguments) + + def print_usage(self): + self._parser.print_usage(sys.stderr) + + def print_help(self): + self._parser.print_help(sys.stderr) diff --git a/thefuck/conf.py b/thefuck/conf.py index 7d25ce3c..32e051a0 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -14,7 +14,7 @@ class Settings(dict): def __setattr__(self, key, value): self[key] = value - def init(self): + def init(self, args=None): """Fills `settings` with values from `settings.py` and env.""" from .logs import exception @@ -31,6 +31,8 @@ class Settings(dict): except Exception: exception("Can't load settings from env", sys.exc_info()) + self.update(self._settings_from_args(args)) + def _init_settings_file(self): settings_path = self.user_dir.joinpath('settings.py') if not settings_path.is_file(): @@ -109,5 +111,12 @@ class Settings(dict): for env, attr in const.ENV_TO_ATTR.items() if env in os.environ} + def _settings_from_args(self, args): + """Loads settings from args.""" + if args and args.yes: + return {'require_confirmation': False} + else: + return {} + settings = Settings(const.DEFAULT_SETTINGS) diff --git a/thefuck/const.py b/thefuck/const.py index e6fbf387..e57ce960 100644 --- a/thefuck/const.py +++ b/thefuck/const.py @@ -59,3 +59,5 @@ SETTINGS_HEADER = u"""# The Fuck settings file # """ + +ARGUMENT_PLACEHOLDER = 'THEFUCK_ARGUMENT_PLACEHOLDER' diff --git a/thefuck/logs.py b/thefuck/logs.py index cb96e571..b141f175 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -121,3 +121,9 @@ def configured_successfully(configuration_details): bold=color(colorama.Style.BRIGHT), reset=color(colorama.Style.RESET_ALL), reload=configuration_details.reload)) + + +def version(thefuck_version, python_version): + print(u'The Fuck {} using Python {}'.format(thefuck_version, + python_version), + file=sys.stderr) diff --git a/thefuck/main.py b/thefuck/main.py index 565ef384..d39fff1a 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -3,7 +3,6 @@ from .system import init_output init_output() -from argparse import ArgumentParser # noqa: E402 from pprint import pformat # noqa: E402 import sys # noqa: E402 from . import logs, types # noqa: E402 @@ -11,18 +10,19 @@ from .shells import shell # noqa: E402 from .conf import settings # noqa: E402 from .corrector import get_corrected_commands # noqa: E402 from .exceptions import EmptyCommand # noqa: E402 -from .utils import get_installation_info, get_alias # noqa: E402 from .ui import select_command # noqa: E402 +from .argument_parser import Parser # noqa: E402 +from .utils import get_installation_info # noqa: E402 -def fix_command(): +def fix_command(known_args): """Fixes previous command. Used when `thefuck` called without arguments.""" - settings.init() + settings.init(known_args) with logs.debug_time('Total'): logs.debug(u'Run with settings: {}'.format(pformat(settings))) try: - command = types.Command.from_raw_script(sys.argv[1:]) + command = types.Command.from_raw_script(known_args.command) except EmptyCommand: logs.debug('Empty command, nothing to do') return @@ -36,34 +36,18 @@ def fix_command(): sys.exit(1) -def print_alias(): - """Prints alias for current shell.""" - try: - alias = sys.argv[2] - except IndexError: - alias = get_alias() - - print(shell.app_alias(alias)) - - def main(): - parser = ArgumentParser(prog='thefuck') - version = get_installation_info().version - parser.add_argument('-v', '--version', - action='version', - version='The Fuck {} using Python {}'.format( - version, sys.version.split()[0])) - parser.add_argument('-a', '--alias', - action='store_true', - help='[custom-alias-name] prints alias for current shell') - parser.add_argument('command', - nargs='*', - help='command that should be fixed') - known_args = parser.parse_args(sys.argv[1:2]) + parser = Parser() + known_args = parser.parse(sys.argv) - if known_args.alias: - print_alias() + if known_args.help: + parser.print_help() + elif known_args.version: + logs.version(get_installation_info().version, + sys.version.split()[0]) elif known_args.command: - fix_command() + fix_command(known_args) + elif known_args.alias: + print(shell.app_alias(known_args.alias)) else: parser.print_usage() diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py index 5bfd2bec..219d2fd9 100644 --- a/thefuck/shells/zsh.py +++ b/thefuck/shells/zsh.py @@ -1,23 +1,28 @@ from time import time import os from ..conf import settings +from ..const import ARGUMENT_PLACEHOLDER from ..utils import memoize from .generic import Generic class Zsh(Generic): def app_alias(self, alias_name): - # It is VERY important to have the variables declared WITHIN the alias - alias = "alias {0}='TF_CMD=$(TF_ALIAS={0}" \ - " PYTHONIOENCODING=utf-8" \ - " TF_SHELL_ALIASES=$(alias)" \ - " thefuck $(fc -ln -1 | tail -n 1)) &&" \ - " eval $TF_CMD".format(alias_name) - - if settings.alter_history: - return alias + " ; test -n \"$TF_CMD\" && print -s $TF_CMD'" - else: - return alias + "'" + # It is VERY important to have the variables declared WITHIN the function + return ''' + {name} () {{ + TF_ALIAS={name}; + PYTHONIOENCODING=utf-8; + TF_SHELL_ALIASES=$(alias); + TF_PREVIOUS=$(fc -ln -1 | tail -n 1); + TF_CMD=$(thefuck $TF_PREVIOUS {argument_placeholder} $*) && eval $TF_CMD; + {alter_history} + }} + '''.format( + name=alias_name, + argument_placeholder=ARGUMENT_PLACEHOLDER, + alter_history=('test -n "$TF_CMD" && print -s $TF_CMD' + if settings.alter_history else '')) def _parse_alias(self, alias): name, value = alias.split('=', 1)