mirror of
				https://github.com/nvbn/thefuck.git
				synced 2025-10-31 07:04:12 +00:00 
			
		
		
		
	Merge branch 'mcarton-unzip-clean'
This commit is contained in:
		| @@ -139,6 +139,8 @@ using the matched rule and runs it. Rules enabled by default are as follows: | |||||||
| * `composer_not_command` – fixes composer command name; | * `composer_not_command` – fixes composer command name; | ||||||
| * `cp_omitting_directory` – adds `-a` when you `cp` directory; | * `cp_omitting_directory` – adds `-a` when you `cp` directory; | ||||||
| * `cpp11` – adds missing `-std=c++11` to `g++` or `clang++`; | * `cpp11` – adds missing `-std=c++11` to `g++` or `clang++`; | ||||||
|  | * `dirty_untar` – fixes `tar x` command that untarred in the current directory; | ||||||
|  | * `dirty_unzip` – fixes `unzip` command that unzipped in the current directory; | ||||||
| * `django_south_ghost` – adds `--delete-ghost-migrations` to failed because ghosts django south migration; | * `django_south_ghost` – adds `--delete-ghost-migrations` to failed because ghosts django south migration; | ||||||
| * `django_south_merge` – adds `--merge` to inconsistent django south migration; | * `django_south_merge` – adds `--merge` to inconsistent django south migration; | ||||||
| * `docker_not_command` – fixes wrong docker commands like `docker tags`; | * `docker_not_command` – fixes wrong docker commands like `docker tags`; | ||||||
| @@ -212,8 +214,8 @@ get_new_command(command: Command, settings: Settings) -> str | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Also the rule can contain an optional function | Also the rule can contain an optional function | ||||||
| `side_effect(command: Command, settings: Settings) -> None` and an | `side_effect(command: Command, settings: Settings) -> None` and | ||||||
| optional boolean `enabled_by_default`. | optional `enabled_by_default`, `requires_output` and `priority` variables. | ||||||
|  |  | ||||||
| `Command` has three attributes: `script`, `stdout` and `stderr`. | `Command` has three attributes: `script`, `stdout` and `stderr`. | ||||||
|  |  | ||||||
| @@ -237,6 +239,8 @@ def side_effect(command, settings): | |||||||
|     subprocess.call('chmod 777 .', shell=True) |     subprocess.call('chmod 777 .', shell=True) | ||||||
|  |  | ||||||
| priority = 1000  # Lower first, default is 1000 | priority = 1000  # Lower first, default is 1000 | ||||||
|  |  | ||||||
|  | requires_output = True | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| [More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules), | [More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules), | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								tests/rules/test_dirty_untar.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								tests/rules/test_dirty_untar.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | import os | ||||||
|  | import pytest | ||||||
|  | import tarfile | ||||||
|  | from thefuck.rules.dirty_untar import match, get_new_command, side_effect | ||||||
|  | from tests.utils import Command | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture | ||||||
|  | def tar_error(tmpdir): | ||||||
|  |     def fixture(filename): | ||||||
|  |         path = os.path.join(str(tmpdir), filename) | ||||||
|  |  | ||||||
|  |         def reset(path): | ||||||
|  |             with tarfile.TarFile(path, 'w') as archive: | ||||||
|  |                 for file in ('a', 'b', 'c'): | ||||||
|  |                     with open(file, 'w') as f: | ||||||
|  |                         f.write('*') | ||||||
|  |  | ||||||
|  |                     archive.add(file) | ||||||
|  |  | ||||||
|  |                     os.remove(file) | ||||||
|  |  | ||||||
|  |             with tarfile.TarFile(path, 'r') as archive: | ||||||
|  |                 archive.extractall() | ||||||
|  |  | ||||||
|  |         os.chdir(str(tmpdir)) | ||||||
|  |         reset(path) | ||||||
|  |  | ||||||
|  |         assert(set(os.listdir('.')) == {filename, 'a', 'b', 'c'}) | ||||||
|  |  | ||||||
|  |     return fixture | ||||||
|  |  | ||||||
|  | parametrize_filename = pytest.mark.parametrize('filename', [ | ||||||
|  |     'foo.tar', | ||||||
|  |     'foo.tar.gz', | ||||||
|  |     'foo.tgz']) | ||||||
|  |  | ||||||
|  | parametrize_script = pytest.mark.parametrize('script, fixed', [ | ||||||
|  |     ('tar xvf {}', 'mkdir -p foo && tar xvf {} -C foo'), | ||||||
|  |     ('tar -xvf {}', 'mkdir -p foo && tar -xvf {} -C foo'), | ||||||
|  |     ('tar --extract -f {}', 'mkdir -p foo && tar --extract -f {} -C foo')]) | ||||||
|  |  | ||||||
|  | @parametrize_filename | ||||||
|  | @parametrize_script | ||||||
|  | def test_match(tar_error, filename, script, fixed): | ||||||
|  |     tar_error(filename) | ||||||
|  |     assert match(Command(script=script.format(filename)), None) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @parametrize_filename | ||||||
|  | @parametrize_script | ||||||
|  | def test_side_effect(tar_error, filename, script, fixed): | ||||||
|  |     tar_error(filename) | ||||||
|  |     side_effect(Command(script=script.format(filename)), None) | ||||||
|  |     assert(os.listdir('.') == [filename]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @parametrize_filename | ||||||
|  | @parametrize_script | ||||||
|  | def test_get_new_command(tar_error, filename, script, fixed): | ||||||
|  |     tar_error(filename) | ||||||
|  |     assert get_new_command(Command(script=script.format(filename)), None) == fixed.format(filename) | ||||||
							
								
								
									
										45
									
								
								tests/rules/test_dirty_unzip.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								tests/rules/test_dirty_unzip.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | import os | ||||||
|  | import pytest | ||||||
|  | import zipfile | ||||||
|  | from thefuck.rules.dirty_unzip import match, get_new_command, side_effect | ||||||
|  | from tests.utils import Command | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture | ||||||
|  | def zip_error(tmpdir): | ||||||
|  |     path = os.path.join(str(tmpdir), 'foo.zip') | ||||||
|  |  | ||||||
|  |     def reset(path): | ||||||
|  |         with zipfile.ZipFile(path, 'w') as archive: | ||||||
|  |             archive.writestr('a', '1') | ||||||
|  |             archive.writestr('b', '2') | ||||||
|  |             archive.writestr('c', '3') | ||||||
|  |  | ||||||
|  |             archive.extractall() | ||||||
|  |  | ||||||
|  |     os.chdir(str(tmpdir)) | ||||||
|  |     reset(path) | ||||||
|  |  | ||||||
|  |     assert(set(os.listdir('.')) == {'foo.zip', 'a', 'b', 'c'}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize('script', [ | ||||||
|  |     'unzip foo', | ||||||
|  |     'unzip foo.zip']) | ||||||
|  | def test_match(zip_error, script): | ||||||
|  |     assert match(Command(script=script), None) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize('script', [ | ||||||
|  |     'unzip foo', | ||||||
|  |     'unzip foo.zip']) | ||||||
|  | def test_side_effect(zip_error, script): | ||||||
|  |     side_effect(Command(script=script), None) | ||||||
|  |     assert(os.listdir('.') == ['foo.zip']) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize('script,fixed', [ | ||||||
|  |     ('unzip foo', 'unzip foo -d foo'), | ||||||
|  |     ('unzip foo.zip', 'unzip foo.zip -d foo')]) | ||||||
|  | def test_get_new_command(zip_error, script, fixed): | ||||||
|  |     assert get_new_command(Command(script=script), None) == fixed | ||||||
| @@ -14,7 +14,8 @@ def test_load_rule(mocker): | |||||||
|         return_value=Mock(match=match, |         return_value=Mock(match=match, | ||||||
|                           get_new_command=get_new_command, |                           get_new_command=get_new_command, | ||||||
|                           enabled_by_default=True, |                           enabled_by_default=True, | ||||||
|                           priority=900)) |                           priority=900, | ||||||
|  |                           requires_output=True)) | ||||||
|     assert main.load_rule(Path('/rules/bash.py')) \ |     assert main.load_rule(Path('/rules/bash.py')) \ | ||||||
|            == Rule('bash', match, get_new_command, priority=900) |            == Rule('bash', match, get_new_command, priority=900) | ||||||
|     load_source.assert_called_once_with('bash', '/rules/bash.py') |     load_source.assert_called_once_with('bash', '/rules/bash.py') | ||||||
| @@ -152,7 +153,7 @@ class TestConfirm(object): | |||||||
|  |  | ||||||
|     def test_with_side_effect_and_without_confirmation(self, capsys): |     def test_with_side_effect_and_without_confirmation(self, capsys): | ||||||
|         assert main.confirm('command', Mock(), Mock(require_confirmation=False)) |         assert main.confirm('command', Mock(), Mock(require_confirmation=False)) | ||||||
|         assert capsys.readouterr() == ('', 'command*\n') |         assert capsys.readouterr() == ('', 'command (+side effect)\n') | ||||||
|  |  | ||||||
|     # `stdin` fixture should be applied after `capsys` |     # `stdin` fixture should be applied after `capsys` | ||||||
|     def test_when_confirmation_required_and_confirmed(self, capsys, stdin): |     def test_when_confirmation_required_and_confirmed(self, capsys, stdin): | ||||||
| @@ -164,7 +165,7 @@ class TestConfirm(object): | |||||||
|     def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin): |     def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin): | ||||||
|         assert main.confirm('command', Mock(), Mock(require_confirmation=True, |         assert main.confirm('command', Mock(), Mock(require_confirmation=True, | ||||||
|                                                     no_colors=True)) |                                                     no_colors=True)) | ||||||
|         assert capsys.readouterr() == ('', 'command* [enter/ctrl+c]') |         assert capsys.readouterr() == ('', 'command (+side effect) [enter/ctrl+c]') | ||||||
|  |  | ||||||
|     def test_when_confirmation_required_and_aborted(self, capsys, stdin): |     def test_when_confirmation_required_and_aborted(self, capsys, stdin): | ||||||
|         stdin.side_effect = KeyboardInterrupt |         stdin.side_effect = KeyboardInterrupt | ||||||
|   | |||||||
| @@ -10,7 +10,8 @@ def Rule(name='', match=lambda *_: True, | |||||||
|          get_new_command=lambda *_: '', |          get_new_command=lambda *_: '', | ||||||
|          enabled_by_default=True, |          enabled_by_default=True, | ||||||
|          side_effect=None, |          side_effect=None, | ||||||
|          priority=DEFAULT_PRIORITY): |          priority=DEFAULT_PRIORITY, | ||||||
|  |          requires_output=True): | ||||||
|     return types.Rule(name, match, get_new_command, |     return types.Rule(name, match, get_new_command, | ||||||
|                       enabled_by_default, side_effect, |                       enabled_by_default, side_effect, | ||||||
|                       priority) |                       priority, requires_output) | ||||||
|   | |||||||
| @@ -29,19 +29,19 @@ def rule_failed(rule, exc_info, settings): | |||||||
|  |  | ||||||
|  |  | ||||||
| def show_command(new_command, side_effect, settings): | def show_command(new_command, side_effect, settings): | ||||||
|     sys.stderr.write('{bold}{command}{side_effect}{reset}\n'.format( |     sys.stderr.write('{bold}{command}{reset}{side_effect}\n'.format( | ||||||
|         command=new_command, |         command=new_command, | ||||||
|         side_effect='*' if side_effect else '', |         side_effect=' (+side effect)' if side_effect else '', | ||||||
|         bold=color(colorama.Style.BRIGHT, settings), |         bold=color(colorama.Style.BRIGHT, settings), | ||||||
|         reset=color(colorama.Style.RESET_ALL, settings))) |         reset=color(colorama.Style.RESET_ALL, settings))) | ||||||
|  |  | ||||||
|  |  | ||||||
| def confirm_command(new_command, side_effect, settings): | def confirm_command(new_command, side_effect, settings): | ||||||
|     sys.stderr.write( |     sys.stderr.write( | ||||||
|         '{bold}{command}{side_effect}{reset} ' |         '{bold}{command}{reset}{side_effect} ' | ||||||
|         '[{green}enter{reset}/{red}ctrl+c{reset}]'.format( |         '[{green}enter{reset}/{red}ctrl+c{reset}]'.format( | ||||||
|             command=new_command, |             command=new_command, | ||||||
|             side_effect='*' if side_effect else '', |             side_effect=' (+side effect)' if side_effect else '', | ||||||
|             bold=color(colorama.Style.BRIGHT, settings), |             bold=color(colorama.Style.BRIGHT, settings), | ||||||
|             green=color(colorama.Fore.GREEN, settings), |             green=color(colorama.Fore.GREEN, settings), | ||||||
|             red=color(colorama.Fore.RED, settings), |             red=color(colorama.Fore.RED, settings), | ||||||
|   | |||||||
| @@ -28,7 +28,8 @@ def load_rule(rule): | |||||||
|                       rule_module.get_new_command, |                       rule_module.get_new_command, | ||||||
|                       getattr(rule_module, 'enabled_by_default', True), |                       getattr(rule_module, 'enabled_by_default', True), | ||||||
|                       getattr(rule_module, 'side_effect', None), |                       getattr(rule_module, 'side_effect', None), | ||||||
|                       getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY)) |                       getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY), | ||||||
|  |                       getattr(rule_module, 'requires_output', True)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _get_loaded_rules(rules, settings): | def _get_loaded_rules(rules, settings): | ||||||
| @@ -87,13 +88,26 @@ def get_command(settings, args): | |||||||
|                          settings): |                          settings): | ||||||
|         result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env) |         result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env) | ||||||
|         if wait_output(settings, result): |         if wait_output(settings, result): | ||||||
|             return types.Command(script, result.stdout.read().decode('utf-8'), |             stdout = result.stdout.read().decode('utf-8') | ||||||
|                                  result.stderr.read().decode('utf-8')) |             stderr = result.stderr.read().decode('utf-8') | ||||||
|  |  | ||||||
|  |             logs.debug(u'Received stdout: {}'.format(stdout), settings) | ||||||
|  |             logs.debug(u'Received stderr: {}'.format(stderr), settings) | ||||||
|  |  | ||||||
|  |             return types.Command(script, stdout, stderr) | ||||||
|  |         else: | ||||||
|  |             logs.debug(u'Execution timed out!', settings) | ||||||
|  |             return types.Command(script, None, None) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_matched_rule(command, rules, settings): | def get_matched_rule(command, rules, settings): | ||||||
|     """Returns first matched rule for command.""" |     """Returns first matched rule for command.""" | ||||||
|  |     script_only = command.stdout is None and command.stderr is None | ||||||
|  |  | ||||||
|     for rule in rules: |     for rule in rules: | ||||||
|  |         if script_only and rule.requires_output: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             with logs.debug_time(u'Trying rule: {};'.format(rule.name), |             with logs.debug_time(u'Trying rule: {};'.format(rule.name), | ||||||
|                                  settings): |                                  settings): | ||||||
| @@ -138,10 +152,6 @@ def main(): | |||||||
|         logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings) |         logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings) | ||||||
|  |  | ||||||
|         command = get_command(settings, sys.argv) |         command = get_command(settings, sys.argv) | ||||||
|         if command: |  | ||||||
|             logs.debug(u'Received stdout: {}'.format(command.stdout), settings) |  | ||||||
|             logs.debug(u'Received stderr: {}'.format(command.stderr), settings) |  | ||||||
|  |  | ||||||
|         rules = get_rules(user_dir, settings) |         rules = get_rules(user_dir, settings) | ||||||
|         logs.debug( |         logs.debug( | ||||||
|             u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), |             u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								thefuck/rules/dirty_untar.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								thefuck/rules/dirty_untar.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | from thefuck import shells | ||||||
|  | import os | ||||||
|  | import tarfile | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _is_tar_extract(cmd): | ||||||
|  |     if '--extract' in cmd: | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |     cmd = cmd.split() | ||||||
|  |  | ||||||
|  |     return len(cmd) > 1 and 'x' in cmd[1] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _tar_file(cmd): | ||||||
|  |     tar_extentions = ('.tar', '.tar.Z', '.tar.bz2', '.tar.gz', '.tar.lz', | ||||||
|  |                       '.tar.lzma', '.tar.xz', '.taz', '.tb2', '.tbz', '.tbz2', | ||||||
|  |                       '.tgz', '.tlz', '.txz', '.tz') | ||||||
|  |  | ||||||
|  |     for c in cmd.split(): | ||||||
|  |         for ext in tar_extentions: | ||||||
|  |             if c.endswith(ext): | ||||||
|  |                 return (c, c[0:len(c)-len(ext)]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def match(command, settings): | ||||||
|  |     return (command.script.startswith('tar') | ||||||
|  |             and '-C' not in command.script | ||||||
|  |             and _is_tar_extract(command.script) | ||||||
|  |             and _tar_file(command.script) is not None) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_new_command(command, settings): | ||||||
|  |     return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \ | ||||||
|  |                  .format(dir=_tar_file(command.script)[1], cmd=command.script) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def side_effect(command, settings): | ||||||
|  |     with tarfile.TarFile(_tar_file(command.script)[0]) as archive: | ||||||
|  |         for file in archive.getnames(): | ||||||
|  |             os.remove(file) | ||||||
							
								
								
									
										39
									
								
								thefuck/rules/dirty_unzip.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								thefuck/rules/dirty_unzip.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | import os | ||||||
|  | import zipfile | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _is_bad_zip(file): | ||||||
|  |     with zipfile.ZipFile(file, 'r') as archive: | ||||||
|  |         return len(archive.namelist()) > 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _zip_file(command): | ||||||
|  |     # unzip works that way: | ||||||
|  |     # unzip [-flags] file[.zip] [file(s) ...] [-x file(s) ...] | ||||||
|  |     #                ^          ^ files to unzip from the archive | ||||||
|  |     #                archive to unzip | ||||||
|  |     for c in command.script.split()[1:]: | ||||||
|  |         if not c.startswith('-'): | ||||||
|  |             if c.endswith('.zip'): | ||||||
|  |                 return c | ||||||
|  |             else: | ||||||
|  |                 return '{}.zip'.format(c) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def match(command, settings): | ||||||
|  |     return (command.script.startswith('unzip') | ||||||
|  |             and '-d' not in command.script | ||||||
|  |             and _is_bad_zip(_zip_file(command))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_new_command(command, settings): | ||||||
|  |     return '{} -d {}'.format(command.script, _zip_file(command)[:-4]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def side_effect(command, settings): | ||||||
|  |     with zipfile.ZipFile(_zip_file(command), 'r') as archive: | ||||||
|  |         for file in archive.namelist(): | ||||||
|  |             os.remove(file) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | requires_output = False | ||||||
| @@ -5,7 +5,7 @@ Command = namedtuple('Command', ('script', 'stdout', 'stderr')) | |||||||
|  |  | ||||||
| Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', | Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', | ||||||
|                            'enabled_by_default', 'side_effect', |                            'enabled_by_default', 'side_effect', | ||||||
|                            'priority')) |                            'priority', 'requires_output')) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RulesNamesList(list): | class RulesNamesList(list): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user