From db4b37910d1fbab3df4294b6aa3ff896829e9150 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 3 Oct 2016 00:30:48 -0400 Subject: [PATCH 01/62] Suggest `ag -Q` when relevant This detects when `ag` suggests the `-Q` option, and adds it. --- README.md | 1 + tests/rules/test_ag_literal.py | 25 +++++++++++++++++++++++++ thefuck/rules/ag_literal.py | 11 +++++++++++ 3 files changed, 37 insertions(+) create mode 100644 tests/rules/test_ag_literal.py create mode 100644 thefuck/rules/ag_literal.py diff --git a/README.md b/README.md index b2f169cd..77b30641 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ sudo -H pip install thefuck --upgrade The Fuck tries to match a rule for the previous command, creates a new command using the matched rule and runs it. Rules enabled by default are as follows: +* `ag_literal` – adds `-Q` to `ag` when suggested; * `aws_cli` – fixes misspelled commands like `aws dynamdb scan` * `cargo` – runs `cargo build` instead of `cargo`; * `cargo_no_command` – fixes wrongs commands like `cargo buid`; diff --git a/tests/rules/test_ag_literal.py b/tests/rules/test_ag_literal.py new file mode 100644 index 00000000..660786d9 --- /dev/null +++ b/tests/rules/test_ag_literal.py @@ -0,0 +1,25 @@ +import pytest +from thefuck.rules.ag_literal import match, get_new_command +from tests.utils import Command + +stderr = ('ERR: Bad regex! pcre_compile() failed at position 1: missing )\n' + 'If you meant to search for a literal string, run ag with -Q\n') + +matching_command = Command(script='ag \\(', stderr=stderr) + +@pytest.mark.parametrize('command', [ + matching_command]) +def test_match(command): + assert match(matching_command) + + +@pytest.mark.parametrize('command', [ + Command(script='ag foo', stderr='')]) +def test_not_match(command): + assert not match(command) + + +@pytest.mark.parametrize('command, new_command', [ + (matching_command, 'ag -Q \\(')]) +def test_get_new_command(command, new_command): + assert get_new_command(command) == new_command diff --git a/thefuck/rules/ag_literal.py b/thefuck/rules/ag_literal.py new file mode 100644 index 00000000..4014eeda --- /dev/null +++ b/thefuck/rules/ag_literal.py @@ -0,0 +1,11 @@ +from thefuck.utils import for_app +from thefuck.utils import replace_argument + + +@for_app('ag') +def match(command): + return 'run ag with -Q' in command.stderr + + +def get_new_command(command): + return command.script.replace('ag', 'ag -Q', 1) From 5dbbb3b1ed9b3eb7ac7ec3eb155f7ded3b6f348e Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 3 Oct 2016 03:54:13 -0400 Subject: [PATCH 02/62] Add `... --help` to `man` suggestions This is along the lines of what @waldyrious suggested in https://github.com/nvbn/thefuck/issues/546, but it just adds a new suggestion rather than replacing the other ones. --- tests/rules/test_man.py | 2 +- thefuck/rules/man.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/rules/test_man.py b/tests/rules/test_man.py index 01eab173..1c9095ca 100644 --- a/tests/rules/test_man.py +++ b/tests/rules/test_man.py @@ -23,7 +23,7 @@ def test_not_match(command): @pytest.mark.parametrize('command, new_command', [ - (Command('man read'), ['man 3 read', 'man 2 read']), + (Command('man read'), ['man 3 read', 'man 2 read', 'read --help']), (Command('man 2 read'), 'man 3 read'), (Command('man 3 read'), 'man 2 read'), (Command('man -s2 read'), 'man -s3 read'), diff --git a/thefuck/rules/man.py b/thefuck/rules/man.py index ead1361b..6fcea5af 100644 --- a/thefuck/rules/man.py +++ b/thefuck/rules/man.py @@ -18,4 +18,10 @@ def get_new_command(command): split_cmd2.insert(1, ' 2 ') split_cmd3.insert(1, ' 3 ') - return ["".join(split_cmd3), "".join(split_cmd2)] + last_arg = command.script_parts[-1] + + return [ + "".join(split_cmd3), + "".join(split_cmd2), + last_arg + ' --help', + ] From 8bd6c5da67e55c64257345efa4e3cc454c42475c Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 3 Oct 2016 11:54:29 -0400 Subject: [PATCH 03/62] For `man foo`, try `foo --help` before `man 3 foo` `man` without a section searches all sections, so having `foo --help` suggested first makes more sense than adding a specific section. See https://github.com/nvbn/thefuck/pull/562#issuecomment-251142710 However, in cases where multiple sections have man pages for `foo`, running `man foo` could bring up the "wrong" section of man pages. `man read` is an example of this, but that should probably be handled in a way that still suggests `foo --help` first when there are *no* man pages for `foo` in any section. Closes https://github.com/nvbn/thefuck/issues/546 --- tests/rules/test_man.py | 2 +- thefuck/rules/man.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rules/test_man.py b/tests/rules/test_man.py index 1c9095ca..b51b1165 100644 --- a/tests/rules/test_man.py +++ b/tests/rules/test_man.py @@ -23,7 +23,7 @@ def test_not_match(command): @pytest.mark.parametrize('command, new_command', [ - (Command('man read'), ['man 3 read', 'man 2 read', 'read --help']), + (Command('man read'), ['read --help', 'man 3 read', 'man 2 read']), (Command('man 2 read'), 'man 3 read'), (Command('man 3 read'), 'man 2 read'), (Command('man -s2 read'), 'man -s3 read'), diff --git a/thefuck/rules/man.py b/thefuck/rules/man.py index 6fcea5af..3d0347a8 100644 --- a/thefuck/rules/man.py +++ b/thefuck/rules/man.py @@ -21,7 +21,7 @@ def get_new_command(command): last_arg = command.script_parts[-1] return [ + last_arg + ' --help', "".join(split_cmd3), "".join(split_cmd2), - last_arg + ' --help', ] From 0c84eefa55fc1b4bc4940b41d74568884344e35c Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 3 Oct 2016 13:13:35 -0400 Subject: [PATCH 04/62] Don't suggest `man 2/3 foo` if no man pages exist Suggest `foo --help` instead. However, if there are man pages, suggest `foo --help` after `man 2/3 foo` This addresses the comment in the previous commit message: > However, in cases where multiple sections have man pages for `foo`, > running `man foo` could bring up the "wrong" section of man pages. > `man read` is an example of this, but that should probably be handled in > a way that still suggests `foo --help` first when there are *no* man > pages for `foo` in any section. --- tests/rules/test_man.py | 3 ++- thefuck/rules/man.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/rules/test_man.py b/tests/rules/test_man.py index b51b1165..c4714881 100644 --- a/tests/rules/test_man.py +++ b/tests/rules/test_man.py @@ -23,7 +23,8 @@ def test_not_match(command): @pytest.mark.parametrize('command, new_command', [ - (Command('man read'), ['read --help', 'man 3 read', 'man 2 read']), + (Command('man read'), ['man 3 read', 'man 2 read', 'read --help']), + (Command('man missing', stderr="No manual entry for missing\n"), ['missing --help']), (Command('man 2 read'), 'man 3 read'), (Command('man 3 read'), 'man 2 read'), (Command('man -s2 read'), 'man -s3 read'), diff --git a/thefuck/rules/man.py b/thefuck/rules/man.py index 3d0347a8..e4ec54d9 100644 --- a/thefuck/rules/man.py +++ b/thefuck/rules/man.py @@ -12,16 +12,22 @@ def get_new_command(command): if '2' in command.script: return command.script.replace("2", "3") + last_arg = command.script_parts[-1] + help_command = last_arg + ' --help' + + # If there are no man pages for last_arg, suggest `last_arg --help` instead. + # Otherwise, suggest `--help` after suggesting other man page sections. + if command.stderr.strip() == 'No manual entry for ' + last_arg: + return [help_command] + split_cmd2 = command.script_parts split_cmd3 = split_cmd2[:] split_cmd2.insert(1, ' 2 ') split_cmd3.insert(1, ' 3 ') - last_arg = command.script_parts[-1] - return [ - last_arg + ' --help', "".join(split_cmd3), "".join(split_cmd2), + help_command, ] From d2e0a19aae9483906c397d21bdadbc8f3f1d6d8f Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 3 Oct 2016 00:41:34 -0400 Subject: [PATCH 05/62] Add missing semicolon to aws_cli entry in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77b30641..e02b7813 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ The Fuck tries to match a rule for the previous command, creates a new command using the matched rule and runs it. Rules enabled by default are as follows: * `ag_literal` – adds `-Q` to `ag` when suggested; -* `aws_cli` – fixes misspelled commands like `aws dynamdb scan` +* `aws_cli` – fixes misspelled commands like `aws dynamdb scan`; * `cargo` – runs `cargo build` instead of `cargo`; * `cargo_no_command` – fixes wrongs commands like `cargo buid`; * `cd_correction` – spellchecks and correct failed cd commands; From b2947aba8d4d220092d6ccfc8780b35830d78663 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 5 Oct 2016 10:32:14 -0400 Subject: [PATCH 06/62] test_ag_literal.py: Add blank line (PEP 8 E302) https://github.com/nvbn/thefuck/pull/561#discussion_r81892174 --- tests/rules/test_ag_literal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/rules/test_ag_literal.py b/tests/rules/test_ag_literal.py index 660786d9..52376740 100644 --- a/tests/rules/test_ag_literal.py +++ b/tests/rules/test_ag_literal.py @@ -7,6 +7,7 @@ stderr = ('ERR: Bad regex! pcre_compile() failed at position 1: missing )\n' matching_command = Command(script='ag \\(', stderr=stderr) + @pytest.mark.parametrize('command', [ matching_command]) def test_match(command): From 4822ceb87aee14e5bf09f0e662f7422dd198e9f0 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 5 Oct 2016 10:34:17 -0400 Subject: [PATCH 07/62] ag_literal.py: remove unused import (Flake8 F401) https://github.com/nvbn/thefuck/pull/561#discussion_r81892699 --- thefuck/rules/ag_literal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/thefuck/rules/ag_literal.py b/thefuck/rules/ag_literal.py index 4014eeda..d36d698e 100644 --- a/thefuck/rules/ag_literal.py +++ b/thefuck/rules/ag_literal.py @@ -1,5 +1,4 @@ from thefuck.utils import for_app -from thefuck.utils import replace_argument @for_app('ag') From 77fc021a6ca30040c3f27b17aa37377ea434d919 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 5 Oct 2016 10:52:24 -0400 Subject: [PATCH 08/62] Refactor tests/rules/test_ag_literal.py https://github.com/nvbn/thefuck/pull/561#discussion_r81894710 --- tests/rules/test_ag_literal.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/rules/test_ag_literal.py b/tests/rules/test_ag_literal.py index 52376740..4040d5db 100644 --- a/tests/rules/test_ag_literal.py +++ b/tests/rules/test_ag_literal.py @@ -1,26 +1,25 @@ import pytest -from thefuck.rules.ag_literal import match, get_new_command +from thefuck.rules.ag_literal import get_new_command, match from tests.utils import Command -stderr = ('ERR: Bad regex! pcre_compile() failed at position 1: missing )\n' - 'If you meant to search for a literal string, run ag with -Q\n') -matching_command = Command(script='ag \\(', stderr=stderr) +@pytest.fixture +def stderr(): + return ('ERR: Bad regex! pcre_compile() failed at position 1: missing )\n' + 'If you meant to search for a literal string, run ag with -Q\n') -@pytest.mark.parametrize('command', [ - matching_command]) -def test_match(command): - assert match(matching_command) +@pytest.mark.parametrize('script', ['ag \(']) +def test_match(script, stderr): + assert match(Command(script=script, stderr=stderr)) -@pytest.mark.parametrize('command', [ - Command(script='ag foo', stderr='')]) -def test_not_match(command): - assert not match(command) +@pytest.mark.parametrize('script', ['ag foo']) +def test_not_match(script): + assert not match(Command(script=script)) -@pytest.mark.parametrize('command, new_command', [ - (matching_command, 'ag -Q \\(')]) -def test_get_new_command(command, new_command): - assert get_new_command(command) == new_command +@pytest.mark.parametrize('script, new_cmd', [ + ('ag \(', 'ag -Q \(')]) +def test_get_new_command(script, new_cmd, stderr): + assert get_new_command((Command(script=script, stderr=stderr))) == new_cmd From a964af7e9534edae3808acde97de054c3c6dde22 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 5 Oct 2016 10:55:49 -0400 Subject: [PATCH 09/62] ag_literal.py: use `endswith()` rather than `in` https://github.com/nvbn/thefuck/pull/561#discussion_r81898499 --- thefuck/rules/ag_literal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/rules/ag_literal.py b/thefuck/rules/ag_literal.py index d36d698e..a07ae07a 100644 --- a/thefuck/rules/ag_literal.py +++ b/thefuck/rules/ag_literal.py @@ -3,7 +3,7 @@ from thefuck.utils import for_app @for_app('ag') def match(command): - return 'run ag with -Q' in command.stderr + return command.stderr.endswith('run ag with -Q\n') def get_new_command(command): From f915a6ed0ca36dde71ae56e844947bd5accb0a46 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Thu, 6 Oct 2016 10:51:36 -0400 Subject: [PATCH 10/62] test_performance.py: use python:3 image, not ubuntu This should help reduce build times. --- tests/functional/test_performance.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_performance.py b/tests/functional/test_performance.py index d935aec1..b410114e 100644 --- a/tests/functional/test_performance.py +++ b/tests/functional/test_performance.py @@ -2,11 +2,7 @@ import pytest import time dockerfile = u''' -FROM ubuntu:latest -RUN apt-get update -RUN apt-get install -yy python3 python3-pip python3-dev git -RUN pip3 install -U setuptools -RUN ln -s /usr/bin/pip3 /usr/bin/pip +FROM python:3 RUN adduser --disabled-password --gecos '' test ENV SEED "{seed}" WORKDIR /src @@ -42,7 +38,7 @@ def plot(proc, TIMEOUT): @pytest.mark.functional @pytest.mark.benchmark(min_rounds=10) def test_performance(spawnu, TIMEOUT, benchmark): - proc = spawnu(u'thefuck/ubuntu-python3-bash-performance', + proc = spawnu(u'thefuck/python3-bash-performance', dockerfile, u'bash') proc.sendline(u'pip install /src') proc.sendline(u'su test') From 4b79e23ba71ee596967990b6a05d6552680708ed Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Thu, 6 Oct 2016 10:56:37 -0400 Subject: [PATCH 11/62] test_bash.py: use official python images, not ubuntu This should help reduce build times. --- tests/functional/test_bash.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/functional/test_bash.py b/tests/functional/test_bash.py index 45496d1d..6503475b 100644 --- a/tests/functional/test_bash.py +++ b/tests/functional/test_bash.py @@ -3,18 +3,11 @@ from tests.functional.plots import with_confirmation, without_confirmation, \ refuse_with_confirmation, history_changed, history_not_changed, \ select_command_with_arrows, how_to_configure -containers = ((u'thefuck/ubuntu-python3-bash', - u'''FROM ubuntu:latest - RUN apt-get update - RUN apt-get install -yy python3 python3-pip python3-dev git - RUN pip3 install -U setuptools - RUN ln -s /usr/bin/pip3 /usr/bin/pip''', +containers = ((u'thefuck/python3-bash', + u'FROM python:3', u'bash'), - (u'thefuck/ubuntu-python2-bash', - u'''FROM ubuntu:latest - RUN apt-get update - RUN apt-get install -yy python python-pip python-dev git - RUN pip2 install -U pip setuptools''', + (u'thefuck/python2-bash', + u'FROM python:2', u'bash')) From 91fceb401acd504bfb0f3c49f56f1f89e1a9f547 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Thu, 6 Oct 2016 10:59:33 -0400 Subject: [PATCH 12/62] test_fish.py: use official python images, not ubuntu This should help reduce build times. --- tests/functional/test_fish.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_fish.py b/tests/functional/test_fish.py index aec038a4..657ebf19 100644 --- a/tests/functional/test_fish.py +++ b/tests/functional/test_fish.py @@ -2,19 +2,20 @@ import pytest from tests.functional.plots import with_confirmation, without_confirmation, \ refuse_with_confirmation, select_command_with_arrows -containers = (('thefuck/ubuntu-python3-fish', - u'''FROM ubuntu:latest +containers = (('thefuck/python3-fish', + u'''FROM python:3 + # Use jessie-backports since it has the fish package. See here for details: + # https://github.com/tianon/docker-brew-debian/blob/88ae21052affd8a14553bb969f9d41c464032122/jessie/backports/Dockerfile + RUN awk '$1 ~ "^deb" { $3 = $3 "-backports"; print; exit }' /etc/apt/sources.list > /etc/apt/sources.list.d/backports.list RUN apt-get update - RUN apt-get install -yy python3 python3-pip python3-dev fish git - RUN pip3 install -U setuptools - RUN ln -s /usr/bin/pip3 /usr/bin/pip RUN apt-get install -yy fish''', u'fish'), - ('thefuck/ubuntu-python2-fish', - u'''FROM ubuntu:latest + ('thefuck/python2-fish', + u'''FROM python:2 + # Use jessie-backports since it has the fish package. See here for details: + # https://github.com/tianon/docker-brew-debian/blob/88ae21052affd8a14553bb969f9d41c464032122/jessie/backports/Dockerfile + RUN awk '$1 ~ "^deb" { $3 = $3 "-backports"; print; exit }' /etc/apt/sources.list > /etc/apt/sources.list.d/backports.list RUN apt-get update - RUN apt-get install -yy python python-pip python-dev git - RUN pip2 install -U pip setuptools RUN apt-get install -yy fish''', u'fish')) From 10b20574d1f462ed9487f429fcea43136ac77803 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Thu, 6 Oct 2016 11:12:36 -0400 Subject: [PATCH 13/62] test_tcsh.py: use official python images, not ubuntu This should help reduce build times. --- tests/functional/test_tcsh.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_tcsh.py b/tests/functional/test_tcsh.py index e7d621b5..12cabe30 100644 --- a/tests/functional/test_tcsh.py +++ b/tests/functional/test_tcsh.py @@ -2,19 +2,14 @@ import pytest from tests.functional.plots import with_confirmation, without_confirmation, \ refuse_with_confirmation, select_command_with_arrows -containers = (('thefuck/ubuntu-python3-tcsh', - u'''FROM ubuntu:latest +containers = (('thefuck/python3-tcsh', + u'''FROM python:3 RUN apt-get update - RUN apt-get install -yy python3 python3-pip python3-dev git - RUN pip3 install -U setuptools - RUN ln -s /usr/bin/pip3 /usr/bin/pip RUN apt-get install -yy tcsh''', u'tcsh'), - ('thefuck/ubuntu-python2-tcsh', - u'''FROM ubuntu:latest + ('thefuck/python2-tcsh', + u'''FROM python:2 RUN apt-get update - RUN apt-get install -yy python python-pip python-dev git - RUN pip2 install -U pip setuptools RUN apt-get install -yy tcsh''', u'tcsh')) From 16a440cb9d3476b71e9e7ebd975a16ee609159f5 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Thu, 6 Oct 2016 11:15:16 -0400 Subject: [PATCH 14/62] test_zsh.py: use official python images, not ubuntu This should help reduce build times. --- tests/functional/test_zsh.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_zsh.py b/tests/functional/test_zsh.py index 6c12a3d2..7e2d78d0 100644 --- a/tests/functional/test_zsh.py +++ b/tests/functional/test_zsh.py @@ -3,19 +3,14 @@ from tests.functional.plots import with_confirmation, without_confirmation, \ refuse_with_confirmation, history_changed, history_not_changed, \ select_command_with_arrows, how_to_configure -containers = (('thefuck/ubuntu-python3-zsh', - u'''FROM ubuntu:latest +containers = (('thefuck/python3-zsh', + u'''FROM python:3 RUN apt-get update - RUN apt-get install -yy python3 python3-pip python3-dev git - RUN pip3 install -U setuptools - RUN ln -s /usr/bin/pip3 /usr/bin/pip RUN apt-get install -yy zsh''', u'zsh'), - ('thefuck/ubuntu-python2-zsh', - u'''FROM ubuntu:latest + ('thefuck/python2-zsh', + u'''FROM python:2 RUN apt-get update - RUN apt-get install -yy python python-pip python-dev git - RUN pip2 install -U pip setuptools RUN apt-get install -yy zsh''', u'zsh')) From feb36ede5c518fdc3b6eddf945b2d8b1e2294d15 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Thu, 6 Oct 2016 13:03:34 -0400 Subject: [PATCH 15/62] Fix suggestion for `git push -u` This was broken by https://github.com/nvbn/thefuck/pull/559 --- tests/rules/test_git_push.py | 2 ++ thefuck/rules/git_push.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/rules/test_git_push.py b/tests/rules/test_git_push.py index d6654964..1eb61b23 100644 --- a/tests/rules/test_git_push.py +++ b/tests/rules/test_git_push.py @@ -23,6 +23,8 @@ def test_match(stderr): def test_get_new_command(stderr): assert get_new_command(Command('git push', stderr=stderr))\ == "git push --set-upstream origin master" + assert get_new_command(Command('git push -u', stderr=stderr))\ + == "git push --set-upstream origin master" assert get_new_command(Command('git push -u origin', stderr=stderr))\ == "git push --set-upstream origin master" assert get_new_command(Command('git push --set-upstream origin', stderr=stderr))\ diff --git a/thefuck/rules/git_push.py b/thefuck/rules/git_push.py index 0a624eb9..f64d2ce2 100644 --- a/thefuck/rules/git_push.py +++ b/thefuck/rules/git_push.py @@ -24,7 +24,11 @@ def get_new_command(command): pass if upstream_option_index is not -1: command.script_parts.pop(upstream_option_index) - command.script_parts.pop(upstream_option_index) + try: + command.script_parts.pop(upstream_option_index) + except IndexError: + # This happens for `git push -u` + pass push_upstream = command.stderr.split('\n')[-3].strip().partition('git ')[2] return replace_argument(" ".join(command.script_parts), 'push', push_upstream) From 5b535077bf1e5f5d592ff9968f1fd7fae26c75e9 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Sat, 8 Oct 2016 12:18:33 +0200 Subject: [PATCH 16/62] #N/A: Stop changing `Command` inside rules --- README.md | 4 +++- thefuck/rules/brew_link.py | 9 +++++---- thefuck/rules/brew_uninstall.py | 7 ++++--- thefuck/rules/git_push.py | 12 +++++++----- thefuck/rules/git_rm_recursive.py | 7 ++++--- thefuck/rules/python_command.py | 5 ++--- thefuck/rules/systemctl.py | 2 +- thefuck/types.py | 3 ++- thefuck/utils.py | 2 +- 9 files changed, 29 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2604b62b..b158c807 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,9 @@ side_effect(old_command: Command, fixed_command: str) -> None ``` and optional `enabled_by_default`, `requires_output` and `priority` variables. -`Command` has three attributes: `script`, `stdout` and `stderr`. +`Command` has three attributes: `script`, `stdout`, `stderr` and `script_parts`. +Rule shouldn't change `Command`. + *Rules api changed in 3.0:* For accessing settings in rule you need to import it with `from thefuck.conf import settings`. `settings` is a special object filled with `~/.config/thefuck/settings.py` and values from env ([see more below](#settings)). diff --git a/thefuck/rules/brew_link.py b/thefuck/rules/brew_link.py index 23919a47..1c4d251e 100644 --- a/thefuck/rules/brew_link.py +++ b/thefuck/rules/brew_link.py @@ -8,7 +8,8 @@ def match(command): def get_new_command(command): - command.script_parts[1] = 'link' - command.script_parts.insert(2, '--overwrite') - command.script_parts.insert(3, '--dry-run') - return ' '.join(command.script_parts) + command_parts = command.script_parts[:] + command_parts[1] = 'link' + command_parts.insert(2, '--overwrite') + command_parts.insert(3, '--dry-run') + return ' '.join(command_parts) diff --git a/thefuck/rules/brew_uninstall.py b/thefuck/rules/brew_uninstall.py index d2306abd..f72062f7 100644 --- a/thefuck/rules/brew_uninstall.py +++ b/thefuck/rules/brew_uninstall.py @@ -8,6 +8,7 @@ def match(command): def get_new_command(command): - command.script_parts[1] = 'uninstall' - command.script_parts.insert(2, '--force') - return ' '.join(command.script_parts) + command_parts = command.script_parts[:] + command_parts[1] = 'uninstall' + command_parts.insert(2, '--force') + return ' '.join(command_parts) diff --git a/thefuck/rules/git_push.py b/thefuck/rules/git_push.py index 0a624eb9..af5d45fa 100644 --- a/thefuck/rules/git_push.py +++ b/thefuck/rules/git_push.py @@ -14,17 +14,19 @@ def get_new_command(command): # because the remaining arguments are concatenated onto the command suggested # by git, which includes --set-upstream and its argument upstream_option_index = -1 + command_parts = command.script_parts[:] + try: - upstream_option_index = command.script_parts.index('--set-upstream') + upstream_option_index = command_parts.index('--set-upstream') except ValueError: pass try: - upstream_option_index = command.script_parts.index('-u') + upstream_option_index = command_parts.index('-u') except ValueError: pass if upstream_option_index is not -1: - command.script_parts.pop(upstream_option_index) - command.script_parts.pop(upstream_option_index) + command_parts.pop(upstream_option_index) + command_parts.pop(upstream_option_index) push_upstream = command.stderr.split('\n')[-3].strip().partition('git ')[2] - return replace_argument(" ".join(command.script_parts), 'push', push_upstream) + return replace_argument(" ".join(command_parts), 'push', push_upstream) diff --git a/thefuck/rules/git_rm_recursive.py b/thefuck/rules/git_rm_recursive.py index b9cad274..9053aa6c 100644 --- a/thefuck/rules/git_rm_recursive.py +++ b/thefuck/rules/git_rm_recursive.py @@ -10,6 +10,7 @@ def match(command): @git_support def get_new_command(command): - index = command.script_parts.index('rm') + 1 - command.script_parts.insert(index, '-r') - return u' '.join(command.script_parts) + command_parts = command.script_parts[:] + index = command_parts.index('rm') + 1 + command_parts.insert(index, '-r') + return u' '.join(command_parts) diff --git a/thefuck/rules/python_command.py b/thefuck/rules/python_command.py index b4c321b7..98c56f72 100644 --- a/thefuck/rules/python_command.py +++ b/thefuck/rules/python_command.py @@ -6,9 +6,8 @@ from thefuck.specific.sudo import sudo_support @sudo_support def match(command): - toks = command.script_parts - return (toks - and toks[0].endswith('.py') + return (command.script_parts + and command.script_parts[0].endswith('.py') and ('Permission denied' in command.stderr or 'command not found' in command.stderr)) diff --git a/thefuck/rules/systemctl.py b/thefuck/rules/systemctl.py index 334c783e..499235c5 100644 --- a/thefuck/rules/systemctl.py +++ b/thefuck/rules/systemctl.py @@ -17,6 +17,6 @@ def match(command): @sudo_support def get_new_command(command): - cmd = command.script_parts + cmd = command.script_parts[:] cmd[-1], cmd[-2] = cmd[-2], cmd[-1] return ' '.join(cmd) diff --git a/thefuck/types.py b/thefuck/types.py index 527cf651..c03cb932 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -34,7 +34,8 @@ class Command(object): except Exception: logs.debug(u"Can't split command script {} because:\n {}".format( self, sys.exc_info())) - self._script_parts = None + self._script_parts = [] + return self._script_parts def __eq__(self, other): diff --git a/thefuck/utils.py b/thefuck/utils.py index b5680351..f6c0bb71 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -159,7 +159,7 @@ def is_app(command, *app_names, **kwargs): if kwargs: raise TypeError("got an unexpected keyword argument '{}'".format(kwargs.keys())) - if command.script_parts is not None and len(command.script_parts) > at_least: + if len(command.script_parts) > at_least: return command.script_parts[0] in app_names return False From 5ee5439c1e2c4b2e82965e9369197af1f2240d23 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Sat, 8 Oct 2016 12:24:48 +0200 Subject: [PATCH 17/62] #565: Refine `git_push` rule --- thefuck/rules/git_push.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/thefuck/rules/git_push.py b/thefuck/rules/git_push.py index ba4a0875..85f59fe1 100644 --- a/thefuck/rules/git_push.py +++ b/thefuck/rules/git_push.py @@ -8,29 +8,29 @@ def match(command): and 'set-upstream' in command.stderr) +def _get_upstream_option_index(command_parts): + if '--set-upstream' in command_parts: + return command_parts.index('--set-upstream') + elif '-u' in command_parts: + return command_parts.index('-u') + else: + return None + + @git_support def get_new_command(command): # If --set-upstream or -u are passed, remove it and its argument. This is # because the remaining arguments are concatenated onto the command suggested # by git, which includes --set-upstream and its argument - upstream_option_index = -1 command_parts = command.script_parts[:] + upstream_option_index = _get_upstream_option_index(command_parts) - try: - upstream_option_index = command_parts.index('--set-upstream') - except ValueError: - pass - try: - upstream_option_index = command_parts.index('-u') - except ValueError: - pass - if upstream_option_index is not -1: + if upstream_option_index is not None: command_parts.pop(upstream_option_index) - try: + + # In case of `git push -u` we don't have next argument: + if len(command_parts) > upstream_option_index: command_parts.pop(upstream_option_index) - except IndexError: - # This happens for `git push -u` - pass push_upstream = command.stderr.split('\n')[-3].strip().partition('git ')[2] return replace_argument(" ".join(command_parts), 'push', push_upstream) From af28f0334a62bd682f820ab1b505c1f396ed8ec1 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Sat, 29 Oct 2016 17:41:36 -0200 Subject: [PATCH 18/62] #N/A: Add `git_rm_local_modifications` rule --- README.md | 1 + .../rules/test_git_rm_local_modifications.py | 28 +++++++++++++++++++ thefuck/rules/git_rm_local_modifications.py | 19 +++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 tests/rules/test_git_rm_local_modifications.py create mode 100644 thefuck/rules/git_rm_local_modifications.py diff --git a/README.md b/README.md index b158c807..aedaf1b2 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `git_push_pull` – runs `git pull` when `push` was rejected; * `git_rebase_no_changes` – runs `git rebase --skip` instead of `git rebase --continue` when there are no changes; +* `git_rm_local_modifications` – adds `-f` or `--cached` when you try to `rm` a locally modified file; * `git_rm_recursive` – adds `-r` when you try to `rm` a directory; * `git_remote_seturl_add` – runs `git remote add` when `git remote set_url` on nonexistant remote; * `git_stash` – stashes you local modifications before rebasing or switching branch; diff --git a/tests/rules/test_git_rm_local_modifications.py b/tests/rules/test_git_rm_local_modifications.py new file mode 100644 index 00000000..1b0a4df6 --- /dev/null +++ b/tests/rules/test_git_rm_local_modifications.py @@ -0,0 +1,28 @@ +import pytest +from thefuck.rules.git_rm_local_modifications import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(target): + return ('error: the following file has local modifications:\n {}\n(use ' + '--cached to keep the file, or -f to force removal)').format(target) + + +@pytest.mark.parametrize('script, target', [ + ('git rm foo', 'foo'), + ('git rm foo bar', 'bar')]) +def test_match(stderr, script, target): + assert match(Command(script=script, stderr=stderr)) + + +@pytest.mark.parametrize('script', ['git rm foo', 'git rm foo bar', 'git rm']) +def test_not_match(script): + assert not match(Command(script=script, stderr='')) + + +@pytest.mark.parametrize('script, target, new_command', [ + ('git rm foo', 'foo', ['git rm --cached foo', 'git rm -f foo']), + ('git rm foo bar', 'bar', ['git rm --cached foo bar', 'git rm -f foo bar'])]) +def test_get_new_command(stderr, script, target, new_command): + assert get_new_command(Command(script=script, stderr=stderr)) == new_command diff --git a/thefuck/rules/git_rm_local_modifications.py b/thefuck/rules/git_rm_local_modifications.py new file mode 100644 index 00000000..01b8de42 --- /dev/null +++ b/thefuck/rules/git_rm_local_modifications.py @@ -0,0 +1,19 @@ +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return (' rm ' in command.script and + 'error: the following file has local modifications' in command.stderr and + 'use --cached to keep the file, or -f to force removal' in command.stderr) + + +@git_support +def get_new_command(command): + command_parts = command.script_parts[:] + index = command_parts.index('rm') + 1 + command_parts.insert(index, '--cached') + command_list = [u' '.join(command_parts)] + command_parts[index] = '-f' + command_list.append(u' '.join(command_parts)) + return command_list From 30b1c44f913d5d909823d0abd42e705d53e98755 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Sat, 29 Oct 2016 18:07:03 -0200 Subject: [PATCH 19/62] #N/A: Do not fail if formula is already installed --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d4e591b9..02d79886 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ addons: - python3-commandnotfound before_install: - if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew update ; fi - - if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew install $FORMULA; fi + - if [[ $TRAVIS_OS_NAME == "osx" ]]; then if brew ls --versions $FORMULA; then brew upgrade $FORMULA || echo Python is up to date; else brew install $FORMULA; fi; fi - if [[ $TRAVIS_OS_NAME == "osx" ]]; then virtualenv venv -p $FORMULA; fi - if [[ $TRAVIS_OS_NAME == "osx" ]]; then source venv/bin/activate; fi - pip install -U pip From 07005b591a990dceb026136f7742f9f4e5a123a3 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Sun, 30 Oct 2016 20:07:14 -0200 Subject: [PATCH 20/62] #N/A: Add `git_rebase_merge_dir` rule --- README.md | 1 + tests/rules/test_git_rebase_merge_dir.py | 40 ++++++++++++++++++++++++ thefuck/rules/git_rebase_merge_dir.py | 17 ++++++++++ 3 files changed, 58 insertions(+) create mode 100644 tests/rules/test_git_rebase_merge_dir.py create mode 100644 thefuck/rules/git_rebase_merge_dir.py diff --git a/README.md b/README.md index aedaf1b2..f2676077 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_rebase_no_changes` – runs `git rebase --skip` instead of `git rebase --continue` when there are no changes; * `git_rm_local_modifications` – adds `-f` or `--cached` when you try to `rm` a locally modified file; * `git_rm_recursive` – adds `-r` when you try to `rm` a directory; +* `git_rebase_merge_dir` – offers `git rebase (--continue | --abort | --skip)` or removing the `.git/rebase-merge` dir when a rebase is in progress; * `git_remote_seturl_add` – runs `git remote add` when `git remote set_url` on nonexistant remote; * `git_stash` – stashes you local modifications before rebasing or switching branch; * `git_two_dashes` – adds a missing dash to commands like `git commit -amend` or `git rebase -continue`; diff --git a/tests/rules/test_git_rebase_merge_dir.py b/tests/rules/test_git_rebase_merge_dir.py new file mode 100644 index 00000000..3e43ea70 --- /dev/null +++ b/tests/rules/test_git_rebase_merge_dir.py @@ -0,0 +1,40 @@ +import pytest +from thefuck.rules.git_rebase_merge_dir import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(): + return ('\n\nIt seems that there is already a rebase-merge directory, and\n' + 'I wonder if you are in the middle of another rebase. If that is the\n' + 'case, please try\n' + '\tgit rebase (--continue | --abort | --skip)\n' + 'If that is not the case, please\n' + '\trm -fr "/foo/bar/baz/egg/.git/rebase-merge"\n' + 'and run me again. I am stopping in case you still have something\n' + 'valuable there.\n') + + +@pytest.mark.parametrize('script', [ + ('git rebase master'), ('git rebase -skip'), ('git rebase')]) +def test_match(stderr, script): + assert match(Command(script=script, stderr=stderr)) + + +@pytest.mark.parametrize('script', ['git rebase master', 'git rebase -abort']) +def test_not_match(script): + assert not match(Command(script=script)) + + +@pytest.mark.parametrize('script, result', [ + ('git rebase master', [ + 'git rebase --abort', 'git rebase --skip', 'git rebase --continue', + 'rm -fr "/foo/bar/baz/egg/.git/rebase-merge"']), + ('git rebase -skip', [ + 'git rebase --skip', 'git rebase --abort', 'git rebase --continue', + 'rm -fr "/foo/bar/baz/egg/.git/rebase-merge"']), + ('git rebase', [ + 'git rebase --skip', 'git rebase --abort', 'git rebase --continue', + 'rm -fr "/foo/bar/baz/egg/.git/rebase-merge"'])]) +def test_get_new_command(stderr, script, result): + assert get_new_command(Command(script=script, stderr=stderr)) == result diff --git a/thefuck/rules/git_rebase_merge_dir.py b/thefuck/rules/git_rebase_merge_dir.py new file mode 100644 index 00000000..910e3121 --- /dev/null +++ b/thefuck/rules/git_rebase_merge_dir.py @@ -0,0 +1,17 @@ +from difflib import get_close_matches +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return (' rebase' in command.script and + 'It seems that there is already a rebase-merge directory' in command.stderr and + 'I wonder if you are in the middle of another rebase' in command.stderr) + + +@git_support +def get_new_command(command): + command_list = ['git rebase --continue', 'git rebase --abort', 'git rebase --skip'] + rm_cmd = command.stderr.split('\n')[-4] + command_list.append(rm_cmd.strip()) + return get_close_matches(command.script, command_list, 4, 0) From 5b420204c9efa1939925a8ff62e3f5bcb971b516 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sun, 30 Oct 2016 21:15:47 -0400 Subject: [PATCH 21/62] git: fix `fatal: bad flag '...' after filename` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For example: $ git log README.md -p fatal: bad flag '-p' used after filename $ fuck git log -p README.md [enter/↑/↓/ctrl+c] Aborted $ git log -p README.md --name-only fatal: bad flag '--name-only' used after filename $ fuck git log -p --name-only README.md [enter/↑/↓/ctrl+c] Aborted $ git log README.md -p CONTRIBUTING.md fatal: bad flag '-p' used after filename $ fuck git log -p README.md CONTRIBUTING.md [enter/↑/↓/ctrl+c] --- README.md | 1 + tests/rules/test_git_flag_after_filename.py | 20 ++++++++++++++ thefuck/rules/git_flag_after_filename.py | 29 +++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 tests/rules/test_git_flag_after_filename.py create mode 100644 thefuck/rules/git_flag_after_filename.py diff --git a/README.md b/README.md index aedaf1b2..0cf68448 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_diff_no_index` – adds `--no-index` to previous `git diff` on untracked files; * `git_diff_staged` – adds `--staged` to previous `git diff` with unexpected output; * `git_fix_stash` – fixes `git stash` commands (misspelled subcommand and missing `save`); +* `git_flag_after_filename` – fixes `fatal: bad flag '...' after filename` * `git_help_aliased` – fixes `git help ` commands replacing with the aliased command; * `git_not_command` – fixes wrong git commands like `git brnch`; * `git_pull` – sets upstream before executing previous `git pull`; diff --git a/tests/rules/test_git_flag_after_filename.py b/tests/rules/test_git_flag_after_filename.py new file mode 100644 index 00000000..ffb6e43c --- /dev/null +++ b/tests/rules/test_git_flag_after_filename.py @@ -0,0 +1,20 @@ +import pytest +from thefuck.rules.git_flag_after_filename import match, get_new_command +from tests.utils import Command + + +def test_match(): + assert match(Command('git log README.md -p', stderr="fatal: bad flag '-p' used after filename")) + assert match(Command('git log README.md -p CONTRIBUTING.md', stderr="fatal: bad flag '-p' used after filename")) + assert match(Command('git log -p README.md --name-only', stderr="fatal: bad flag '--name-only' used after filename")) + assert not match(Command('git log README.md')) + assert not match(Command('git log -p README.md')) + + +def test_get_new_command(): + assert get_new_command(Command('git log README.md -p', stderr="fatal: bad flag '-p' used after filename"))\ + == "git log -p README.md" + assert get_new_command(Command('git log README.md -p CONTRIBUTING.md', stderr="fatal: bad flag '-p' used after filename"))\ + == "git log -p README.md CONTRIBUTING.md" + assert get_new_command(Command('git log -p README.md --name-only', stderr="fatal: bad flag '--name-only' used after filename"))\ + == "git log -p --name-only README.md" diff --git a/thefuck/rules/git_flag_after_filename.py b/thefuck/rules/git_flag_after_filename.py new file mode 100644 index 00000000..e2a94d8e --- /dev/null +++ b/thefuck/rules/git_flag_after_filename.py @@ -0,0 +1,29 @@ +import re +from thefuck.specific.git import git_support + +error_pattern = "fatal: bad flag '(.*?)' used after filename" + +@git_support +def match(command): + return re.search(error_pattern, command.stderr) + + +@git_support +def get_new_command(command): + command_parts = command.script_parts[:] + + # find the bad flag + bad_flag = re.search(error_pattern, command.stderr).group(1) + bad_flag_index = command_parts.index(bad_flag) + + # find the filename + for index in reversed(range(bad_flag_index)): + if command_parts[index][0] != '-': + filename_index = index + break + + # swap them + command_parts[bad_flag_index], command_parts[filename_index] = \ + command_parts[filename_index], command_parts[bad_flag_index] + + return u' '.join(command_parts) From b519d317f74374eb9b197a56277e44697ac1764d Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sun, 30 Oct 2016 22:56:15 -0400 Subject: [PATCH 22/62] bash: always honor alter_history setting This ensures that even if the command suggested and run by `thefuck` fails, it will still be added to the history, allowing the user to tweak it further (or run `fuck` again) if desired. Note that the fish shell appears to already behave this way. --- thefuck/shells/bash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index 8f4e0e1c..d6f4cdff 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -14,7 +14,7 @@ class Bash(Generic): " eval $TF_CMD".format(fuck) if settings.alter_history: - return alias + " && history -s $TF_CMD'" + return alias + "; history -s $TF_CMD'" else: return alias + "'" From 9cae0bffffc82fa30475de93fa9d6fa5f6fd076e Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 31 Oct 2016 00:08:30 -0400 Subject: [PATCH 23/62] git_flag_after_filename: fix flake8 errors These were found by creating a `.flake8` file containing: [flake8] ignore = E501,W503 exclude = venv then running: flake8 $(git diff master... --name-only) See https://github.com/nvbn/thefuck/pull/563 for running `flake8` in CI --- tests/rules/test_git_flag_after_filename.py | 1 - thefuck/rules/git_flag_after_filename.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rules/test_git_flag_after_filename.py b/tests/rules/test_git_flag_after_filename.py index ffb6e43c..414c0d95 100644 --- a/tests/rules/test_git_flag_after_filename.py +++ b/tests/rules/test_git_flag_after_filename.py @@ -1,4 +1,3 @@ -import pytest from thefuck.rules.git_flag_after_filename import match, get_new_command from tests.utils import Command diff --git a/thefuck/rules/git_flag_after_filename.py b/thefuck/rules/git_flag_after_filename.py index e2a94d8e..bec0591e 100644 --- a/thefuck/rules/git_flag_after_filename.py +++ b/thefuck/rules/git_flag_after_filename.py @@ -3,6 +3,7 @@ from thefuck.specific.git import git_support error_pattern = "fatal: bad flag '(.*?)' used after filename" + @git_support def match(command): return re.search(error_pattern, command.stderr) @@ -24,6 +25,6 @@ def get_new_command(command): # swap them command_parts[bad_flag_index], command_parts[filename_index] = \ - command_parts[filename_index], command_parts[bad_flag_index] + command_parts[filename_index], command_parts[bad_flag_index] # noqa: E122 return u' '.join(command_parts) From fa169c686c39552bb62d0f55e7ccd1c513db0135 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 31 Oct 2016 00:15:21 -0400 Subject: [PATCH 24/62] test_git_flag_after_filename.py: dedupe test commands --- tests/rules/test_git_flag_after_filename.py | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/rules/test_git_flag_after_filename.py b/tests/rules/test_git_flag_after_filename.py index 414c0d95..b81668b0 100644 --- a/tests/rules/test_git_flag_after_filename.py +++ b/tests/rules/test_git_flag_after_filename.py @@ -1,19 +1,23 @@ from thefuck.rules.git_flag_after_filename import match, get_new_command from tests.utils import Command +command1 = Command('git log README.md -p', + stderr="fatal: bad flag '-p' used after filename") +command2 = Command('git log README.md -p CONTRIBUTING.md', + stderr="fatal: bad flag '-p' used after filename") +command3 = Command('git log -p README.md --name-only', + stderr="fatal: bad flag '--name-only' used after filename") + def test_match(): - assert match(Command('git log README.md -p', stderr="fatal: bad flag '-p' used after filename")) - assert match(Command('git log README.md -p CONTRIBUTING.md', stderr="fatal: bad flag '-p' used after filename")) - assert match(Command('git log -p README.md --name-only', stderr="fatal: bad flag '--name-only' used after filename")) + assert match(command1) + assert match(command2) + assert match(command3) assert not match(Command('git log README.md')) assert not match(Command('git log -p README.md')) def test_get_new_command(): - assert get_new_command(Command('git log README.md -p', stderr="fatal: bad flag '-p' used after filename"))\ - == "git log -p README.md" - assert get_new_command(Command('git log README.md -p CONTRIBUTING.md', stderr="fatal: bad flag '-p' used after filename"))\ - == "git log -p README.md CONTRIBUTING.md" - assert get_new_command(Command('git log -p README.md --name-only', stderr="fatal: bad flag '--name-only' used after filename"))\ - == "git log -p --name-only README.md" + assert get_new_command(command1) == "git log -p README.md" + assert get_new_command(command2) == "git log -p README.md CONTRIBUTING.md" + assert get_new_command(command3) == "git log -p --name-only README.md" From ddd8788353eb308cdf0d7b70e568decb95330615 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 31 Oct 2016 12:57:31 +0100 Subject: [PATCH 25/62] #571: always honor alter_history setting in zsh --- thefuck/shells/zsh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py index e522d6a3..79ff89ed 100644 --- a/thefuck/shells/zsh.py +++ b/thefuck/shells/zsh.py @@ -15,7 +15,7 @@ class Zsh(Generic): " eval $TF_CMD".format(alias_name) if settings.alter_history: - return alias + " && print -s $TF_CMD'" + return alias + " ; print -s $TF_CMD'" else: return alias + "'" From 756044e08716f371710129f852d02eeb02fa302a Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Mon, 10 Oct 2016 14:55:45 -0400 Subject: [PATCH 26/62] Suggest `ls -A` when `ls` has no output --- README.md | 1 + tests/rules/test_ls_all.py | 12 ++++++++++++ thefuck/rules/ls_all.py | 10 ++++++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/rules/test_ls_all.py create mode 100644 thefuck/rules/ls_all.py diff --git a/README.md b/README.md index f2676077..89dd7307 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `ln_no_hard_link` – catches hard link creation on directories, suggest symbolic link; * `ln_s_order` – fixes `ln -s` arguments order; +* `ls_all` – adds `-A` to `ls` when output is empty; * `ls_lah` – adds `-lah` to `ls`; * `man` – changes manual section; * `man_no_space` – fixes man commands without spaces, for example `mandiff`; diff --git a/tests/rules/test_ls_all.py b/tests/rules/test_ls_all.py new file mode 100644 index 00000000..475f4e9c --- /dev/null +++ b/tests/rules/test_ls_all.py @@ -0,0 +1,12 @@ +from thefuck.rules.ls_all import match, get_new_command +from tests.utils import Command + + +def test_match(): + assert match(Command(script='ls')) + assert not match(Command(script='ls', stdout='file.py\n')) + + +def test_get_new_command(): + assert get_new_command(Command(script='ls empty_dir')) == 'ls -A empty_dir' + assert get_new_command(Command(script='ls')) == 'ls -A' diff --git a/thefuck/rules/ls_all.py b/thefuck/rules/ls_all.py new file mode 100644 index 00000000..e898d17d --- /dev/null +++ b/thefuck/rules/ls_all.py @@ -0,0 +1,10 @@ +from thefuck.utils import for_app + + +@for_app('ls') +def match(command): + return command.stdout.strip() == '' + + +def get_new_command(command): + return ' '.join(['ls', '-A'] + command.script_parts[1:]) From 6f0d1e287d53e489c576a2629b53456c75d1d38c Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 31 Oct 2016 18:52:48 +0100 Subject: [PATCH 27/62] #571: Don't put empty string in history in zsh --- thefuck/shells/zsh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py index 79ff89ed..3f8bd80c 100644 --- a/thefuck/shells/zsh.py +++ b/thefuck/shells/zsh.py @@ -15,7 +15,7 @@ class Zsh(Generic): " eval $TF_CMD".format(alias_name) if settings.alter_history: - return alias + " ; print -s $TF_CMD'" + return alias + " ; test -n \"$TF_CMD\" && print -s $TF_CMD'" else: return alias + "'" From aec8fe32337e2721d198b78f0a8a37d8161d5da8 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Tue, 8 Nov 2016 23:53:40 +0100 Subject: [PATCH 28/62] #570: Refine tests --- tests/rules/test_git_flag_after_filename.py | 28 +++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/rules/test_git_flag_after_filename.py b/tests/rules/test_git_flag_after_filename.py index b81668b0..4cf49957 100644 --- a/tests/rules/test_git_flag_after_filename.py +++ b/tests/rules/test_git_flag_after_filename.py @@ -1,3 +1,4 @@ +import pytest from thefuck.rules.git_flag_after_filename import match, get_new_command from tests.utils import Command @@ -9,15 +10,22 @@ command3 = Command('git log -p README.md --name-only', stderr="fatal: bad flag '--name-only' used after filename") -def test_match(): - assert match(command1) - assert match(command2) - assert match(command3) - assert not match(Command('git log README.md')) - assert not match(Command('git log -p README.md')) +@pytest.mark.parametrize('command', [ + command1, command2, command3]) +def test_match(command): + assert match(command) -def test_get_new_command(): - assert get_new_command(command1) == "git log -p README.md" - assert get_new_command(command2) == "git log -p README.md CONTRIBUTING.md" - assert get_new_command(command3) == "git log -p --name-only README.md" +@pytest.mark.parametrize('command', [ + Command('git log README.md'), + Command('git log -p README.md')]) +def test_not_match(command): + assert not match(command) + + +@pytest.mark.parametrize('command, result', [ + (command1, "git log -p README.md"), + (command2, "git log -p README.md CONTRIBUTING.md"), + (command3, "git log -p --name-only README.md")]) +def test_get_new_command(command, result): + assert get_new_command(command) == result From a947259eefad7a2332b03afcb1545cb093002d11 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Thu, 17 Nov 2016 22:57:11 -0200 Subject: [PATCH 29/62] #577: Use builtin `history` in Fish function Fix #577 --- tests/shells/test_fish.py | 8 ++++---- thefuck/shells/fish.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/shells/test_fish.py b/tests/shells/test_fish.py index ba71edf5..24780c7b 100644 --- a/tests/shells/test_fish.py +++ b/tests/shells/test_fish.py @@ -76,11 +76,11 @@ class TestFish(object): def test_app_alias_alter_history(self, settings, shell): settings.alter_history = True - assert 'history --delete' in shell.app_alias('FUCK') - assert 'history --merge' in shell.app_alias('FUCK') + assert 'builtin history delete' in shell.app_alias('FUCK') + assert 'builtin history merge' in shell.app_alias('FUCK') settings.alter_history = False - assert 'history --delete' not in shell.app_alias('FUCK') - assert 'history --merge' not in shell.app_alias('FUCK') + assert 'builtin history delete' not in shell.app_alias('FUCK') + assert 'builtin history merge' not in shell.app_alias('FUCK') def test_get_history(self, history_lines, shell): history_lines(['- cmd: ls', ' when: 1432613911', diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 34fdf7b2..0703afc0 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -20,8 +20,9 @@ class Fish(Generic): def app_alias(self, fuck): if settings.alter_history: - alter_history = (' history --delete $fucked_up_command\n' - ' history --merge ^ /dev/null\n') + alter_history = (' builtin history delete --exact' + ' --case-sensitive -- $fucked_up_command\n' + ' builtin history merge ^ /dev/null\n') else: alter_history = '' # It is VERY important to have the variables declared WITHIN the alias From 892e8a8e65a0352cc0ad0a352d5d0c431f77cf0d Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Thu, 17 Nov 2016 14:34:55 -0500 Subject: [PATCH 30/62] Test parsing bash command substitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is to help address bad corrections like the following (note the position of the -p flag): thefuck 'git log $(git ls-files thefuck | grep python_command) -p' git log $(git ls-files thefuck | grep -p python_command) [enter/↑/↓/ctrl+c] --- tests/shells/test_bash.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/shells/test_bash.py b/tests/shells/test_bash.py index c3738f44..a38aff77 100644 --- a/tests/shells/test_bash.py +++ b/tests/shells/test_bash.py @@ -56,3 +56,8 @@ class TestBash(object): def test_get_history(self, history_lines, shell): history_lines(['ls', 'rm']) assert list(shell.get_history()) == ['ls', 'rm'] + + def test_split_command(self, shell): + command = 'git log $(git ls-files thefuck | grep python_command) -p' + command_parts = ['git', 'log', '$(git ls-files thefuck | grep python_command)', '-p'] + assert shell.split_command(command) == command_parts From ca44ee064056642fa62ec15753c825b856c8e932 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Fri, 18 Nov 2016 14:03:28 -0500 Subject: [PATCH 31/62] bash: use bashlex for split_command, not shlex --- setup.py | 2 +- thefuck/shells/bash.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cab15aa8..52308c8b 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ elif (3, 0) < version < (3, 3): VERSION = '3.11' -install_requires = ['psutil', 'colorama', 'six', 'decorator'] +install_requires = ['psutil', 'colorama', 'six', 'decorator', 'bashlex'] extras_require = {':python_version<"3.4"': ['pathlib2'], ":sys_platform=='win32'": ['win_unicode_console']} diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index d6f4cdff..0ab32717 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -1,4 +1,5 @@ import os +import bashlex from ..conf import settings from ..utils import memoize from .generic import Generic @@ -45,3 +46,6 @@ class Bash(Generic): else: config = 'bash config' return 'eval $(thefuck --alias)', config + + def split_command(self, command): + return list(bashlex.split(command)) From dbedcc7aa6a6a1df1b6ea3775c1a7bac5f8fbdc8 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 23 Nov 2016 07:36:58 -0500 Subject: [PATCH 32/62] Test parsing bash arithmetic expressions --- tests/shells/test_bash.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/shells/test_bash.py b/tests/shells/test_bash.py index a38aff77..c4268c04 100644 --- a/tests/shells/test_bash.py +++ b/tests/shells/test_bash.py @@ -61,3 +61,8 @@ class TestBash(object): command = 'git log $(git ls-files thefuck | grep python_command) -p' command_parts = ['git', 'log', '$(git ls-files thefuck | grep python_command)', '-p'] assert shell.split_command(command) == command_parts + + # bashlex doesn't support parsing arithmetic expressions, so make sure + # shlex is used a fallback + # See https://github.com/idank/bashlex#limitations + assert shell.split_command('$((1 + 2))') == ['$((1', '+', '2))'] From 4f87141f0c1b3c7848460fafa34b721dfe999d01 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 23 Nov 2016 07:43:25 -0500 Subject: [PATCH 33/62] bash: fallback to generic parser if bashlex fails --- thefuck/shells/bash.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index 0ab32717..1d8ab7db 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -1,5 +1,6 @@ import os import bashlex +import six from ..conf import settings from ..utils import memoize from .generic import Generic @@ -48,4 +49,17 @@ class Bash(Generic): return 'eval $(thefuck --alias)', config def split_command(self, command): - return list(bashlex.split(command)) + if six.PY2: + command = command.encode('utf8') + + # If bashlex fails for some reason, fallback to shlex + # See https://github.com/idank/bashlex#limitations + try: + command_parts = list(bashlex.split(command)) + except: + return Generic().split_command(command) + + if six.PY2: + return [s.decode('utf8') for s in command_parts] + else: + return command_parts From 385746850ea9ebd12eb98973cc959552a3c21d31 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 23 Nov 2016 07:53:22 -0500 Subject: [PATCH 34/62] generic shell: extract UTF-8 encoding/decoding into methods --- thefuck/shells/generic.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index e20d7ec3..ec38df1c 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -65,9 +65,17 @@ class Generic(object): def split_command(self, command): """Split the command using shell-like syntax.""" + return self.decode_utf8(shlex.split(self.encode_utf8(command))) + + def encode_utf8(self, command): if six.PY2: - return [s.decode('utf8') for s in shlex.split(command.encode('utf8'))] - return shlex.split(command) + return command.encode('utf8') + return command + + def decode_utf8(self, command_parts): + if six.PY2: + return [s.decode('utf8') for s in command_parts] + return command_parts def quote(self, s): """Return a shell-escaped version of the string s.""" From 4ae32cf4ee8cc69d73930f4565c55097cb1f5ce2 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Wed, 23 Nov 2016 08:12:01 -0500 Subject: [PATCH 35/62] bash: use generic shell's UTF-8 methods --- thefuck/shells/bash.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index 1d8ab7db..f914163f 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -1,6 +1,5 @@ import os import bashlex -import six from ..conf import settings from ..utils import memoize from .generic import Generic @@ -49,17 +48,11 @@ class Bash(Generic): return 'eval $(thefuck --alias)', config def split_command(self, command): - if six.PY2: - command = command.encode('utf8') + generic = Generic() # If bashlex fails for some reason, fallback to shlex # See https://github.com/idank/bashlex#limitations try: - command_parts = list(bashlex.split(command)) + return generic.decode_utf8(list(bashlex.split(generic.encode_utf8(command)))) except: - return Generic().split_command(command) - - if six.PY2: - return [s.decode('utf8') for s in command_parts] - else: - return command_parts + return generic.split_command(command) From 8c62706db40008e63b85e43dd1b41c3c1798acae Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sun, 11 Dec 2016 12:37:09 -0500 Subject: [PATCH 36/62] Fix `git stash pop` with local changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When there are local changes to a file, and a git stash is popped that contains other changes to that same file, git fails as follows: $ git stash pop error: Your local changes to the following files would be overwritten by merge: src/index.js Please commit your changes or stash them before you merge. Aborting $ This change adds a rule that corrects this problem as suggested [here]: $ git stash pop error: Your local changes to the following files would be overwritten by merge: src/index.js Please commit your changes or stash them before you merge. Aborting $ fuck git add . && git stash pop && git reset . [enter/↑/↓/ctrl+c] Auto-merging src/index.js On branch flow Changes to be committed: (use "git reset HEAD ..." to unstage) modified: src/index.js Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git checkout -- ..." to discard changes in working directory) modified: src/index.js Dropped refs/stash@{0} (f94776d484c4278997ac6837a7b138b9b9cdead1) Unstaged changes after reset: M src/index.js $ [here]: https://stackoverflow.com/questions/15126463/how-do-i-merge-local-modifications-with-a-git-stash-without-an-extra-commit/15126489#15126489 --- README.md | 1 + tests/rules/test_git_stash_pop.py | 18 ++++++++++++++++++ thefuck/rules/git_stash_pop.py | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 tests/rules/test_git_stash_pop.py create mode 100644 thefuck/rules/git_stash_pop.py diff --git a/README.md b/README.md index 9c149bc1..e0c42e94 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_rebase_merge_dir` – offers `git rebase (--continue | --abort | --skip)` or removing the `.git/rebase-merge` dir when a rebase is in progress; * `git_remote_seturl_add` – runs `git remote add` when `git remote set_url` on nonexistant remote; * `git_stash` – stashes you local modifications before rebasing or switching branch; +* `git_stash_pop` – adds your local modifications before popping stash, then resets; * `git_two_dashes` – adds a missing dash to commands like `git commit -amend` or `git rebase -continue`; * `go_run` – appends `.go` extension when compiling/running Go programs; * `gradle_no_task` – fixes not found or ambiguous `gradle` task; diff --git a/tests/rules/test_git_stash_pop.py b/tests/rules/test_git_stash_pop.py new file mode 100644 index 00000000..1ff34686 --- /dev/null +++ b/tests/rules/test_git_stash_pop.py @@ -0,0 +1,18 @@ +import pytest +from thefuck.rules.git_stash_pop import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(): + return '''error: Your local changes to the following files would be overwritten by merge:''' + + +def test_match(stderr): + assert match(Command('git stash pop', stderr=stderr)) + assert not match(Command('git stash')) + + +def test_get_new_command(stderr): + assert get_new_command(Command('git stash pop', stderr=stderr)) \ + == "git add . && git stash pop && git reset ." diff --git a/thefuck/rules/git_stash_pop.py b/thefuck/rules/git_stash_pop.py new file mode 100644 index 00000000..2073c234 --- /dev/null +++ b/thefuck/rules/git_stash_pop.py @@ -0,0 +1,18 @@ +from thefuck.shells import shell +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return ('stash' in command.script + and 'pop' in command.script + and 'Your local changes to the following files would be overwritten by merge' in command.stderr) + + +@git_support +def get_new_command(command): + return shell.and_('git add .', 'git stash pop', 'git reset .') + + +# make it come before the other applicable rules +priority = 900 From 7db140c45696345c49e0c11b11210685383f628a Mon Sep 17 00:00:00 2001 From: Eugene Duboviy Date: Sun, 8 Jan 2017 17:06:45 +0200 Subject: [PATCH 37/62] Update tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1d98a69a..2b0b03e4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,py35 +envlist = py27,py33,py34,py35,py36 [testenv] deps = -rrequirements.txt From bc9121cb13d0a0a8c4eb44cc5cba9b5d3003f231 Mon Sep 17 00:00:00 2001 From: Eugene Duboviy Date: Sun, 8 Jan 2017 17:08:38 +0200 Subject: [PATCH 38/62] Update appveyor.yml --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index d7072f7f..005e0d1a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,7 @@ environment: - PYTHON: "C:/Python33" - PYTHON: "C:/Python34" - PYTHON: "C:/Python35" + - PYTHON: "C:/Python36" init: - "ECHO %PYTHON%" From 993a661c6048063e84645015cc832602b6ec32df Mon Sep 17 00:00:00 2001 From: Eugene Duboviy Date: Sun, 8 Jan 2017 17:13:22 +0200 Subject: [PATCH 39/62] Update .travis.yml --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 02d79886..a1018d8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: python sudo: false matrix: include: + - os: linux + dist: trusty + python: "3.6" - os: linux dist: trusty python: "3.5" @@ -41,7 +44,7 @@ install: script: - export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1} - export RUN_TESTS="coverage run --source=thefuck,tests -m py.test -v --capture=sys tests" - - if [[ $TRAVIS_PYTHON_VERSION == 3.5 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi - - if [[ $TRAVIS_PYTHON_VERSION != 3.5 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi + - if [[ $TRAVIS_PYTHON_VERSION == 3.6 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi + - if [[ $TRAVIS_PYTHON_VERSION != 3.6 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi after_success: - coveralls From a6f63c05683b4bc3e64a483a9015af20d8b490eb Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 9 Jan 2017 17:50:23 +0100 Subject: [PATCH 40/62] #580: Use bashlex in generic shell --- thefuck/shells/bash.py | 11 ----------- thefuck/shells/generic.py | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index f914163f..d6f4cdff 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -1,5 +1,4 @@ import os -import bashlex from ..conf import settings from ..utils import memoize from .generic import Generic @@ -46,13 +45,3 @@ class Bash(Generic): else: config = 'bash config' return 'eval $(thefuck --alias)', config - - def split_command(self, command): - generic = Generic() - - # If bashlex fails for some reason, fallback to shlex - # See https://github.com/idank/bashlex#limitations - try: - return generic.decode_utf8(list(bashlex.split(generic.encode_utf8(command)))) - except: - return generic.split_command(command) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index ec38df1c..9aa65b3b 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -1,6 +1,7 @@ import io import os import shlex +import bashlex import six from ..utils import memoize from ..conf import settings @@ -64,8 +65,19 @@ class Generic(object): return def split_command(self, command): - """Split the command using shell-like syntax.""" - return self.decode_utf8(shlex.split(self.encode_utf8(command))) + """Split the command using shell-like syntax. + + If bashlex fails for some reason, fallback to shlex + See https://github.com/idank/bashlex#limitations + """ + encoded = self.encode_utf8(command) + + try: + splitted = list(bashlex.split(encoded)) + except Exception: + splitted = shlex.split(encoded) + + return self.decode_utf8(splitted) def encode_utf8(self, command): if six.PY2: From 4a0d71c1c4422419c9539a2ecd9ccbeb674c08ef Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 9 Jan 2017 18:13:37 +0100 Subject: [PATCH 41/62] #N/A: Add ifconfig_device_not_found rule --- README.md | 1 + tests/rules/test_ifconfig_device_not_found.py | 53 +++++++++++++++++++ thefuck/rules/ifconfig_device_not_found.py | 25 +++++++++ 3 files changed, 79 insertions(+) create mode 100644 tests/rules/test_ifconfig_device_not_found.py create mode 100644 thefuck/rules/ifconfig_device_not_found.py diff --git a/README.md b/README.md index e0c42e94..db5a6f88 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `has_exists_script` – prepends `./` when script/binary exists; * `heroku_not_command` – fixes wrong `heroku` commands like `heroku log`; * `history` – tries to replace command with most similar command from history; +* `ifconfig_device_not_found` – fixes wrong device names like `wlan0` to `wlp2s0`; * `java` – removes `.java` extension when running Java programs; * `javac` – appends missing `.java` when compiling Java files; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; diff --git a/tests/rules/test_ifconfig_device_not_found.py b/tests/rules/test_ifconfig_device_not_found.py new file mode 100644 index 00000000..e09daffc --- /dev/null +++ b/tests/rules/test_ifconfig_device_not_found.py @@ -0,0 +1,53 @@ +import pytest +from six import BytesIO +from thefuck.rules.ifconfig_device_not_found import match, get_new_command +from tests.utils import Command + + +stderr = '{}: error fetching interface information: Device not found' + +stdout = b''' +wlp2s0 Link encap:Ethernet HWaddr 5c:51:4f:7c:58:5d + inet addr:192.168.0.103 Bcast:192.168.0.255 Mask:255.255.255.0 + inet6 addr: fe80::be23:69b9:96d2:6d39/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + RX packets:23581604 errors:0 dropped:0 overruns:0 frame:0 + TX packets:17017655 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:1000 + RX bytes:16148429061 (16.1 GB) TX bytes:7067533695 (7.0 GB) +''' + + +@pytest.fixture(autouse=True) +def ifconfig(mocker): + mock = mocker.patch( + 'thefuck.rules.ifconfig_device_not_found.subprocess.Popen') + mock.return_value.stdout = BytesIO(stdout) + return mock + + +@pytest.mark.parametrize('script, stderr', [ + ('ifconfig wlan0', stderr.format('wlan0')), + ('ifconfig -s eth0', stderr.format('eth0')), +]) +def test_match(script, stderr): + assert match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, stderr', [ + ('config wlan0', + 'wlan0: error fetching interface information: Device not found'), + ('ifconfig eth0', ''), +]) +def test_not_match(script, stderr): + assert not match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, result', [ + ('ifconfig wlan0', ['ifconfig wlp2s0']), + ('ifconfig -s wlan0', ['ifconfig -s wlp2s0']), +]) +def test_get_new_comman(script, result): + new_command = get_new_command( + Command(script, stderr=stderr.format('wlan0'))) + assert new_command == result diff --git a/thefuck/rules/ifconfig_device_not_found.py b/thefuck/rules/ifconfig_device_not_found.py new file mode 100644 index 00000000..f8692236 --- /dev/null +++ b/thefuck/rules/ifconfig_device_not_found.py @@ -0,0 +1,25 @@ +import subprocess +from thefuck.utils import for_app, replace_command, eager +import sys + +@for_app('ifconfig') +def match(command): + return 'error fetching interface information: Device not found' \ + in command.stderr + + +@eager +def _get_possible_interfaces(): + proc = subprocess.Popen(['ifconfig', '-a'], stdout=subprocess.PIPE) + for line in proc.stdout.readlines(): + line = line.decode() + if line and line != '\n' and not line.startswith(' '): + yield line.split(' ')[0] + + +def get_new_command(command): + interface = command.stderr.split(' ')[0][:-1] + possible_interfaces = _get_possible_interfaces() + return replace_command(command, interface, possible_interfaces) + + From 03a828d5868402bcfd6cbaa9bad322421b3be6fd Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 9 Jan 2017 18:17:50 +0100 Subject: [PATCH 42/62] Bump to 3.12 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 52308c8b..025ae275 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ elif (3, 0) < version < (3, 3): ' ({}.{} detected).'.format(*version)) sys.exit(-1) -VERSION = '3.11' +VERSION = '3.12' install_requires = ['psutil', 'colorama', 'six', 'decorator', 'bashlex'] extras_require = {':python_version<"3.4"': ['pathlib2'], From a778ea62037e7710c4f7ef1923d3d1dd7b8c9b10 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Wed, 11 Jan 2017 14:58:50 +0100 Subject: [PATCH 43/62] #588: Stop using bashlex --- setup.py | 2 +- tests/shells/test_bash.py | 9 ++------- thefuck/shells/generic.py | 14 ++------------ 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/setup.py b/setup.py index 025ae275..f5862e13 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ elif (3, 0) < version < (3, 3): VERSION = '3.12' -install_requires = ['psutil', 'colorama', 'six', 'decorator', 'bashlex'] +install_requires = ['psutil', 'colorama', 'six', 'decorator'] extras_require = {':python_version<"3.4"': ['pathlib2'], ":sys_platform=='win32'": ['win_unicode_console']} diff --git a/tests/shells/test_bash.py b/tests/shells/test_bash.py index c4268c04..96661b53 100644 --- a/tests/shells/test_bash.py +++ b/tests/shells/test_bash.py @@ -58,11 +58,6 @@ class TestBash(object): assert list(shell.get_history()) == ['ls', 'rm'] def test_split_command(self, shell): - command = 'git log $(git ls-files thefuck | grep python_command) -p' - command_parts = ['git', 'log', '$(git ls-files thefuck | grep python_command)', '-p'] + command = 'git log -p' + command_parts = ['git', 'log', '-p'] assert shell.split_command(command) == command_parts - - # bashlex doesn't support parsing arithmetic expressions, so make sure - # shlex is used a fallback - # See https://github.com/idank/bashlex#limitations - assert shell.split_command('$((1 + 2))') == ['$((1', '+', '2))'] diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index 9aa65b3b..a4f0b1e4 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -1,7 +1,6 @@ import io import os import shlex -import bashlex import six from ..utils import memoize from ..conf import settings @@ -65,18 +64,9 @@ class Generic(object): return def split_command(self, command): - """Split the command using shell-like syntax. - - If bashlex fails for some reason, fallback to shlex - See https://github.com/idank/bashlex#limitations - """ + """Split the command using shell-like syntax.""" encoded = self.encode_utf8(command) - - try: - splitted = list(bashlex.split(encoded)) - except Exception: - splitted = shlex.split(encoded) - + splitted = shlex.split(encoded) return self.decode_utf8(splitted) def encode_utf8(self, command): From a65f90813b2a6597af46fdaec07a31db71f6038a Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Wed, 11 Jan 2017 14:59:18 +0100 Subject: [PATCH 44/62] Bump to 3.13 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f5862e13..d1613417 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ elif (3, 0) < version < (3, 3): ' ({}.{} detected).'.format(*version)) sys.exit(-1) -VERSION = '3.12' +VERSION = '3.13' install_requires = ['psutil', 'colorama', 'six', 'decorator'] extras_require = {':python_version<"3.4"': ['pathlib2'], From 3a9942061dcd7098f922304b3ef6524a8c5ac3a2 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Wed, 11 Jan 2017 15:05:29 +0100 Subject: [PATCH 45/62] Bump to 3.14 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d1613417..4149f663 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ elif (3, 0) < version < (3, 3): ' ({}.{} detected).'.format(*version)) sys.exit(-1) -VERSION = '3.13' +VERSION = '3.14' install_requires = ['psutil', 'colorama', 'six', 'decorator'] extras_require = {':python_version<"3.4"': ['pathlib2'], From 8447b5caa24ab04ca6f06c4baa9b9a0a9693ca22 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Sun, 15 Jan 2017 14:03:09 +0100 Subject: [PATCH 46/62] #585: Add note about reloading changes in how to configure message --- thefuck/logs.py | 9 +++++---- thefuck/shells/bash.py | 7 ++++++- thefuck/shells/fish.py | 7 +++++-- thefuck/shells/powershell.py | 6 +++++- thefuck/shells/tcsh.py | 6 +++++- thefuck/shells/zsh.py | 6 +++++- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/thefuck/logs.py b/thefuck/logs.py index 15f232a4..e783c5f8 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -83,13 +83,14 @@ def how_to_configure_alias(configuration_details): print("Seems like {bold}fuck{reset} alias isn't configured!".format( bold=color(colorama.Style.BRIGHT), reset=color(colorama.Style.RESET_ALL))) + if configuration_details: - content, path = configuration_details print( "Please put {bold}{content}{reset} in your " - "{bold}{path}{reset}.".format( + "{bold}{path}{reset} and apply " + "changes with {bold}{reload}{reset} or restart your shell.".format( bold=color(colorama.Style.BRIGHT), reset=color(colorama.Style.RESET_ALL), - path=path, - content=content)) + **configuration_details)) + print('More details - https://github.com/nvbn/thefuck#manual-installation') diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index d6f4cdff..c5ff5359 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -44,4 +44,9 @@ class Bash(Generic): config = '~/.bashrc' else: config = 'bash config' - return 'eval $(thefuck --alias)', config + + return { + 'content': 'eval $(thefuck --alias)', + 'path': config, + 'reload': u'source {}'.format(config), + } diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 0703afc0..e20ce29e 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -67,8 +67,11 @@ class Fish(Generic): return u'; and '.join(commands) def how_to_configure(self): - return (r"eval (thefuck --alias | tr '\n' ';')", - '~/.config/fish/config.fish') + return { + 'content': r"eval (thefuck --alias | tr '\n' ';')", + 'path': '~/.config/fish/config.fish', + 'reload': 'fish', + } def put_to_history(self, command): try: diff --git a/thefuck/shells/powershell.py b/thefuck/shells/powershell.py index 160a05c4..8e5d994d 100644 --- a/thefuck/shells/powershell.py +++ b/thefuck/shells/powershell.py @@ -15,4 +15,8 @@ class Powershell(Generic): return u' -and '.join('({0})'.format(c) for c in commands) def how_to_configure(self): - return 'iex "thefuck --alias"', '$profile' + return { + 'content': 'iex "thefuck --alias"', + 'path': '$profile', + 'reload': '& $profile', + } diff --git a/thefuck/shells/tcsh.py b/thefuck/shells/tcsh.py index 6c355caa..057dc560 100644 --- a/thefuck/shells/tcsh.py +++ b/thefuck/shells/tcsh.py @@ -31,4 +31,8 @@ class Tcsh(Generic): return u'#+{}\n{}\n'.format(int(time()), command_script) def how_to_configure(self): - return 'eval `thefuck --alias`', '~/.tcshrc' + return { + 'content': 'eval `thefuck --alias`', + 'path': '~/.tcshrc', + 'reload': 'tcsh', + } diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py index 3f8bd80c..24551462 100644 --- a/thefuck/shells/zsh.py +++ b/thefuck/shells/zsh.py @@ -45,4 +45,8 @@ class Zsh(Generic): return '' def how_to_configure(self): - return 'eval $(thefuck --alias)', '~/.zshrc' + return { + 'content': 'eval $(thefuck --alias)', + 'path': '~/.zshrc', + 'reload': 'source ~/.zshrc', + } From dbe324bcd8b8a39d9efa2cb2ee1c51f1634ae939 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Sun, 15 Jan 2017 14:40:50 +0100 Subject: [PATCH 47/62] #587: Add scm correction rule --- README.md | 1 + tests/rules/test_scm_correction.py | 46 ++++++++++++++++++++++++++++++ thefuck/rules/scm_correction.py | 32 +++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 tests/rules/test_scm_correction.py create mode 100644 thefuck/rules/scm_correction.py diff --git a/README.md b/README.md index db5a6f88..6e482727 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `react_native_command_unrecognized` – fixes unrecognized `react-native` commands; * `remove_trailing_cedilla` – remove trailling cedillas `ç`, a common typo for european keyboard layouts; * `rm_dir` – adds `-rf` when you trying to remove directory; +* `scm_correction` – corrects wrong scm like `hg log` to `git log`; * `sed_unterminated_s` – adds missing '/' to `sed`'s `s` commands; * `sl_ls` – changes `sl` to `ls`; * `ssh_known_hosts` – removes host from `known_hosts` on warning; diff --git a/tests/rules/test_scm_correction.py b/tests/rules/test_scm_correction.py new file mode 100644 index 00000000..7f02b905 --- /dev/null +++ b/tests/rules/test_scm_correction.py @@ -0,0 +1,46 @@ +import pytest +from thefuck.rules.scm_correction import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def get_actual_scm_mock(mocker): + return mocker.patch('thefuck.rules.scm_correction._get_actual_scm', + return_value=None) + + +@pytest.mark.parametrize('script, stderr, actual_scm', [ + ('git log', 'fatal: Not a git repository ' + '(or any of the parent directories): .git', + 'hg'), + ('hg log', "abort: no repository found in '/home/nvbn/exp/thefuck' " + "(.hg not found)!", + 'git')]) +def test_match(get_actual_scm_mock, script, stderr, actual_scm): + get_actual_scm_mock.return_value = actual_scm + assert match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, stderr, actual_scm', [ + ('git log', '', 'hg'), + ('git log', 'fatal: Not a git repository ' + '(or any of the parent directories): .git', + None), + ('hg log', "abort: no repository found in '/home/nvbn/exp/thefuck' " + "(.hg not found)!", + None), + ('not-scm log', "abort: no repository found in '/home/nvbn/exp/thefuck' " + "(.hg not found)!", + 'git')]) +def test_not_match(get_actual_scm_mock, script, stderr, actual_scm): + get_actual_scm_mock.return_value = actual_scm + assert not match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, actual_scm, result', [ + ('git log', 'hg', 'hg log'), + ('hg log', 'git', 'git log')]) +def test_get_new_command(get_actual_scm_mock, script, actual_scm, result): + get_actual_scm_mock.return_value = actual_scm + new_command = get_new_command(Command(script)) + assert new_command == result diff --git a/thefuck/rules/scm_correction.py b/thefuck/rules/scm_correction.py new file mode 100644 index 00000000..0b2fc7e5 --- /dev/null +++ b/thefuck/rules/scm_correction.py @@ -0,0 +1,32 @@ +from thefuck.utils import for_app, memoize +from thefuck.system import Path + +path_to_scm = { + '.git': 'git', + '.hg': 'hg', +} + +wrong_scm_patterns = { + 'git': 'fatal: Not a git repository', + 'hg': 'abort: no repository found', +} + + +@memoize +def _get_actual_scm(): + for path, scm in path_to_scm.items(): + if Path(path).is_dir(): + return scm + + +@for_app(*wrong_scm_patterns.keys()) +def match(command): + scm = command.script_parts[0] + pattern = wrong_scm_patterns[scm] + + return pattern in command.stderr and _get_actual_scm() + + +def get_new_command(command): + scm = _get_actual_scm() + return u' '.join([scm] + command.script_parts[1:]) From a015c0f5e271e9e3c668a9c6a75d13f337baafd0 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Sun, 15 Jan 2017 15:11:34 +0100 Subject: [PATCH 48/62] #N/A: Add gem unknown command rule --- README.md | 1 + tests/rules/test_gem_unknown_command.py | 82 +++++++++++++++++++++++++ thefuck/rules/gem_unknown_command.py | 32 ++++++++++ 3 files changed, 115 insertions(+) create mode 100644 tests/rules/test_gem_unknown_command.py create mode 100644 thefuck/rules/gem_unknown_command.py diff --git a/README.md b/README.md index 6e482727..292431fd 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `fab_command_not_found` – fix misspelled fabric commands; * `fix_alt_space` – replaces Alt+Space with Space character; * `fix_file` – opens a file with an error in your `$EDITOR`; +* `gem_unknown_command` – fixes wrong `gem` commands; * `git_add` – fixes *"pathspec 'foo' did not match any file(s) known to git."*; * `git_bisect_usage` – fixes `git bisect strt`, `git bisect goood`, `git bisect rset`, etc. when bisecting; * `git_branch_delete` – changes `git branch -d` to `git branch -D`; diff --git a/tests/rules/test_gem_unknown_command.py b/tests/rules/test_gem_unknown_command.py new file mode 100644 index 00000000..c5d30887 --- /dev/null +++ b/tests/rules/test_gem_unknown_command.py @@ -0,0 +1,82 @@ +import pytest +from six import BytesIO +from thefuck.rules.gem_unknown_command import match, get_new_command +from tests.utils import Command + +stderr = ''' +ERROR: While executing gem ... (Gem::CommandLineError) + Unknown command {} +''' + +gem_help_commands_stdout = b''' +GEM commands are: + + build Build a gem from a gemspec + cert Manage RubyGems certificates and signing settings + check Check a gem repository for added or missing files + cleanup Clean up old versions of installed gems + contents Display the contents of the installed gems + dependency Show the dependencies of an installed gem + environment Display information about the RubyGems environment + fetch Download a gem and place it in the current directory + generate_index Generates the index files for a gem server directory + help Provide help on the 'gem' command + install Install a gem into the local repository + list Display local gems whose name matches REGEXP + lock Generate a lockdown list of gems + mirror Mirror all gem files (requires rubygems-mirror) + open Open gem sources in editor + outdated Display all gems that need updates + owner Manage gem owners of a gem on the push server + pristine Restores installed gems to pristine condition from files + located in the gem cache + push Push a gem up to the gem server + query Query gem information in local or remote repositories + rdoc Generates RDoc for pre-installed gems + search Display remote gems whose name matches REGEXP + server Documentation and gem repository HTTP server + sources Manage the sources and cache file RubyGems uses to search + for gems + specification Display gem specification (in yaml) + stale List gems along with access times + uninstall Uninstall gems from the local repository + unpack Unpack an installed gem to the current directory + update Update installed gems to the latest version + which Find the location of a library file you can require + yank Remove a pushed gem from the index + +For help on a particular command, use 'gem help COMMAND'. + +Commands may be abbreviated, so long as they are unambiguous. +e.g. 'gem i rake' is short for 'gem install rake'. + +''' + + +@pytest.fixture(autouse=True) +def gem_help_commands(mocker): + patch = mocker.patch('subprocess.Popen') + patch.return_value.stdout = BytesIO(gem_help_commands_stdout) + return patch + + +@pytest.mark.parametrize('script, command', [ + ('gem isntall jekyll', 'isntall'), + ('gem last --local', 'last')]) +def test_match(script, command): + assert match(Command(script, stderr=stderr.format(command))) + + +@pytest.mark.parametrize('script, stderr', [ + ('gem install jekyll', ''), + ('git log', stderr.format('log'))]) +def test_not_match(script, stderr): + assert not match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, stderr, result', [ + ('gem isntall jekyll', stderr.format('isntall'), 'gem install jekyll'), + ('gem last --local', stderr.format('last'), 'gem list --local')]) +def test_get_new_command(script, stderr, result): + new_command = get_new_command(Command(script, stderr=stderr)) + assert new_command[0] == result diff --git a/thefuck/rules/gem_unknown_command.py b/thefuck/rules/gem_unknown_command.py new file mode 100644 index 00000000..e20f3ba4 --- /dev/null +++ b/thefuck/rules/gem_unknown_command.py @@ -0,0 +1,32 @@ +import re +import subprocess +from thefuck.utils import for_app, eager, replace_command + + +@for_app('gem') +def match(command): + return ('ERROR: While executing gem ... (Gem::CommandLineError)' + in command.stderr + and 'Unknown command' in command.stderr) + + +def _get_unknown_command(command): + return re.findall(r'Unknown command (.*)$', command.stderr)[0] + + +@eager +def _get_all_commands(): + proc = subprocess.Popen(['gem', 'help', 'commands'], + stdout=subprocess.PIPE) + + for line in proc.stdout.readlines(): + line = line.decode() + + if line.startswith(' '): + yield line.strip().split(' ')[0] + + +def get_new_command(command): + unknown_command = _get_unknown_command(command) + all_commands = _get_all_commands() + return replace_command(command, unknown_command, all_commands) From ace6e882696110c88d97f6693010de471debd9e2 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Fri, 27 Jan 2017 19:43:59 -0500 Subject: [PATCH 49/62] README.md: fix typo in git_stash description from "stashes you local modifications" to "stashes your local modifications" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 292431fd..89956111 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_rm_recursive` – adds `-r` when you try to `rm` a directory; * `git_rebase_merge_dir` – offers `git rebase (--continue | --abort | --skip)` or removing the `.git/rebase-merge` dir when a rebase is in progress; * `git_remote_seturl_add` – runs `git remote add` when `git remote set_url` on nonexistant remote; -* `git_stash` – stashes you local modifications before rebasing or switching branch; +* `git_stash` – stashes your local modifications before rebasing or switching branch; * `git_stash_pop` – adds your local modifications before popping stash, then resets; * `git_two_dashes` – adds a missing dash to commands like `git commit -amend` or `git rebase -continue`; * `go_run` – appends `.go` extension when compiling/running Go programs; From 8da4dce5f251f96ab2141273757950aeaa3e2ef3 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Fri, 27 Jan 2017 19:38:34 -0500 Subject: [PATCH 50/62] Add git_tag_force rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds `--force` to `git tag` when needed. For example: $ git tag alert fatal: tag 'alert' already exists $ fuck git tag --force alert [enter/↑/↓/ctrl+c] Updated tag 'alert' (was dec6956) $ --- README.md | 1 + tests/rules/test_git_tag_force.py | 18 ++++++++++++++++++ thefuck/rules/git_tag_force.py | 13 +++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 tests/rules/test_git_tag_force.py create mode 100644 thefuck/rules/git_tag_force.py diff --git a/README.md b/README.md index 292431fd..28c23132 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_remote_seturl_add` – runs `git remote add` when `git remote set_url` on nonexistant remote; * `git_stash` – stashes you local modifications before rebasing or switching branch; * `git_stash_pop` – adds your local modifications before popping stash, then resets; +* `git_tag_force` – adds `--force` to `git tag ` when the tag already exists; * `git_two_dashes` – adds a missing dash to commands like `git commit -amend` or `git rebase -continue`; * `go_run` – appends `.go` extension when compiling/running Go programs; * `gradle_no_task` – fixes not found or ambiguous `gradle` task; diff --git a/tests/rules/test_git_tag_force.py b/tests/rules/test_git_tag_force.py new file mode 100644 index 00000000..46c96fc6 --- /dev/null +++ b/tests/rules/test_git_tag_force.py @@ -0,0 +1,18 @@ +import pytest +from thefuck.rules.git_tag_force import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(): + return '''fatal: tag 'alert' already exists''' + + +def test_match(stderr): + assert match(Command('git tag alert', stderr=stderr)) + assert not match(Command('git tag alert')) + + +def test_get_new_command(stderr): + assert get_new_command(Command('git tag alert', stderr=stderr)) \ + == "git tag --force alert" diff --git a/thefuck/rules/git_tag_force.py b/thefuck/rules/git_tag_force.py new file mode 100644 index 00000000..6f6de7f2 --- /dev/null +++ b/thefuck/rules/git_tag_force.py @@ -0,0 +1,13 @@ +from thefuck.utils import replace_argument +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return ('tag' in command.script_parts + and 'already exists' in command.stderr) + + +@git_support +def get_new_command(command): + return replace_argument(command.script, 'tag', 'tag --force') From 4d0388b53cf452ba5e7acbccb833d41cdddd97b1 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 28 Jan 2017 13:18:23 -0500 Subject: [PATCH 51/62] Add git_add_force rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds `--force` to `git add` when needed. For example: $ git add dist/*.js The following paths are ignored by one of your .gitignore files: dist/app.js dist/background.js dist/options.js Use -f if you really want to add them. $ fuck git add --force dist/app.js dist/background.js dist/options.js [enter/↑/↓/ctrl+c] $ --- README.md | 1 + tests/rules/test_git_add_force.py | 22 ++++++++++++++++++++++ thefuck/rules/git_add_force.py | 13 +++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 tests/rules/test_git_add_force.py create mode 100644 thefuck/rules/git_add_force.py diff --git a/README.md b/README.md index 292431fd..4b179e8b 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `fix_file` – opens a file with an error in your `$EDITOR`; * `gem_unknown_command` – fixes wrong `gem` commands; * `git_add` – fixes *"pathspec 'foo' did not match any file(s) known to git."*; +* `git_add_force` – adds `--force` to `git add ...` when paths are .gitignore'd; * `git_bisect_usage` – fixes `git bisect strt`, `git bisect goood`, `git bisect rset`, etc. when bisecting; * `git_branch_delete` – changes `git branch -d` to `git branch -D`; * `git_branch_exists` – offers `git branch -d foo`, `git branch -D foo` or `git checkout foo` when creating a branch that already exists; diff --git a/tests/rules/test_git_add_force.py b/tests/rules/test_git_add_force.py new file mode 100644 index 00000000..abe2bd79 --- /dev/null +++ b/tests/rules/test_git_add_force.py @@ -0,0 +1,22 @@ +import pytest +from thefuck.rules.git_add_force import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(): + return ('The following paths are ignored by one of your .gitignore files:\n' + 'dist/app.js\n' + 'dist/background.js\n' + 'dist/options.js\n' + 'Use -f if you really want to add them.\n') + + +def test_match(stderr): + assert match(Command('git add dist/*.js', stderr=stderr)) + assert not match(Command('git add dist/*.js')) + + +def test_get_new_command(stderr): + assert get_new_command(Command('git add dist/*.js', stderr=stderr)) \ + == "git add --force dist/*.js" diff --git a/thefuck/rules/git_add_force.py b/thefuck/rules/git_add_force.py new file mode 100644 index 00000000..9adc0725 --- /dev/null +++ b/thefuck/rules/git_add_force.py @@ -0,0 +1,13 @@ +from thefuck.utils import replace_argument +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return ('add' in command.script_parts + and 'Use -f if you really want to add them.' in command.stderr) + + +@git_support +def get_new_command(command): + return replace_argument(command.script, 'add', 'add --force') From ac7b633e288fad8f5bae06664a9cf6b90b99923a Mon Sep 17 00:00:00 2001 From: Julian Zimmermann Date: Sun, 29 Jan 2017 00:15:55 +0100 Subject: [PATCH 52/62] Added support for "not installed" message in apt_get --- tests/rules/test_apt_get.py | 2 ++ thefuck/rules/apt_get.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/rules/test_apt_get.py b/tests/rules/test_apt_get.py index 1226e522..d669037c 100644 --- a/tests/rules/test_apt_get.py +++ b/tests/rules/test_apt_get.py @@ -7,6 +7,8 @@ from tests.utils import Command (Command(script='vim', stderr='vim: command not found'), [('vim', 'main'), ('vim-tiny', 'main')]), (Command(script='sudo vim', stderr='vim: command not found'), + [('vim', 'main'), ('vim-tiny', 'main')]), + (Command(script='vim', stderr="The program 'vim' is currently not installed. You can install it by typing: sudo apt install vim"), [('vim', 'main'), ('vim-tiny', 'main')])]) def test_match(mocker, command, packages): mocker.patch('thefuck.rules.apt_get.which', return_value=None) diff --git a/thefuck/rules/apt_get.py b/thefuck/rules/apt_get.py index dafc68e0..eaef4818 100644 --- a/thefuck/rules/apt_get.py +++ b/thefuck/rules/apt_get.py @@ -1,6 +1,7 @@ from thefuck.specific.apt import apt_available from thefuck.utils import memoize, which from thefuck.shells import shell +from pprint import pprint try: from CommandNotFound import CommandNotFound @@ -29,7 +30,7 @@ def get_package(executable): def match(command): - if 'not found' in command.stderr: + if 'not found' in command.stderr or 'not installed' in command.stderr: executable = _get_executable(command) return not which(executable) and get_package(executable) else: From 430a7135afb8c46cba0f9c5143a72d0803b4af94 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 30 Jan 2017 13:06:19 +0100 Subject: [PATCH 53/62] #599: Remove unused import --- thefuck/rules/apt_get.py | 1 - 1 file changed, 1 deletion(-) diff --git a/thefuck/rules/apt_get.py b/thefuck/rules/apt_get.py index eaef4818..abe742b3 100644 --- a/thefuck/rules/apt_get.py +++ b/thefuck/rules/apt_get.py @@ -1,7 +1,6 @@ from thefuck.specific.apt import apt_available from thefuck.utils import memoize, which from thefuck.shells import shell -from pprint import pprint try: from CommandNotFound import CommandNotFound From ae68bcbac1e252b743929402e6b8c39c9024bfce Mon Sep 17 00:00:00 2001 From: wouter bolsterlee Date: Wed, 8 Feb 2017 11:59:54 +0100 Subject: [PATCH 54/62] add support for colemak style vi bindings this allows e/n in addition to j/k (same places on the keyboard on colemak and qwerty) to be used as arrow keys when selecting a command from the suggested fixups. fixes #603 --- thefuck/ui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/thefuck/ui.py b/thefuck/ui.py index 2a2f84ba..d30f74d9 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -12,9 +12,10 @@ def read_actions(): while True: key = get_key() - if key in (const.KEY_UP, 'k'): + # Handle arrows, j/k (qwerty), and n/e (colemak) + if key in (const.KEY_UP, 'k', 'e'): yield const.ACTION_PREVIOUS - elif key in (const.KEY_DOWN, 'j'): + elif key in (const.KEY_DOWN, 'j', 'n'): yield const.ACTION_NEXT elif key in (const.KEY_CTRL_C, 'q'): yield const.ACTION_ABORT From bbed17fe07b30a4d54e95b3bfb4c646819ce3b6b Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Thu, 9 Feb 2017 16:09:37 +0100 Subject: [PATCH 55/62] #N/A: Add `sudo_command_from_user_path` rule --- README.md | 1 + .../rules/test_sudo_command_from_user_path.py | 39 +++++++++++++++++++ thefuck/rules/sudo_command_from_user_path.py | 21 ++++++++++ 3 files changed, 61 insertions(+) create mode 100644 tests/rules/test_sudo_command_from_user_path.py create mode 100644 thefuck/rules/sudo_command_from_user_path.py diff --git a/README.md b/README.md index 0aeae4f1..5498bba8 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `sl_ls` – changes `sl` to `ls`; * `ssh_known_hosts` – removes host from `known_hosts` on warning; * `sudo` – prepends `sudo` to previous command if it failed because of permissions; +* `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`; * `test.py` – runs `py.test` instead of `test.py`; diff --git a/tests/rules/test_sudo_command_from_user_path.py b/tests/rules/test_sudo_command_from_user_path.py new file mode 100644 index 00000000..7d6dacee --- /dev/null +++ b/tests/rules/test_sudo_command_from_user_path.py @@ -0,0 +1,39 @@ +import pytest +from thefuck.rules.sudo_command_from_user_path import match, get_new_command +from tests.utils import Command + + +stderr = 'sudo: {}: command not found' + + +@pytest.fixture(autouse=True) +def which(mocker): + return mocker.patch('thefuck.rules.sudo_command_from_user_path.which', + return_value='/usr/bin/app') + + +@pytest.mark.parametrize('script, stderr', [ + ('sudo npm install -g react-native-cli', stderr.format('npm')), + ('sudo -u app appcfg update .', stderr.format('appcfg'))]) +def test_match(script, stderr): + assert match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, stderr, which_result', [ + ('npm --version', stderr.format('npm'), '/usr/bin/npm'), + ('sudo npm --version', '', '/usr/bin/npm'), + ('sudo npm --version', stderr.format('npm'), None)]) +def test_not_match(which, script, stderr, which_result): + which.return_value = which_result + assert not match(Command(script, stderr=stderr)) + + +@pytest.mark.parametrize('script, stderr, result', [ + ('sudo npm install -g react-native-cli', + stderr.format('npm'), + 'sudo env "PATH=$PATH" npm install -g react-native-cli'), + ('sudo -u app appcfg update .', + stderr.format('appcfg'), + 'sudo -u app env "PATH=$PATH" appcfg update .')]) +def test_get_new_command(script, stderr, result): + assert get_new_command(Command(script, stderr=stderr)) == result diff --git a/thefuck/rules/sudo_command_from_user_path.py b/thefuck/rules/sudo_command_from_user_path.py new file mode 100644 index 00000000..39ddace9 --- /dev/null +++ b/thefuck/rules/sudo_command_from_user_path.py @@ -0,0 +1,21 @@ +import re +from thefuck.utils import for_app, which, replace_argument + + +def _get_command_name(command): + found = re.findall(r'sudo: (.*): command not found', command.stderr) + if found: + return found[0] + + +@for_app('sudo') +def match(command): + if 'command not found' in command.stderr: + command_name = _get_command_name(command) + return which(command_name) + + +def get_new_command(command): + command_name = _get_command_name(command) + return replace_argument(command.script, command_name, + u'env "PATH=$PATH" {}'.format(command_name)) From e893521872c80272e92f21073885297ec542c38c Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Thu, 9 Feb 2017 16:13:09 +0100 Subject: [PATCH 56/62] #N/A: Run coveralls only on full test run (python 3.6 with linux) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a1018d8a..2798eed9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,4 +47,4 @@ script: - if [[ $TRAVIS_PYTHON_VERSION == 3.6 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi - if [[ $TRAVIS_PYTHON_VERSION != 3.6 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi after_success: - - coveralls + - if [[ $TRAVIS_PYTHON_VERSION == 3.6 && $TRAVIS_OS_NAME != "osx" ]]; then coveralls; fi From 778f2bdf1af8c86700fc8ce7418567c7c26f4a73 Mon Sep 17 00:00:00 2001 From: Andrew Epstein Date: Wed, 22 Feb 2017 07:56:40 -0500 Subject: [PATCH 57/62] Improve performance of history rule --- thefuck/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/utils.py b/thefuck/utils.py index f6c0bb71..2bb20a22 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -264,7 +264,7 @@ def get_valid_history_without_current(command): from thefuck.shells import shell history = shell.get_history() tf_alias = get_alias() - executables = get_all_executables() + executables = set(get_all_executables()) return [line for line in _not_corrected(history, tf_alias) if not line.startswith(tf_alias) and not line == command.script and line.split(' ')[0] in executables] From 42ec01dab1287b62f6079a40ba50dedb6e67a9ea Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sun, 26 Feb 2017 19:16:15 -0500 Subject: [PATCH 58/62] Add git_rm_staged rule for removing locally staged changes It would be nice if `thefuck` could help me `git rm` a file I had already staged. This rule, adapted from `git_rm_local_modifications`, does that. --- README.md | 1 + tests/rules/test_git_rm_staged.py | 28 ++++++++++++++++++++++++++++ thefuck/rules/git_rm_staged.py | 19 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 tests/rules/test_git_rm_staged.py create mode 100644 thefuck/rules/git_rm_staged.py diff --git a/README.md b/README.md index 5498bba8..2ffd8994 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_rebase_no_changes` – runs `git rebase --skip` instead of `git rebase --continue` when there are no changes; * `git_rm_local_modifications` – adds `-f` or `--cached` when you try to `rm` a locally modified file; * `git_rm_recursive` – adds `-r` when you try to `rm` a directory; +* `git_rm_staged` – adds `-f` or `--cached` when you try to `rm` a file with staged changes * `git_rebase_merge_dir` – offers `git rebase (--continue | --abort | --skip)` or removing the `.git/rebase-merge` dir when a rebase is in progress; * `git_remote_seturl_add` – runs `git remote add` when `git remote set_url` on nonexistant remote; * `git_stash` – stashes your local modifications before rebasing or switching branch; diff --git a/tests/rules/test_git_rm_staged.py b/tests/rules/test_git_rm_staged.py new file mode 100644 index 00000000..125ae998 --- /dev/null +++ b/tests/rules/test_git_rm_staged.py @@ -0,0 +1,28 @@ +import pytest +from thefuck.rules.git_rm_staged import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(target): + return ('error: the following file has changes staged in the index:\n {}\n(use ' + '--cached to keep the file, or -f to force removal)').format(target) + + +@pytest.mark.parametrize('script, target', [ + ('git rm foo', 'foo'), + ('git rm foo bar', 'bar')]) +def test_match(stderr, script, target): + assert match(Command(script=script, stderr=stderr)) + + +@pytest.mark.parametrize('script', ['git rm foo', 'git rm foo bar', 'git rm']) +def test_not_match(script): + assert not match(Command(script=script, stderr='')) + + +@pytest.mark.parametrize('script, target, new_command', [ + ('git rm foo', 'foo', ['git rm --cached foo', 'git rm -f foo']), + ('git rm foo bar', 'bar', ['git rm --cached foo bar', 'git rm -f foo bar'])]) +def test_get_new_command(stderr, script, target, new_command): + assert get_new_command(Command(script=script, stderr=stderr)) == new_command diff --git a/thefuck/rules/git_rm_staged.py b/thefuck/rules/git_rm_staged.py new file mode 100644 index 00000000..7532a1f6 --- /dev/null +++ b/thefuck/rules/git_rm_staged.py @@ -0,0 +1,19 @@ +from thefuck.specific.git import git_support + + +@git_support +def match(command): + return (' rm ' in command.script and + 'error: the following file has changes staged in the index' in command.stderr and + 'use --cached to keep the file, or -f to force removal' in command.stderr) + + +@git_support +def get_new_command(command): + command_parts = command.script_parts[:] + index = command_parts.index('rm') + 1 + command_parts.insert(index, '--cached') + command_list = [u' '.join(command_parts)] + command_parts[index] = '-f' + command_list.append(u' '.join(command_parts)) + return command_list From 7e16a2eb7cdcd92d43cddd57e84c073ab7a51cd2 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sun, 26 Feb 2017 20:26:15 -0500 Subject: [PATCH 59/62] Fix aliased `yarn` commands like `yarn ls` [Yarn] has a handful of subcommand [aliases], but does not automatically [correct] them for the user. This makes it so that `fuck` will do the trick. For example: $ yarn ls yarn ls v0.20.3 error Did you mean `yarn list`? info Visit https://yarnpkg.com/en/docs/cli/list for documentation about this command. $ fuck yarn list [enter/?/?/ctrl+c] [Yarn]: https://yarnpkg.com/en/ [aliases]: https://github.com/yarnpkg/yarn/blob/0adbc59b18b38b6ac6e4b248e19788ed1d2e80da/src/cli/aliases.js [correct]: https://github.com/yarnpkg/yarn/pull/1044#issuecomment-253763230 --- README.md | 1 + tests/rules/test_yarn_alias.py | 22 ++++++++++++++++++++++ thefuck/rules/yarn_alias.py | 14 ++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 tests/rules/test_yarn_alias.py create mode 100644 thefuck/rules/yarn_alias.py diff --git a/README.md b/README.md index 5498bba8..12199293 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `vagrant_up` – starts up the vagrant instance; * `whois` – fixes `whois` command; * `workon_doesnt_exists` – fixes `virtualenvwrapper` env name os suggests to create new. +* `yarn_alias` – fixes aliased `yarn` commands like `yarn ls`; Enabled by default only on specific platforms: diff --git a/tests/rules/test_yarn_alias.py b/tests/rules/test_yarn_alias.py new file mode 100644 index 00000000..83d38117 --- /dev/null +++ b/tests/rules/test_yarn_alias.py @@ -0,0 +1,22 @@ +import pytest +from thefuck.rules.yarn_alias import match, get_new_command +from tests.utils import Command + + +stderr_remove = 'error Did you mean `yarn remove`?' + +stderr_list = 'error Did you mean `yarn list`?' + + +@pytest.mark.parametrize('command', [ + Command(script='yarn rm', stderr=stderr_remove), + Command(script='yarn ls', stderr=stderr_list)]) +def test_match(command): + assert match(command) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('yarn rm', stderr=stderr_remove), 'yarn remove'), + (Command('yarn ls', stderr=stderr_list), 'yarn list')]) +def test_get_new_command(command, new_command): + assert get_new_command(command) == new_command diff --git a/thefuck/rules/yarn_alias.py b/thefuck/rules/yarn_alias.py new file mode 100644 index 00000000..4c1a584b --- /dev/null +++ b/thefuck/rules/yarn_alias.py @@ -0,0 +1,14 @@ +import re +from thefuck.utils import replace_argument, for_app + + +@for_app('yarn', at_least=1) +def match(command): + return ('Did you mean' in command.stderr) + + +def get_new_command(command): + broken = command.script_parts[1] + fix = re.findall(r'Did you mean `yarn ([^`]*)`', command.stderr)[0] + + return replace_argument(command.script, broken, fix) From 4669a033ee4fbde5e3c2447778657a20a73d5df8 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Wed, 1 Mar 2017 09:32:55 -0800 Subject: [PATCH 60/62] Update PowerShell alias to handle no history If history is cleared (or the shell is new and there is no history), invoking thefuck results in an error because the alias attempts to execute the usage string. The fix is to check if Get-History returns anything before invoking thefuck. --- thefuck/shells/powershell.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/thefuck/shells/powershell.py b/thefuck/shells/powershell.py index 8e5d994d..bf954bf3 100644 --- a/thefuck/shells/powershell.py +++ b/thefuck/shells/powershell.py @@ -3,11 +3,14 @@ from .generic import Generic class Powershell(Generic): def app_alias(self, fuck): - return 'function ' + fuck + ' { \n' \ - ' $fuck = $(thefuck (Get-History -Count 1).CommandLine);\n' \ - ' if (-not [string]::IsNullOrWhiteSpace($fuck)) {\n' \ - ' if ($fuck.StartsWith("echo")) { $fuck = $fuck.Substring(5); }\n' \ - ' else { iex "$fuck"; }\n' \ + return 'function ' + fuck + ' {\n' \ + ' $history = (Get-History -Count 1).CommandLine;\n' \ + ' if (-not [string]::IsNullOrWhiteSpace($history)) {\n' \ + ' $fuck = $(thefuck $history);\n' \ + ' if (-not [string]::IsNullOrWhiteSpace($fuck)) {\n' \ + ' if ($fuck.StartsWith("echo")) { $fuck = $fuck.Substring(5); }\n' \ + ' else { iex "$fuck"; }\n' \ + ' }\n' \ ' }\n' \ '}\n' From df5428c5e4fc193866c53e0a0f6448a1db2e1ee7 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Fri, 3 Mar 2017 23:38:20 -0500 Subject: [PATCH 61/62] Add `yarn_command_not_found` rule This addresses https://github.com/nvbn/thefuck/pull/607#issuecomment-283945505 The code was adapted from the `grunt_task_not_found` rule --- README.md | 1 + tests/rules/test_yarn_command_not_found.py | 111 +++++++++++++++++++++ thefuck/rules/yarn_command_not_found.py | 33 ++++++ 3 files changed, 145 insertions(+) create mode 100644 tests/rules/test_yarn_command_not_found.py create mode 100644 thefuck/rules/yarn_command_not_found.py diff --git a/README.md b/README.md index acc222d3..3e84a35a 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `whois` – fixes `whois` command; * `workon_doesnt_exists` – fixes `virtualenvwrapper` env name os suggests to create new. * `yarn_alias` – fixes aliased `yarn` commands like `yarn ls`; +* `yarn_command_not_found` – fixes misspelled `yarn` commands; Enabled by default only on specific platforms: diff --git a/tests/rules/test_yarn_command_not_found.py b/tests/rules/test_yarn_command_not_found.py new file mode 100644 index 00000000..8a4eba70 --- /dev/null +++ b/tests/rules/test_yarn_command_not_found.py @@ -0,0 +1,111 @@ +# -*- encoding: utf-8 -*- + +from io import BytesIO +import pytest +from tests.utils import Command +from thefuck.rules.yarn_command_not_found import match, get_new_command + +stderr = ''' +error Command "{}" not found. +'''.format + +yarn_help_stdout = b''' + + Usage: yarn [command] [flags] + + Options: + + -h, --help output usage information + -V, --version output the version number + --verbose output verbose messages on internal operations + --offline trigger an error if any required dependencies are not available in local cache + --prefer-offline use network only if dependencies are not available in local cache + --strict-semver + --json + --ignore-scripts don't run lifecycle scripts + --har save HAR output of network traffic + --ignore-platform ignore platform checks + --ignore-engines ignore engines check + --ignore-optional ignore optional dependencies + --force ignore all caches + --no-bin-links don't generate bin links when setting up packages + --flat only allow one version of a package + --prod, --production [prod] + --no-lockfile don't read or generate a lockfile + --pure-lockfile don't generate a lockfile + --frozen-lockfile don't generate a lockfile and fail if an update is needed + --link-duplicates create hardlinks to the repeated modules in node_modules + --global-folder + --modules-folder rather than installing modules into the node_modules folder relative to the cwd, output them here + --cache-folder specify a custom folder to store the yarn cache + --mutex [:specifier] use a mutex to ensure only one yarn instance is executing + --no-emoji disable emoji in output + --proxy + --https-proxy + --no-progress disable progress bar + --network-concurrency maximum number of concurrent network requests + + Commands: + + - access + - add + - bin + - cache + - check + - clean + - config + - generate-lock-entry + - global + - import + - info + - init + - install + - licenses + - link + - list + - login + - logout + - outdated + - owner + - pack + - publish + - remove + - run + - tag + - team + - unlink + - upgrade + - upgrade-interactive + - version + - versions + - why + + Run `yarn help COMMAND` for more information on specific commands. + Visit https://yarnpkg.com/en/docs/cli/ to learn more about Yarn. +''' + + +@pytest.fixture(autouse=True) +def yarn_help(mocker): + patch = mocker.patch('thefuck.rules.yarn_command_not_found.Popen') + patch.return_value.stdout = BytesIO(yarn_help_stdout) + return patch + + +@pytest.mark.parametrize('command', [ + Command('yarn whyy webpack', stderr=stderr('whyy'))]) +def test_match(command): + assert match(command) + + +@pytest.mark.parametrize('command', [ + Command('npm nuild', stderr=stderr('nuild')), + Command('yarn install')]) +def test_not_match(command): + assert not match(command) + + +@pytest.mark.parametrize('command, result', [ + (Command('yarn whyy webpack', stderr=stderr('whyy')), 'yarn why webpack')]) +def test_get_new_command(command, result): + assert get_new_command(command) == result diff --git a/thefuck/rules/yarn_command_not_found.py b/thefuck/rules/yarn_command_not_found.py new file mode 100644 index 00000000..0a425f22 --- /dev/null +++ b/thefuck/rules/yarn_command_not_found.py @@ -0,0 +1,33 @@ +import re +from subprocess import Popen, PIPE +from thefuck.utils import for_app, eager, get_closest + +regex = re.compile(r'error Command "(.*)" not found.') + + +@for_app('yarn') +def match(command): + return regex.findall(command.stderr) + + +@eager +def _get_all_tasks(): + proc = Popen(['yarn', '--help'], stdout=PIPE) + should_yield = False + for line in proc.stdout.readlines(): + line = line.decode().strip() + + if 'Commands:' in line: + should_yield = True + continue + + if should_yield and '- ' in line: + yield line.split(' ')[-1] + + +def get_new_command(command): + misspelled_task = regex.findall(command.stderr)[0] + tasks = _get_all_tasks() + fixed = get_closest(misspelled_task, tasks) + return command.script.replace(' {}'.format(misspelled_task), + ' {}'.format(fixed)) From 02f3250d39305c3154c7645a724126e95c09ea87 Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Mon, 6 Mar 2017 17:31:57 +0100 Subject: [PATCH 62/62] #609: Use `replace_command` in `yarn_command_not_found` --- tests/rules/test_yarn_command_not_found.py | 2 +- thefuck/rules/yarn_command_not_found.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/rules/test_yarn_command_not_found.py b/tests/rules/test_yarn_command_not_found.py index 8a4eba70..67b7823b 100644 --- a/tests/rules/test_yarn_command_not_found.py +++ b/tests/rules/test_yarn_command_not_found.py @@ -108,4 +108,4 @@ def test_not_match(command): @pytest.mark.parametrize('command, result', [ (Command('yarn whyy webpack', stderr=stderr('whyy')), 'yarn why webpack')]) def test_get_new_command(command, result): - assert get_new_command(command) == result + assert get_new_command(command)[0] == result diff --git a/thefuck/rules/yarn_command_not_found.py b/thefuck/rules/yarn_command_not_found.py index 0a425f22..6d51ff84 100644 --- a/thefuck/rules/yarn_command_not_found.py +++ b/thefuck/rules/yarn_command_not_found.py @@ -1,6 +1,6 @@ import re from subprocess import Popen, PIPE -from thefuck.utils import for_app, eager, get_closest +from thefuck.utils import for_app, eager, replace_command regex = re.compile(r'error Command "(.*)" not found.') @@ -28,6 +28,4 @@ def _get_all_tasks(): def get_new_command(command): misspelled_task = regex.findall(command.stderr)[0] tasks = _get_all_tasks() - fixed = get_closest(misspelled_task, tasks) - return command.script.replace(' {}'.format(misspelled_task), - ' {}'.format(fixed)) + return replace_command(command, misspelled_task, tasks)