From 828ae537da5d7eb7a2a5c9f976b90975adfe1f43 Mon Sep 17 00:00:00 2001 From: Inga Feick Date: Thu, 4 Apr 2019 00:01:14 +0200 Subject: [PATCH 01/16] Docker login (#894) * Add docker_login rule * Add docker_login rule * Whitespace fix * Fix typo in test case * Fix typo in test case * Add test cases --- README.md | 1 + tests/rules/test_docker_login.py | 37 ++++++++++++++++++++++++++++++++ thefuck/rules/docker_login.py | 12 +++++++++++ 3 files changed, 50 insertions(+) create mode 100644 tests/rules/test_docker_login.py create mode 100644 thefuck/rules/docker_login.py diff --git a/README.md b/README.md index 53cfebae..bb1d8df2 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ following rules are enabled by default: * `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_merge` – adds `--merge` to inconsistent django south migration; +* `docker_login` – executes a `docker login` and repeats the previous command; * `docker_not_command` – fixes wrong docker commands like `docker tags`; * `dry` – fixes repetitions like `git git push`; * `fab_command_not_found` – fix misspelled fabric commands; diff --git a/tests/rules/test_docker_login.py b/tests/rules/test_docker_login.py new file mode 100644 index 00000000..2e62ae2a --- /dev/null +++ b/tests/rules/test_docker_login.py @@ -0,0 +1,37 @@ +from thefuck.rules.docker_login import match, get_new_command +from thefuck.types import Command + + +def test_match(): + err_response1 = """ + Sending build context to Docker daemon 118.8kB +Step 1/6 : FROM foo/bar:fdb7c6d +pull access denied for foo/bar, repository does not exist or may require 'docker login' +""" + assert match(Command('docker build -t artifactory:9090/foo/bar:fdb7c6d .', err_response1)) + + err_response2 = """ + The push refers to repository [artifactory:9090/foo/bar] +push access denied for foo/bar, repository does not exist or may require 'docker login' +""" + assert match(Command('docker push artifactory:9090/foo/bar:fdb7c6d', err_response2)) + + err_response3 = """ + docker push artifactory:9090/foo/bar:fdb7c6d +The push refers to repository [artifactory:9090/foo/bar] +9c29c7ad209d: Preparing +71f3ad53dfe0: Preparing +f58ee068224c: Preparing +aeddc924d0f7: Preparing +c2040e5d6363: Preparing +4d42df4f350f: Preparing +35723dab26f9: Preparing +71f3ad53dfe0: Pushed +cb95fa0faeb1: Layer already exists +""" + assert not match(Command('docker push artifactory:9090/foo/bar:fdb7c6d', err_response3)) + + +def test_get_new_command(): + assert get_new_command(Command('docker build -t artifactory:9090/foo/bar:fdb7c6d .', '')) == 'docker login && docker build -t artifactory:9090/foo/bar:fdb7c6d .' + assert get_new_command(Command('docker push artifactory:9090/foo/bar:fdb7c6d', '')) == 'docker login && docker push artifactory:9090/foo/bar:fdb7c6d' diff --git a/thefuck/rules/docker_login.py b/thefuck/rules/docker_login.py new file mode 100644 index 00000000..5a96b6c3 --- /dev/null +++ b/thefuck/rules/docker_login.py @@ -0,0 +1,12 @@ +from thefuck.utils import for_app + + +@for_app('docker') +def match(command): + return ('docker' in command.script + and "access denied" in command.output + and "may require 'docker login'" in command.output) + + +def get_new_command(command): + return 'docker login && {}'.format(command.script) From 82902fb50d99e3d5ac1d8785d11138f786e2fedc Mon Sep 17 00:00:00 2001 From: Inga Feick Date: Wed, 24 Apr 2019 18:15:38 +0200 Subject: [PATCH 02/16] Add rule for pip_install permission fix (#895) * Add rule for pip_install permission fix * Fix whitespace * Switch quotation to single * remove 2nd else * E261 indent comment --- README.md | 1 + tests/rules/test_pip_install.py | 27 +++++++++++++++++++++++++++ thefuck/rules/pip_install.py | 15 +++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 tests/rules/test_pip_install.py create mode 100644 thefuck/rules/pip_install.py diff --git a/README.md b/README.md index bb1d8df2..55ef2b32 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ following rules are enabled by default: * `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_such_file` – creates missing directories with `mv` and `cp` commands; * `open` – either prepends `http://` to address passed to `open` or create a new file or directory and passes it to `open`; +* `pip_install` – fixes permission issues with `pip install` commands by adding `--user` or prepending `sudo` if necessary; * `pip_unknown_command` – fixes wrong `pip` commands, for example `pip instatl/pip install`; * `php_s` – replaces `-s` by `-S` when trying to run a local php server; * `port_already_in_use` – kills process that bound port; diff --git a/tests/rules/test_pip_install.py b/tests/rules/test_pip_install.py new file mode 100644 index 00000000..c920b7cc --- /dev/null +++ b/tests/rules/test_pip_install.py @@ -0,0 +1,27 @@ +# -*- coding: UTF-8 -*- +from thefuck.rules.pip_install import match, get_new_command +from thefuck.types import Command + + +def test_match(): + response1 = """ + Could not install packages due to an EnvironmentError: [Errno 13] Permission denied: '/Library/Python/2.7/site-packages/entrypoints.pyc' +Consider using the `--user` option or check the permissions. +""" + assert match(Command('pip install -r requirements.txt', response1)) + + response2 = """ +Collecting bacon + Downloading https://files.pythonhosted.org/packages/b2/81/19fb79139ee71c8bc4e5a444546f318e2b87253b8939ec8a7e10d63b7341/bacon-0.3.1.zip (11.0MB) + 100% |████████████████████████████████| 11.0MB 3.0MB/s +Installing collected packages: bacon + Running setup.py install for bacon ... done +Successfully installed bacon-0.3.1 +""" + assert not match(Command('pip install bacon', response2)) + + +def test_get_new_command(): + assert get_new_command(Command('pip install -r requirements.txt', '')) == 'pip install --user -r requirements.txt' + assert get_new_command(Command('pip install bacon', '')) == 'pip install --user bacon' + assert get_new_command(Command('pip install --user -r requirements.txt', '')) == 'sudo pip install -r requirements.txt' diff --git a/thefuck/rules/pip_install.py b/thefuck/rules/pip_install.py new file mode 100644 index 00000000..8ee013c0 --- /dev/null +++ b/thefuck/rules/pip_install.py @@ -0,0 +1,15 @@ +from thefuck.utils import for_app +from thefuck.specific.sudo import sudo_support + + +@sudo_support +@for_app('pip') +def match(command): + return ('pip install' in command.script and 'Permission denied' in command.output) + + +def get_new_command(command): + if '--user' not in command.script: # add --user (attempt 1) + return command.script.replace(' install ', ' install --user ') + + return 'sudo {}'.format(command.script.replace(' --user', '')) # since --user didn't fix things, let's try sudo (attempt 2) From 55cb3546dfbe3ee29f06c1a455b1a5fb6d797ffa Mon Sep 17 00:00:00 2001 From: "Nick \"darkfiberiru\" Wolff" Date: Wed, 24 Apr 2019 12:17:01 -0400 Subject: [PATCH 03/16] Pkg should be recommend on freebsd to install (#905) --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 55ef2b32..c3d3e9d9 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,7 @@ sudo pip3 install thefuck On FreeBSD, install *The Fuck* with the following commands: ```bash -sudo portsnap fetch update -cd /usr/ports/misc/thefuck && sudo make install clean +pkg install thefuck ``` On ChromeOS, install *The Fuck* using [chromebrew](https://github.com/skycocker/chromebrew) with the following command: From 40ab4eb62db57627bff10cf029d29c94704086a2 Mon Sep 17 00:00:00 2001 From: Pablo Aguiar Date: Wed, 24 Apr 2019 18:17:52 +0200 Subject: [PATCH 04/16] #899: Support `-y/--yeah` command line args in Fish Shell (#900) --- tests/shells/test_fish.py | 2 ++ thefuck/shells/fish.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/shells/test_fish.py b/tests/shells/test_fish.py index 5ae2c79e..172f6f49 100644 --- a/tests/shells/test_fish.py +++ b/tests/shells/test_fish.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import pytest +from thefuck.const import ARGUMENT_PLACEHOLDER from thefuck.shells import Fish @@ -82,6 +83,7 @@ class TestFish(object): assert 'TF_SHELL=fish' in shell.app_alias('fuck') assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck') assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck') + assert ARGUMENT_PLACEHOLDER in shell.app_alias('fuck') def test_app_alias_alter_history(self, settings, shell): settings.alter_history = True diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 01706e19..2a89db6f 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -5,6 +5,7 @@ import sys import six from .. import logs from ..conf import settings +from ..const import ARGUMENT_PLACEHOLDER from ..utils import DEVNULL, cache from .generic import Generic @@ -56,11 +57,11 @@ class Fish(Generic): return ('function {0} -d "Correct your previous console command"\n' ' set -l fucked_up_command $history[1]\n' ' env TF_SHELL=fish TF_ALIAS={0} PYTHONIOENCODING=utf-8' - ' thefuck $fucked_up_command | read -l unfucked_command\n' + ' thefuck $fucked_up_command {2} $argv | read -l unfucked_command\n' ' if [ "$unfucked_command" != "" ]\n' ' eval $unfucked_command\n{1}' ' end\n' - 'end').format(alias_name, alter_history) + 'end').format(alias_name, alter_history, ARGUMENT_PLACEHOLDER) def get_aliases(self): overridden = self._get_overridden_aliases() From 78ef9eec88f43d5727986be2237f6e0e250cbbbc Mon Sep 17 00:00:00 2001 From: Pablo Aguiar Date: Tue, 21 May 2019 20:47:47 +0200 Subject: [PATCH 05/16] #902: Use `os.pathsep` to split PATH env var (#917) Fix #902 --- tests/test_utils.py | 20 +++++++++++++++++++- thefuck/utils.py | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index cf9208b8..3e73c3d7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ import pytest import warnings -from mock import Mock, patch +from mock import Mock, call, patch from thefuck.utils import default_settings, \ memoize, get_closest, get_all_executables, replace_argument, \ get_all_matched_commands, is_app, for_app, cache, \ @@ -76,6 +76,24 @@ def test_get_all_executables(): assert 'fuck' not in all_callables +@pytest.fixture +def os_environ_pathsep(monkeypatch, path, pathsep): + env = {'PATH': path} + monkeypatch.setattr('os.environ', env) + monkeypatch.setattr('os.pathsep', pathsep) + return env + + +@pytest.mark.usefixtures('no_memoize', 'os_environ_pathsep') +@pytest.mark.parametrize('path, pathsep', [ + ('/foo:/bar:/baz:/foo/bar', ':'), + (r'C:\\foo;C:\\bar;C:\\baz;C:\\foo\\bar', ';')]) +def test_get_all_executables_pathsep(path, pathsep): + with patch('thefuck.utils.Path') as Path_mock: + get_all_executables() + Path_mock.assert_has_calls([call(p) for p in path.split(pathsep)], True) + + @pytest.mark.parametrize('args, result', [ (('apt-get instol vim', 'instol', 'install'), 'apt-get install vim'), (('git brnch', 'brnch', 'branch'), 'git branch')]) diff --git a/thefuck/utils.py b/thefuck/utils.py index bd8028ec..6112c019 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -118,7 +118,7 @@ def get_all_executables(): tf_entry_points = ['thefuck', 'fuck'] bins = [exe.name.decode('utf8') if six.PY2 else exe.name - for path in os.environ.get('PATH', '').split(':') + for path in os.environ.get('PATH', '').split(os.pathsep) for exe in _safe(lambda: list(Path(path).iterdir()), []) if not _safe(exe.is_dir, True) and exe.name not in tf_entry_points] From 201c01fc74d479d9e418d32c92a88b50b27728a7 Mon Sep 17 00:00:00 2001 From: Jesus Cuesta Date: Tue, 21 May 2019 18:49:04 +0000 Subject: [PATCH 06/16] Adding yay AUR manager to Arch Linux's commands since yaourt is unmaintained and has some security issues. (#907) --- README.md | 4 ++-- tests/rules/test_pacman_not_found.py | 4 ++++ thefuck/rules/pacman_not_found.py | 6 +++--- thefuck/specific/archlinux.py | 4 +++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c3d3e9d9..b6e756cf 100644 --- a/README.md +++ b/README.md @@ -315,8 +315,8 @@ The following rules are enabled by default on specific platforms only: * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `brew_update_formula` – turns `brew update ` into `brew upgrade `; * `dnf_no_such_command` – fixes mistyped DNF commands; -* `pacman` – installs app with `pacman` if it is not installed (uses `yaourt` if available); -* `pacman_not_found` – fixes package name with `pacman` or `yaourt`. +* `pacman` – installs app with `pacman` if it is not installed (uses `yay` or `yaourt` if available); +* `pacman_not_found` – fixes package name with `pacman`, `yay` or `yaourt`. The following commands are bundled with *The Fuck*, but are not enabled by default: diff --git a/tests/rules/test_pacman_not_found.py b/tests/rules/test_pacman_not_found.py index ee81d269..a1b65327 100644 --- a/tests/rules/test_pacman_not_found.py +++ b/tests/rules/test_pacman_not_found.py @@ -11,6 +11,7 @@ extra/llvm35 3.5.2-13/usr/bin/llc''' @pytest.mark.skipif(not getattr(pacman_not_found, 'enabled_by_default', True), reason='Skip if pacman is not available') @pytest.mark.parametrize('command', [ + Command('yay -S llc', 'error: target not found: llc'), Command('yaourt -S llc', 'error: target not found: llc'), Command('pacman llc', 'error: target not found: llc'), Command('sudo pacman llc', 'error: target not found: llc')]) @@ -19,6 +20,7 @@ def test_match(command): @pytest.mark.parametrize('command', [ + Command('yay -S llc', 'error: target not found: llc'), Command('yaourt -S llc', 'error: target not found: llc'), Command('pacman llc', 'error: target not found: llc'), Command('sudo pacman llc', 'error: target not found: llc')]) @@ -31,6 +33,7 @@ def test_match_mocked(subp_mock, command): @pytest.mark.skipif(not getattr(pacman_not_found, 'enabled_by_default', True), reason='Skip if pacman is not available') @pytest.mark.parametrize('command, fixed', [ + (Command('yay -S llc', 'error: target not found: llc'), ['yay -S extra/llvm', 'yay -S extra/llvm35']), (Command('yaourt -S llc', 'error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']), (Command('pacman -S llc', 'error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']), (Command('sudo pacman -S llc', 'error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])]) @@ -39,6 +42,7 @@ def test_get_new_command(command, fixed): @pytest.mark.parametrize('command, fixed', [ + (Command('yay -S llc', 'error: target not found: llc'), ['yay -S extra/llvm', 'yay -S extra/llvm35']), (Command('yaourt -S llc', 'error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']), (Command('pacman -S llc', 'error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']), (Command('sudo pacman -S llc', 'error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])]) diff --git a/thefuck/rules/pacman_not_found.py b/thefuck/rules/pacman_not_found.py index 720135aa..4e55b0c0 100644 --- a/thefuck/rules/pacman_not_found.py +++ b/thefuck/rules/pacman_not_found.py @@ -1,9 +1,9 @@ """ Fixes wrong package names with pacman or yaourt. For example the `llc` program is in package `llvm` so this: - yaourt -S llc + yay -S llc should be: - yaourt -S llvm + yay -S llvm """ from thefuck.utils import replace_command @@ -12,7 +12,7 @@ from thefuck.specific.archlinux import get_pkgfile, archlinux_env def match(command): return (command.script_parts - and (command.script_parts[0] in ('pacman', 'yaourt') + and (command.script_parts[0] in ('pacman', 'yay', 'yaourt') or command.script_parts[0:2] == ['sudo', 'pacman']) and 'error: target not found:' in command.output) diff --git a/thefuck/specific/archlinux.py b/thefuck/specific/archlinux.py index 5c95aa5b..2d9e7b93 100644 --- a/thefuck/specific/archlinux.py +++ b/thefuck/specific/archlinux.py @@ -32,7 +32,9 @@ def get_pkgfile(command): def archlinux_env(): - if utils.which('yaourt'): + if utils.which('yay'): + pacman = 'yay' + elif utils.which('yaourt'): pacman = 'yaourt' elif utils.which('pacman'): pacman = 'sudo pacman' From 70a13406f0a37f49daf7b0b327d9d7c5850a6836 Mon Sep 17 00:00:00 2001 From: Ryan Delaney <1139517+rpdelaney@users.noreply.github.com> Date: Wed, 22 May 2019 11:22:09 -0700 Subject: [PATCH 07/16] Fix a couple small shellcheck errors (#915) * Fix shellcheck SC2046 Further reading: https://github.com/koalaman/shellcheck/wiki/Sc2046 * Fix shellcheck 2068 Further reading: https://github.com/koalaman/shellcheck/wiki/Sc2068 * Fix syntax error from bad quoting I also used a docstring here because the escaping makes it harder for humans to parse --- thefuck/shells/bash.py | 6 +++--- thefuck/shells/generic.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index d8a53180..df972380 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -20,8 +20,8 @@ class Bash(Generic): export TF_HISTORY=$(fc -ln -10); export PYTHONIOENCODING=utf-8; TF_CMD=$( - thefuck {argument_placeholder} $@ - ) && eval $TF_CMD; + thefuck {argument_placeholder} "$@" + ) && eval "$TF_CMD"; unset TF_HISTORY; export PYTHONIOENCODING=$TF_PYTHONIOENCODING; {alter_history} @@ -79,7 +79,7 @@ class Bash(Generic): config = 'bash config' return self._create_shell_configuration( - content=u'eval $(thefuck --alias)', + content=u'eval "$(thefuck --alias)"', path=config, reload=u'source {}'.format(config)) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index ddd868ae..d7a936cb 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -34,8 +34,8 @@ class Generic(object): return command_script def app_alias(self, alias_name): - return "alias {0}='eval $(TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \ - "thefuck $(fc -ln -1))'".format(alias_name) + return """alias {0}='eval "$(TF_ALIAS={0} PYTHONIOENCODING=utf-8 """ \ + """thefuck "$(fc -ln -1)")"'""".format(alias_name) def instant_mode_alias(self, alias_name): warn("Instant mode not supported by your shell") From 5efcf1019f90b75561bbe9f1109b83778d0dddeb Mon Sep 17 00:00:00 2001 From: Pablo Aguiar Date: Mon, 27 May 2019 18:23:06 +0200 Subject: [PATCH 08/16] #N/A: Improve support to Windows in `no_command` rule (#918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows “not found” message is quite different from POSIX systems. --- tests/rules/test_no_command.py | 3 ++- thefuck/rules/no_command.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/rules/test_no_command.py b/tests/rules/test_no_command.py index ab8b0dac..0df4590b 100644 --- a/tests/rules/test_no_command.py +++ b/tests/rules/test_no_command.py @@ -6,7 +6,7 @@ from thefuck.types import Command @pytest.fixture(autouse=True) def get_all_executables(mocker): mocker.patch('thefuck.rules.no_command.get_all_executables', - return_value=['vim', 'fsck', 'git', 'go']) + return_value=['vim', 'fsck', 'git', 'go', 'python']) @pytest.fixture(autouse=True) @@ -20,6 +20,7 @@ def history_without_current(mocker): @pytest.mark.parametrize('script, output', [ ('vom file.py', 'vom: not found'), ('fucck', 'fucck: not found'), + ('puthon', "'puthon' is not recognized as an internal or external command"), ('got commit', 'got: command not found')]) def test_match(mocker, script, output): mocker.patch('thefuck.rules.no_command.which', return_value=None) diff --git a/thefuck/rules/no_command.py b/thefuck/rules/no_command.py index 0e5a22d0..03e023b3 100644 --- a/thefuck/rules/no_command.py +++ b/thefuck/rules/no_command.py @@ -6,7 +6,8 @@ from thefuck.specific.sudo import sudo_support @sudo_support def match(command): return (not which(command.script_parts[0]) - and 'not found' in command.output + and ('not found' in command.output + or 'is not recognized as' in command.output) and bool(get_close_matches(command.script_parts[0], get_all_executables()))) From ba949f7fd97abfc9caeb7df671be3f2fb029dbcc Mon Sep 17 00:00:00 2001 From: Pablo Aguiar Date: Mon, 27 May 2019 18:23:45 +0200 Subject: [PATCH 09/16] #N/A: Add `pyenv_no_such_command` rule (#919) --- README.md | 1 + tests/rules/test_pyenv_no_such_command.py | 52 +++++++++++++++++++++++ thefuck/rules/pyenv_no_such_command.py | 33 ++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 tests/rules/test_pyenv_no_such_command.py create mode 100644 thefuck/rules/pyenv_no_such_command.py diff --git a/README.md b/README.md index b6e756cf..a6788565 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,7 @@ following rules are enabled by default: * `php_s` – replaces `-s` by `-S` when trying to run a local php server; * `port_already_in_use` – kills process that bound port; * `prove_recursively` – adds `-r` when called with directory; +* `pyenv_no_such_command` – fixes wrong pyenv commands like `pyenv isntall` or `pyenv list`; * `python_command` – prepends `python` when you try to run non-executable/without `./` python script; * `python_execute` – appends missing `.py` when executing Python files; * `quotation_marks` – fixes uneven usage of `'` and `"` when containing args'; diff --git a/tests/rules/test_pyenv_no_such_command.py b/tests/rules/test_pyenv_no_such_command.py new file mode 100644 index 00000000..298620f7 --- /dev/null +++ b/tests/rules/test_pyenv_no_such_command.py @@ -0,0 +1,52 @@ +import pytest + +from thefuck.rules.pyenv_no_such_command import get_new_command, match +from thefuck.types import Command + + +@pytest.fixture +def output(pyenv_cmd): + return "pyenv: no such command `{}'".format(pyenv_cmd) + + +@pytest.fixture(autouse=True) +def Popen(mocker): + mock = mocker.patch('thefuck.rules.pyenv_no_such_command.Popen') + mock.return_value.stdout.readlines.return_value = ( + b'--version\nactivate\ncommands\ncompletions\ndeactivate\nexec_\n' + b'global\nhelp\nhooks\ninit\ninstall\nlocal\nprefix_\n' + b'realpath.dylib\nrehash\nroot\nshell\nshims\nuninstall\nversion_\n' + b'version-file\nversion-file-read\nversion-file-write\nversion-name_\n' + b'version-origin\nversions\nvirtualenv\nvirtualenv-delete_\n' + b'virtualenv-init\nvirtualenv-prefix\nvirtualenvs_\n' + b'virtualenvwrapper\nvirtualenvwrapper_lazy\nwhence\nwhich_\n' + ).split() + return mock + + +@pytest.mark.parametrize('script, pyenv_cmd', [ + ('pyenv globe', 'globe'), + ('pyenv intall 3.8.0', 'intall'), + ('pyenv list', 'list'), +]) +def test_match(script, pyenv_cmd, output): + assert match(Command(script, output=output)) + + +@pytest.mark.parametrize('script, output', [ + ('pyenv global', 'system'), + ('pyenv versions', ' 3.7.0\n 3.7.1\n* 3.7.2\n'), + ('pyenv install --list', ' 3.7.0\n 3.7.1\n 3.7.2\n'), +]) +def test_not_match(script, output): + assert not match(Command(script, output=output)) + + +@pytest.mark.parametrize('script, pyenv_cmd, result', [ + ('pyenv globe', 'globe', 'pyenv global'), + ('pyenv intall 3.8.0', 'intall', 'pyenv install 3.8.0'), + ('pyenv list', 'list', 'pyenv install --list'), + ('pyenv remove 3.8.0', 'remove', 'pyenv uninstall 3.8.0'), +]) +def test_get_new_command(script, pyenv_cmd, output, result): + assert result in get_new_command(Command(script, output)) diff --git a/thefuck/rules/pyenv_no_such_command.py b/thefuck/rules/pyenv_no_such_command.py new file mode 100644 index 00000000..cc9b609e --- /dev/null +++ b/thefuck/rules/pyenv_no_such_command.py @@ -0,0 +1,33 @@ +import re +from subprocess import PIPE, Popen + +from thefuck.utils import (cache, for_app, replace_argument, replace_command, + which) + +COMMON_TYPOS = { + 'list': ['versions', 'install --list'], + 'remove': ['uninstall'], +} + + +@for_app('pyenv') +def match(command): + return 'pyenv: no such command' in command.output + + +def get_pyenv_commands(): + proc = Popen(['pyenv', 'commands'], stdout=PIPE) + return [line.decode('utf-8').strip() for line in proc.stdout.readlines()] + + +if which('pyenv'): + get_pyenv_commands = cache(which('pyenv'))(get_pyenv_commands) + + +@for_app('pyenv') +def get_new_command(command): + broken = re.findall(r"pyenv: no such command `([^']*)'", command.output)[0] + matched = [replace_argument(command.script, broken, common_typo) + for common_typo in COMMON_TYPOS.get(broken, [])] + matched.extend(replace_command(command, broken, get_pyenv_commands())) + return matched From ff2944086d06fe48a29f646f82d831943874c1f8 Mon Sep 17 00:00:00 2001 From: Pablo Aguiar Date: Mon, 27 May 2019 18:24:55 +0200 Subject: [PATCH 10/16] #N/A: Improve how version is fetched for all shells (#920) --- tests/shells/test_bash.py | 16 +++++++++++++--- tests/shells/test_fish.py | 14 ++++++++++++-- tests/shells/test_generic.py | 11 +++++++++++ tests/shells/test_powershell.py | 22 ++++++++++++++++++++++ tests/shells/test_tcsh.py | 14 ++++++++++++++ tests/shells/test_zsh.py | 16 +++++++++++++--- thefuck/shells/bash.py | 9 +++++---- thefuck/shells/fish.py | 12 ++++++------ thefuck/shells/generic.py | 13 ++++++++++++- thefuck/shells/powershell.py | 17 +++++++++++++++++ thefuck/shells/tcsh.py | 7 +++++++ thefuck/shells/zsh.py | 9 +++++---- 12 files changed, 137 insertions(+), 23 deletions(-) diff --git a/tests/shells/test_bash.py b/tests/shells/test_bash.py index 1bb36651..9a55eb5e 100644 --- a/tests/shells/test_bash.py +++ b/tests/shells/test_bash.py @@ -11,6 +11,11 @@ class TestBash(object): def shell(self): return Bash() + @pytest.fixture(autouse=True) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.bash.Popen') + return mock + @pytest.fixture(autouse=True) def shell_aliases(self): os.environ['TF_SHELL_ALIASES'] = ( @@ -74,7 +79,12 @@ class TestBash(object): config_exists.return_value = False assert not shell.how_to_configure().can_configure_automatically - def test_info(self, shell, mocker): - patch = mocker.patch('thefuck.shells.bash.Popen') - patch.return_value.stdout.read.side_effect = [b'3.5.9'] + def test_info(self, shell, Popen): + Popen.return_value.stdout.read.side_effect = [b'3.5.9'] assert shell.info() == 'Bash 3.5.9' + + def test_get_version_error(self, shell, Popen): + Popen.return_value.stdout.read.side_effect = OSError + with pytest.raises(OSError): + shell._get_version() + assert Popen.call_args[0][0] == ['bash', '-c', 'echo $BASH_VERSION'] diff --git a/tests/shells/test_fish.py b/tests/shells/test_fish.py index 172f6f49..ff627a4e 100644 --- a/tests/shells/test_fish.py +++ b/tests/shells/test_fish.py @@ -116,7 +116,17 @@ class TestFish(object): config_exists.return_value = False assert not shell.how_to_configure().can_configure_automatically - def test_info(self, shell, Popen): + def test_get_version(self, shell, Popen): Popen.return_value.stdout.read.side_effect = [b'fish, version 3.5.9\n'] - assert shell.info() == 'Fish Shell 3.5.9' + assert shell._get_version() == '3.5.9' + assert Popen.call_args[0][0] == ['fish', '--version'] + + @pytest.mark.parametrize('side_effect, exception', [ + ([b'\n'], IndexError), + (OSError('file not found'), OSError), + ]) + def test_get_version_error(self, side_effect, exception, shell, Popen): + Popen.return_value.stdout.read.side_effect = side_effect + with pytest.raises(exception): + shell._get_version() assert Popen.call_args[0][0] == ['fish', '--version'] diff --git a/tests/shells/test_generic.py b/tests/shells/test_generic.py index a0535ff6..05f20e63 100644 --- a/tests/shells/test_generic.py +++ b/tests/shells/test_generic.py @@ -43,3 +43,14 @@ class TestGeneric(object): def test_how_to_configure(self, shell): assert shell.how_to_configure() is None + + @pytest.mark.parametrize('side_effect, expected_info, warn', [ + ([u'3.5.9'], u'Generic Shell 3.5.9', False), + ([OSError], u'Generic Shell', True), + ]) + def test_info(self, side_effect, expected_info, warn, shell, mocker): + warn_mock = mocker.patch('thefuck.shells.generic.warn') + shell._get_version = mocker.Mock(side_effect=side_effect) + assert shell.info() == expected_info + assert warn_mock.called is warn + assert shell._get_version.called diff --git a/tests/shells/test_powershell.py b/tests/shells/test_powershell.py index dd6b3628..417174fb 100644 --- a/tests/shells/test_powershell.py +++ b/tests/shells/test_powershell.py @@ -10,6 +10,11 @@ class TestPowershell(object): def shell(self): return Powershell() + @pytest.fixture(autouse=True) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.powershell.Popen') + return mock + def test_and_(self, shell): assert shell.and_('ls', 'cd') == '(ls) -and (cd)' @@ -20,3 +25,20 @@ class TestPowershell(object): def test_how_to_configure(self, shell): assert not shell.how_to_configure().can_configure_automatically + + @pytest.mark.parametrize('side_effect, expected_version, call_args', [ + ([b'''Major Minor Build Revision +----- ----- ----- -------- +5 1 17763 316 \n'''], 'PowerShell 5.1.17763.316', ['powershell.exe']), + ([IOError, b'PowerShell 6.1.2\n'], 'PowerShell 6.1.2', ['powershell.exe', 'pwsh'])]) + def test_info(self, side_effect, expected_version, call_args, shell, Popen): + Popen.return_value.stdout.read.side_effect = side_effect + assert shell.info() == expected_version + assert Popen.call_count == len(call_args) + assert all([Popen.call_args_list[i][0][0][0] == call_arg for i, call_arg in enumerate(call_args)]) + + def test_get_version_error(self, shell, Popen): + Popen.return_value.stdout.read.side_effect = RuntimeError + with pytest.raises(RuntimeError): + shell._get_version() + assert Popen.call_args[0][0] == ['powershell.exe', '$PSVersionTable.PSVersion'] diff --git a/tests/shells/test_tcsh.py b/tests/shells/test_tcsh.py index 014c3c95..f024b589 100644 --- a/tests/shells/test_tcsh.py +++ b/tests/shells/test_tcsh.py @@ -61,3 +61,17 @@ class TestTcsh(object): config_exists): config_exists.return_value = False assert not shell.how_to_configure().can_configure_automatically + + def test_info(self, shell, Popen): + Popen.return_value.stdout.read.side_effect = [ + b'tcsh 6.20.00 (Astron) 2016-11-24 (unknown-unknown-bsd44) \n'] + assert shell.info() == 'Tcsh 6.20.00' + assert Popen.call_args[0][0] == ['tcsh', '--version'] + + @pytest.mark.parametrize('side_effect, exception', [ + ([b'\n'], IndexError), (OSError, OSError)]) + def test_get_version_error(self, side_effect, exception, shell, Popen): + Popen.return_value.stdout.read.side_effect = side_effect + with pytest.raises(exception): + shell._get_version() + assert Popen.call_args[0][0] == ['tcsh', '--version'] diff --git a/tests/shells/test_zsh.py b/tests/shells/test_zsh.py index cbbf3f57..05b3d62e 100644 --- a/tests/shells/test_zsh.py +++ b/tests/shells/test_zsh.py @@ -11,6 +11,11 @@ class TestZsh(object): def shell(self): return Zsh() + @pytest.fixture(autouse=True) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.zsh.Popen') + return mock + @pytest.fixture(autouse=True) def shell_aliases(self): os.environ['TF_SHELL_ALIASES'] = ( @@ -69,7 +74,12 @@ class TestZsh(object): config_exists.return_value = False assert not shell.how_to_configure().can_configure_automatically - def test_info(self, shell, mocker): - patch = mocker.patch('thefuck.shells.zsh.Popen') - patch.return_value.stdout.read.side_effect = [b'3.5.9'] + def test_info(self, shell, Popen): + Popen.return_value.stdout.read.side_effect = [b'3.5.9'] assert shell.info() == 'ZSH 3.5.9' + + def test_get_version_error(self, shell, Popen): + Popen.return_value.stdout.read.side_effect = OSError + with pytest.raises(OSError): + shell._get_version() + assert Popen.call_args[0][0] == ['zsh', '-c', 'echo $ZSH_VERSION'] diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index df972380..fa7a2072 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -9,6 +9,8 @@ from .generic import Generic class Bash(Generic): + friendly_name = 'Bash' + def app_alias(self, alias_name): # It is VERY important to have the variables declared WITHIN the function return ''' @@ -83,9 +85,8 @@ class Bash(Generic): path=config, reload=u'source {}'.format(config)) - def info(self): - """Returns the name and version of the current shell""" + def _get_version(self): + """Returns the version of the current shell""" proc = Popen(['bash', '-c', 'echo $BASH_VERSION'], stdout=PIPE, stderr=DEVNULL) - version = proc.stdout.read().decode('utf-8').strip() - return u'Bash {}'.format(version) + return proc.stdout.read().decode('utf-8').strip() diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 2a89db6f..51478192 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -38,6 +38,8 @@ def _get_aliases(overridden): class Fish(Generic): + friendly_name = 'Fish Shell' + def _get_overridden_aliases(self): overridden = os.environ.get('THEFUCK_OVERRIDDEN_ALIASES', os.environ.get('TF_OVERRIDDEN_ALIASES', '')) @@ -104,12 +106,10 @@ class Fish(Generic): path='~/.config/fish/config.fish', reload='fish') - def info(self): - """Returns the name and version of the current shell""" - proc = Popen(['fish', '--version'], - stdout=PIPE, stderr=DEVNULL) - version = proc.stdout.read().decode('utf-8').split()[-1] - return u'Fish Shell {}'.format(version) + def _get_version(self): + """Returns the version of the current shell""" + proc = Popen(['fish', '--version'], stdout=PIPE, stderr=DEVNULL) + return proc.stdout.read().decode('utf-8').split()[-1] def put_to_history(self, command): try: diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index d7a936cb..aa81e2ac 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -14,6 +14,8 @@ ShellConfiguration = namedtuple('ShellConfiguration', ( class Generic(object): + friendly_name = 'Generic Shell' + def get_aliases(self): return {} @@ -131,9 +133,18 @@ class Generic(object): 'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset', 'until', 'wait', 'while'] + def _get_version(self): + """Returns the version of the current shell""" + return '' + def info(self): """Returns the name and version of the current shell""" - return 'Generic Shell' + try: + version = self._get_version() + except Exception as e: + warn(u'Could not determine shell version: {}'.format(e)) + version = '' + return u'{} {}'.format(self.friendly_name, version).rstrip() def _create_shell_configuration(self, content, path, reload): return ShellConfiguration( diff --git a/thefuck/shells/powershell.py b/thefuck/shells/powershell.py index 19daf64b..59b30ba2 100644 --- a/thefuck/shells/powershell.py +++ b/thefuck/shells/powershell.py @@ -1,7 +1,11 @@ +from subprocess import Popen, PIPE +from ..utils import DEVNULL from .generic import Generic, ShellConfiguration class Powershell(Generic): + friendly_name = 'PowerShell' + def app_alias(self, alias_name): return 'function ' + alias_name + ' {\n' \ ' $history = (Get-History -Count 1).CommandLine;\n' \ @@ -24,3 +28,16 @@ class Powershell(Generic): path='$profile', reload='& $profile', can_configure_automatically=False) + + def _get_version(self): + """Returns the version of the current shell""" + try: + proc = Popen( + ['powershell.exe', '$PSVersionTable.PSVersion'], + stdout=PIPE, + stderr=DEVNULL) + version = proc.stdout.read().decode('utf-8').rstrip().split('\n') + return '.'.join(version[-1].split()) + except IOError: + proc = Popen(['pwsh', '--version'], stdout=PIPE, stderr=DEVNULL) + return proc.stdout.read().decode('utf-8').split()[-1] diff --git a/thefuck/shells/tcsh.py b/thefuck/shells/tcsh.py index 0911f046..b9075f93 100644 --- a/thefuck/shells/tcsh.py +++ b/thefuck/shells/tcsh.py @@ -6,6 +6,8 @@ from .generic import Generic class Tcsh(Generic): + friendly_name = 'Tcsh' + def app_alias(self, alias_name): return ("alias {0} 'setenv TF_SHELL tcsh && setenv TF_ALIAS {0} && " "set fucked_cmd=`history -h 2 | head -n 1` && " @@ -35,3 +37,8 @@ class Tcsh(Generic): content=u'eval `thefuck --alias`', path='~/.tcshrc', reload='tcsh') + + def _get_version(self): + """Returns the version of the current shell""" + proc = Popen(['tcsh', '--version'], stdout=PIPE, stderr=DEVNULL) + return proc.stdout.read().decode('utf-8').split()[1] diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py index da0217ae..e1fdf207 100644 --- a/thefuck/shells/zsh.py +++ b/thefuck/shells/zsh.py @@ -10,6 +10,8 @@ from .generic import Generic class Zsh(Generic): + friendly_name = 'ZSH' + def app_alias(self, alias_name): # It is VERY important to have the variables declared WITHIN the function return ''' @@ -87,9 +89,8 @@ class Zsh(Generic): path='~/.zshrc', reload='source ~/.zshrc') - def info(self): - """Returns the name and version of the current shell""" + def _get_version(self): + """Returns the version of the current shell""" proc = Popen(['zsh', '-c', 'echo $ZSH_VERSION'], stdout=PIPE, stderr=DEVNULL) - version = proc.stdout.read().decode('utf-8').strip() - return u'ZSH {}'.format(version) + return proc.stdout.read().decode('utf-8').strip() From 59e1f7b122468e0a6530ef3bb2b5a25ff74572c6 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 27 May 2019 18:29:04 +0200 Subject: [PATCH 11/16] Bump to 3.29 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 751aa1d0..7beb16f7 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ elif (3, 0) < version < (3, 4): ' ({}.{} detected).'.format(*version)) sys.exit(-1) -VERSION = '3.28' +VERSION = '3.29' install_requires = ['psutil', 'colorama', 'six', 'decorator', 'pyte'] extras_require = {':python_version<"3.4"': ['pathlib2'], From 59dc6cbf906d910af04ff54fc339d85f837d0fe2 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 27 May 2019 18:32:48 +0200 Subject: [PATCH 12/16] #N/A: Fix the release script --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index da29cece..82c07f3b 100755 --- a/release.py +++ b/release.py @@ -32,6 +32,6 @@ call('git push --tags', shell=True) env = os.environ env['CONVERT_README'] = 'true' -call('rm -rf dist/*') +call('rm -rf dist/*', shell=True, env=env) call('python setup.py sdist bdist_wheel', shell=True, env=env) call('twine upload dist/*', shell=True, env=env) From 48e1e4217fd112b89c409135466ea73e38071280 Mon Sep 17 00:00:00 2001 From: Tycho Grouwstra Date: Wed, 26 Jun 2019 20:01:02 +0200 Subject: [PATCH 13/16] support nixos command-not-found, closes #912 (#922) --- README.md | 1 + tests/rules/test_nixos_cmd_not_found.py | 25 +++++++++++++++++++++++++ thefuck/rules/nixos_cmd_not_found.py | 15 +++++++++++++++ thefuck/specific/nix.py | 3 +++ 4 files changed, 44 insertions(+) create mode 100644 tests/rules/test_nixos_cmd_not_found.py create mode 100644 thefuck/rules/nixos_cmd_not_found.py create mode 100644 thefuck/specific/nix.py diff --git a/README.md b/README.md index a6788565..26cf0d0f 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,7 @@ The following rules are enabled by default on specific platforms only: * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `brew_update_formula` – turns `brew update ` into `brew upgrade `; * `dnf_no_such_command` – fixes mistyped DNF commands; +* `nixos_cmd_not_found` – installs apps on NixOS; * `pacman` – installs app with `pacman` if it is not installed (uses `yay` or `yaourt` if available); * `pacman_not_found` – fixes package name with `pacman`, `yay` or `yaourt`. diff --git a/tests/rules/test_nixos_cmd_not_found.py b/tests/rules/test_nixos_cmd_not_found.py new file mode 100644 index 00000000..43703345 --- /dev/null +++ b/tests/rules/test_nixos_cmd_not_found.py @@ -0,0 +1,25 @@ +import pytest +from thefuck.rules.nixos_cmd_not_found import match, get_new_command +from thefuck.types import Command + + +@pytest.mark.parametrize('command', [ + Command('vim', 'nix-env -iA nixos.vim')]) +def test_match(mocker, command): + mocker.patch('thefuck.rules.nixos_cmd_not_found', return_value=None) + assert match(command) + + +@pytest.mark.parametrize('command', [ + Command('vim', ''), + Command('', '')]) +def test_not_match(mocker, command): + mocker.patch('thefuck.rules.nixos_cmd_not_found', return_value=None) + assert not match(command) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('vim', 'nix-env -iA nixos.vim'), 'nix-env -iA nixos.vim && vim'), + (Command('pacman', 'nix-env -iA nixos.pacman'), 'nix-env -iA nixos.pacman && pacman')]) +def test_get_new_command(mocker, command, new_command): + assert get_new_command(command) == new_command diff --git a/thefuck/rules/nixos_cmd_not_found.py b/thefuck/rules/nixos_cmd_not_found.py new file mode 100644 index 00000000..40d9d550 --- /dev/null +++ b/thefuck/rules/nixos_cmd_not_found.py @@ -0,0 +1,15 @@ +import re +from thefuck.specific.nix import nix_available +from thefuck.shells import shell + +regex = re.compile(r'nix-env -iA ([^\s]*)') +enabled_by_default = nix_available + + +def match(command): + return regex.findall(command.output) + + +def get_new_command(command): + name = regex.findall(command.output)[0] + return shell.and_('nix-env -iA {}'.format(name), command.script) diff --git a/thefuck/specific/nix.py b/thefuck/specific/nix.py new file mode 100644 index 00000000..87b1f1d7 --- /dev/null +++ b/thefuck/specific/nix.py @@ -0,0 +1,3 @@ +from thefuck.utils import which + +nix_available = bool(which('nix')) From e047c1eb40b8b1b05cbb013213406a82676ee215 Mon Sep 17 00:00:00 2001 From: Pablo Aguiar Date: Wed, 26 Jun 2019 15:01:38 -0300 Subject: [PATCH 14/16] #921: Try printing alias before trying to fix a command (#923) Fixes #921 --- thefuck/entrypoints/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/thefuck/entrypoints/main.py b/thefuck/entrypoints/main.py index 01346870..865b9ca1 100644 --- a/thefuck/entrypoints/main.py +++ b/thefuck/entrypoints/main.py @@ -22,10 +22,13 @@ def main(): elif known_args.version: logs.version(get_installation_info().version, sys.version.split()[0], shell.info()) - elif known_args.command or 'TF_HISTORY' in os.environ: - fix_command(known_args) + # It's important to check if an alias is being requested before checking if + # `TF_HISTORY` is in `os.environ`, otherwise it might mess with subshells. + # Check https://github.com/nvbn/thefuck/issues/921 for reference elif known_args.alias: print_alias(known_args) + elif known_args.command or 'TF_HISTORY' in os.environ: + fix_command(known_args) elif known_args.shell_logger: try: from .shell_logger import shell_logger # noqa: E402 From 4c3a55912456f77570488481cb61234ad561dc23 Mon Sep 17 00:00:00 2001 From: Mathieu Cantin <41298167+mathcantin@users.noreply.github.com> Date: Wed, 26 Jun 2019 14:02:01 -0400 Subject: [PATCH 15/16] Added rules to run terraform init before terraform plan or apply (#924) * Run terraform init to initialize terraform modules * Fix indent * Add unit tests for terraform_init.py --- README.md | 1 + tests/rules/test_terraform_init.py | 33 ++++++++++++++++++++++++++++++ thefuck/rules/terraform_init.py | 13 ++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 tests/rules/test_terraform_init.py create mode 100644 thefuck/rules/terraform_init.py diff --git a/README.md b/README.md index 26cf0d0f..d9424dfd 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,7 @@ following rules are enabled by default: * `sudo_command_from_user_path` – runs commands from users `$PATH` with `sudo`; * `switch_lang` – switches command from your local layout to en; * `systemctl` – correctly orders parameters of confusing `systemctl`; +* `terraform_init.py` – run `terraform init` before plan or apply; * `test.py` – runs `py.test` instead of `test.py`; * `touch` – creates missing directories before "touching"; * `tsuru_login` – runs `tsuru login` if not authenticated or session expired; diff --git a/tests/rules/test_terraform_init.py b/tests/rules/test_terraform_init.py new file mode 100644 index 00000000..1bd744c2 --- /dev/null +++ b/tests/rules/test_terraform_init.py @@ -0,0 +1,33 @@ +import pytest +from thefuck.rules.terraform_init import match, get_new_command +from thefuck.types import Command + + +@pytest.mark.parametrize('script, output', [ + ('terraform plan', 'Error: Initialization required. ' + 'Please see the error message above.'), + ('terraform plan', 'This module is not yet installed. Run "terraform init" ' + 'to install all modules required by this configuration.'), + ('terraform apply', 'Error: Initialization required. ' + 'Please see the error message above.'), + ('terraform apply', 'This module is not yet installed. Run "terraform init" ' + 'to install all modules required by this configuration.')]) +def test_match(script, output): + assert match(Command(script, output)) + + +@pytest.mark.parametrize('script, output', [ + ('terraform --version', 'Terraform v0.12.2'), + ('terraform plan', 'No changes. Infrastructure is up-to-date.'), + ('terraform apply', 'Apply complete! Resources: 0 added, 0 changed, 0 destroyed.'), +]) +def test_not_match(script, output): + assert not match(Command(script, output=output)) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('terraform plan', ''), 'terraform init && terraform plan'), + (Command('terraform apply', ''), 'terraform init && terraform apply'), +]) +def test_get_new_command(command, new_command): + assert get_new_command(command) == new_command diff --git a/thefuck/rules/terraform_init.py b/thefuck/rules/terraform_init.py new file mode 100644 index 00000000..29dd44fd --- /dev/null +++ b/thefuck/rules/terraform_init.py @@ -0,0 +1,13 @@ +from thefuck.shells import shell +from thefuck.utils import for_app + + +@for_app('terraform') +def match(command): + return ('this module is not yet installed' in command.output.lower() or + 'initialization required' in command.output.lower() + ) + + +def get_new_command(command): + return shell.and_('terraform init', command.script) From 01a5ba99d0648bbb54193d36878141fb6ab363c5 Mon Sep 17 00:00:00 2001 From: Connor Martin Date: Wed, 10 Jul 2019 13:34:20 -0500 Subject: [PATCH 16/16] Docker remove container before remove image (#928) * add docker container removal * remove container before deleting image * update readme * clean up and add assert not test * test not docker command * use shell.and_ correctly --- README.md | 1 + ...st_docker_image_being_used_by_container.py | 27 +++++++++++++++++++ .../docker_image_being_used_by_container.py | 20 ++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 tests/rules/test_docker_image_being_used_by_container.py create mode 100644 thefuck/rules/docker_image_being_used_by_container.py diff --git a/README.md b/README.md index d9424dfd..83830076 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ following rules are enabled by default: * `django_south_merge` – adds `--merge` to inconsistent django south migration; * `docker_login` – executes a `docker login` and repeats the previous command; * `docker_not_command` – fixes wrong docker commands like `docker tags`; +* `docker_image_being_used_by_container` ‐ removes the container that is using the image before removing the image; * `dry` – fixes repetitions like `git git push`; * `fab_command_not_found` – fix misspelled fabric commands; * `fix_alt_space` – replaces Alt+Space with Space character; diff --git a/tests/rules/test_docker_image_being_used_by_container.py b/tests/rules/test_docker_image_being_used_by_container.py new file mode 100644 index 00000000..00d9e674 --- /dev/null +++ b/tests/rules/test_docker_image_being_used_by_container.py @@ -0,0 +1,27 @@ +from thefuck.rules.docker_image_being_used_by_container import match, get_new_command +from thefuck.types import Command + + +def test_match(): + err_response = """Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image is being used by running container e5e2591040d1""" + assert match(Command('docker image rm -f cd809b04b6ff', err_response)) + + +def test_not_match(): + err_response = 'bash: docker: command not found' + assert not match(Command('docker image rm -f cd809b04b6ff', err_response)) + + +def test_not_docker_command(): + err_response = """Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image is being used by running container e5e2591040d1""" + assert not match(Command('git image rm -f cd809b04b6ff', err_response)) + + +def test_get_new_command(): + err_response = """ + Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image + is being used by running container e5e2591040d1 + """ + result = get_new_command(Command('docker image rm -f cd809b04b6ff', err_response)) + expected = 'docker container rm -f e5e2591040d1 && docker image rm -f cd809b04b6ff' + assert result == expected diff --git a/thefuck/rules/docker_image_being_used_by_container.py b/thefuck/rules/docker_image_being_used_by_container.py new file mode 100644 index 00000000..d68f9192 --- /dev/null +++ b/thefuck/rules/docker_image_being_used_by_container.py @@ -0,0 +1,20 @@ +from thefuck.utils import for_app +from thefuck.shells import shell + + +@for_app('docker') +def match(command): + ''' + Matches a command's output with docker's output + warning you that you need to remove a container before removing an image. + ''' + return 'image is being used by running container' in command.output + + +def get_new_command(command): + ''' + Prepends docker container rm -f {container ID} to + the previous docker image rm {image ID} command + ''' + container_id = command.output.strip().split(' ') + return shell.and_('docker container rm -f {}', '{}').format(container_id[-1], command.script)