diff --git a/tests/shells/test_bash.py b/tests/shells/test_bash.py index 84a2fa51..117e3af4 100644 --- a/tests/shells/test_bash.py +++ b/tests/shells/test_bash.py @@ -33,6 +33,9 @@ class TestBash(object): def test_and_(self, shell): assert shell.and_('ls', 'cd') == 'ls && cd' + def test_or_(self, shell): + assert shell.or_('ls', 'cd') == 'ls || cd' + def test_get_aliases(self, shell): assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))', 'l': 'ls -CF', diff --git a/tests/shells/test_fish.py b/tests/shells/test_fish.py index 4e900fae..ce7858fb 100644 --- a/tests/shells/test_fish.py +++ b/tests/shells/test_fish.py @@ -55,6 +55,9 @@ class TestFish(object): def test_and_(self, shell): assert shell.and_('foo', 'bar') == 'foo; and bar' + def test_or_(self, shell): + assert shell.or_('foo', 'bar') == 'foo; or bar' + def test_get_aliases(self, shell): assert shell.get_aliases() == {'fish_config': 'fish_config', 'fuck': 'fuck', diff --git a/tests/shells/test_generic.py b/tests/shells/test_generic.py index 81838da7..a0535ff6 100644 --- a/tests/shells/test_generic.py +++ b/tests/shells/test_generic.py @@ -18,6 +18,9 @@ class TestGeneric(object): def test_and_(self, shell): assert shell.and_('ls', 'cd') == 'ls && cd' + def test_or_(self, shell): + assert shell.or_('ls', 'cd') == 'ls || cd' + def test_get_aliases(self, shell): assert shell.get_aliases() == {} diff --git a/tests/shells/test_tcsh.py b/tests/shells/test_tcsh.py index 9453c1ad..7bc8ae30 100644 --- a/tests/shells/test_tcsh.py +++ b/tests/shells/test_tcsh.py @@ -34,6 +34,9 @@ class TestTcsh(object): def test_and_(self, shell): assert shell.and_('ls', 'cd') == 'ls && cd' + def test_or_(self, shell): + assert shell.or_('ls', 'cd') == 'ls || cd' + def test_get_aliases(self, shell): assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))', 'l': 'ls -CF', diff --git a/tests/shells/test_zsh.py b/tests/shells/test_zsh.py index 378b8e22..e31c362e 100644 --- a/tests/shells/test_zsh.py +++ b/tests/shells/test_zsh.py @@ -32,6 +32,9 @@ class TestZsh(object): def test_and_(self, shell): assert shell.and_('ls', 'cd') == 'ls && cd' + def test_or_(self, shell): + assert shell.or_('ls', 'cd') == 'ls || cd' + def test_get_aliases(self, shell): assert shell.get_aliases() == { 'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))', diff --git a/tests/test_argument_parser.py b/tests/test_argument_parser.py index 7705a229..015449bd 100644 --- a/tests/test_argument_parser.py +++ b/tests/test_argument_parser.py @@ -3,29 +3,27 @@ from thefuck.argument_parser import Parser from thefuck.const import ARGUMENT_PLACEHOLDER +def _args(**override): + args = {'alias': None, 'command': [], 'yes': False, + 'help': False, 'version': False, 'debug': False, + 'force_command': None, 'repeat': False} + args.update(override) + return args + + @pytest.mark.parametrize('argv, result', [ - (['thefuck'], {'alias': None, 'command': [], 'yes': False, - 'help': False, 'version': False, 'debug': False}), - (['thefuck', '-a'], - {'alias': 'fuck', 'command': [], 'yes': False, - 'help': False, 'version': False, 'debug': False}), - (['thefuck', '-a', 'fix'], - {'alias': 'fix', 'command': [], 'yes': False, - 'help': False, 'version': False, 'debug': False}), + (['thefuck'], _args()), + (['thefuck', '-a'], _args(alias='fuck')), + (['thefuck', '-a', 'fix'], _args(alias='fix')), (['thefuck', 'git', 'branch', ARGUMENT_PLACEHOLDER, '-y'], - {'alias': None, 'command': ['git', 'branch'], 'yes': True, - 'help': False, 'version': False, 'debug': False}), + _args(command=['git', 'branch'], yes=True)), (['thefuck', 'git', 'branch', '-a', ARGUMENT_PLACEHOLDER, '-y'], - {'alias': None, 'command': ['git', 'branch', '-a'], 'yes': True, - 'help': False, 'version': False, 'debug': False}), - (['thefuck', ARGUMENT_PLACEHOLDER, '-v'], - {'alias': None, 'command': [], 'yes': False, 'help': False, - 'version': True, 'debug': False}), - (['thefuck', ARGUMENT_PLACEHOLDER, '--help'], - {'alias': None, 'command': [], 'yes': False, 'help': True, - 'version': False, 'debug': False}), + _args(command=['git', 'branch', '-a'], yes=True)), + (['thefuck', ARGUMENT_PLACEHOLDER, '-v'], _args(version=True)), + (['thefuck', ARGUMENT_PLACEHOLDER, '--help'], _args(help=True)), (['thefuck', 'git', 'branch', '-a', ARGUMENT_PLACEHOLDER, '-y', '-d'], - {'alias': None, 'command': ['git', 'branch', '-a'], 'yes': True, - 'help': False, 'version': False, 'debug': True})]) + _args(command=['git', 'branch', '-a'], yes=True, debug=True)), + (['thefuck', 'git', 'branch', '-a', ARGUMENT_PLACEHOLDER, '-r', '-d'], + _args(command=['git', 'branch', '-a'], repeat=True, debug=True))]) def test_parse(argv, result): assert vars(Parser().parse(argv)) == result diff --git a/tests/test_conf.py b/tests/test_conf.py index fec66af9..f89d4e26 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -80,9 +80,10 @@ class TestSettingsFromEnv(object): def test_settings_from_args(settings): - settings.init(Mock(yes=True, debug=True)) + settings.init(Mock(yes=True, debug=True, repeat=True)) assert not settings.require_confirmation assert settings.debug + assert settings.repeat class TestInitializeSettingsFile(object): diff --git a/tests/test_types.py b/tests/test_types.py index 4faf8edf..5ddb489a 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -28,6 +28,20 @@ class TestCorrectedCommand(object): assert u'{}'.format(CorrectedCommand(u'echo café', None, 100)) == \ u'CorrectedCommand(script=echo café, side_effect=None, priority=100)' + @pytest.mark.parametrize('script, printed, override_settings', [ + ('git branch', 'git branch', {'repeat': False, 'debug': False}), + ('git brunch', + "git brunch || fuck --repeat --force-command 'git brunch'", + {'repeat': True, 'debug': False}), + ('git brunch', + "git brunch || fuck --repeat --debug --force-command 'git brunch'", + {'repeat': True, 'debug': True})]) + def test_run(self, capsys, settings, script, printed, override_settings): + settings.update(override_settings) + CorrectedCommand(script, None, 1000).run(Command()) + out, _ = capsys.readouterr() + assert out[:-1] == printed + class TestRule(object): def test_from_path(self, mocker): diff --git a/thefuck/argument_parser.py b/thefuck/argument_parser.py index 7e86d4cb..583d929a 100644 --- a/thefuck/argument_parser.py +++ b/thefuck/argument_parser.py @@ -1,5 +1,5 @@ import sys -from argparse import ArgumentParser +from argparse import ArgumentParser, SUPPRESS from .const import ARGUMENT_PLACEHOLDER from .utils import get_alias @@ -20,17 +20,27 @@ class Parser(object): '-h', '--help', action='store_true', help='show this help message and exit') - self._parser.add_argument( + group = self._parser.add_mutually_exclusive_group() + group.add_argument( '-y', '--yes', action='store_true', help='execute fixed command without confirmation') + group.add_argument( + '-r', '--repeat', + action='store_true', + help='repeat on failure') self._parser.add_argument( '-d', '--debug', action='store_true', help='enable debug output') - self._parser.add_argument('command', - nargs='*', - help='command that should be fixed') + self._parser.add_argument( + '--force-command', + action='store', + help=SUPPRESS) + self._parser.add_argument( + 'command', + nargs='*', + help='command that should be fixed') def _get_arguments(self, argv): if ARGUMENT_PLACEHOLDER in argv: diff --git a/thefuck/conf.py b/thefuck/conf.py index 04f6694c..e1ae8eb8 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -121,6 +121,8 @@ class Settings(dict): from_args['require_confirmation'] = not args.yes if args.debug: from_args['debug'] = args.debug + if args.repeat: + from_args['repeat'] = args.repeat return from_args diff --git a/thefuck/const.py b/thefuck/const.py index e57ce960..7c800057 100644 --- a/thefuck/const.py +++ b/thefuck/const.py @@ -34,6 +34,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'wait_slow_command': 15, 'slow_commands': ['lein', 'react-native', 'gradle', './gradlew', 'vagrant'], + 'repeat': False, 'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}} ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', @@ -46,7 +47,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', 'THEFUCK_HISTORY_LIMIT': 'history_limit', 'THEFUCK_ALTER_HISTORY': 'alter_history', 'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command', - 'THEFUCK_SLOW_COMMANDS': 'slow_commands'} + 'THEFUCK_SLOW_COMMANDS': 'slow_commands', + 'THEFUCK_REPEAT': 'repeat'} SETTINGS_HEADER = u"""# The Fuck settings file # diff --git a/thefuck/main.py b/thefuck/main.py index d39fff1a..e02cc2ba 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -20,9 +20,11 @@ def fix_command(known_args): settings.init(known_args) with logs.debug_time('Total'): logs.debug(u'Run with settings: {}'.format(pformat(settings))) + raw_command = ([known_args.force_command] if known_args.force_command + else known_args.command) try: - command = types.Command.from_raw_script(known_args.command) + command = types.Command.from_raw_script(raw_command) except EmptyCommand: logs.debug('Empty command, nothing to do') return diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index d20c5f89..6d777461 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -66,6 +66,9 @@ class Fish(Generic): def and_(self, *commands): return u'; and '.join(commands) + def or_(self, *commands): + return u'; or '.join(commands) + def how_to_configure(self): return self._create_shell_configuration( content=u"eval (thefuck --alias | tr '\n' ';')", diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index d8ece219..277e7980 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -66,6 +66,9 @@ class Generic(object): def and_(self, *commands): return u' && '.join(commands) + def or_(self, *commands): + return u' || '.join(commands) + def how_to_configure(self): return diff --git a/thefuck/types.py b/thefuck/types.py index 91f69e9c..71d6e493 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -9,6 +9,7 @@ from .shells import shell from .conf import settings from .const import DEFAULT_PRIORITY, ALL_ENABLED from .exceptions import EmptyCommand +from .utils import get_alias class Command(object): @@ -276,6 +277,22 @@ class CorrectedCommand(object): return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format( self.script, self.side_effect, self.priority) + def _get_script(self): + """Returns fixed commands script. + + If `settings.repeat` is `True`, appends command with second attempt + of running fuck in case fixed command fails again. + + """ + if settings.repeat: + repeat_fuck = '{} --repeat {}--force-command {}'.format( + get_alias(), + '--debug ' if settings.debug else '', + shell.quote(self.script)) + return shell.or_(self.script, repeat_fuck) + else: + return self.script + def run(self, old_cmd): """Runs command from rule for passed command. @@ -289,4 +306,5 @@ class CorrectedCommand(object): # This depends on correct setting of PYTHONIOENCODING by the alias: logs.debug(u'PYTHONIOENCODING: {}'.format( os.environ.get('PYTHONIOENCODING', '!!not-set!!'))) - print(self.script) + + print(self._get_script())