From 64318c09b728e1bf45fd9c431139812d41bab917 Mon Sep 17 00:00:00 2001 From: nvbn Date: Mon, 11 May 2015 14:16:23 +0200 Subject: [PATCH 01/54] #161 support different psutils versions --- thefuck/shells.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index e1203f45..2ed5c483 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -98,7 +98,10 @@ shells = defaultdict(lambda: Generic(), { def _get_shell(): - shell = Process(os.getpid()).parent().cmdline()[0] + try: + shell = Process(os.getpid()).parent().cmdline()[0] + except TypeError: + shell = Process(os.getpid()).parent.cmdline[0] return shells[shell] From 0fc7c00e8dd5b90054b5b0d41c1c25239c18c878 Mon Sep 17 00:00:00 2001 From: nvbn Date: Mon, 11 May 2015 14:16:59 +0200 Subject: [PATCH 02/54] Bump to 1.40 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c79654f0..147d9422 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.39' +VERSION = '1.40' setup(name='thefuck', From 484a53e314356985f0ba9c3215077bcf45a0ff23 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Mon, 11 May 2015 23:58:53 -0300 Subject: [PATCH 03/54] fix(brew_unknown_command): make subprocess.check_output return str Fix `TypeError: can't concat bytes to str` error on Python 3.4. Signed-off-by: Pablo Santiago Blum de Aguiar --- thefuck/rules/brew_unknown_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/thefuck/rules/brew_unknown_command.py b/thefuck/rules/brew_unknown_command.py index 6664d8e8..26c0924f 100644 --- a/thefuck/rules/brew_unknown_command.py +++ b/thefuck/rules/brew_unknown_command.py @@ -12,7 +12,8 @@ TAP_CMD_PATH = '/%s/%s/cmd' def _get_brew_path_prefix(): """To get brew path""" try: - return subprocess.check_output(['brew', '--prefix']).strip() + return subprocess.check_output(['brew', '--prefix'], + universal_newlines=True).strip() except: return None From 7489040f8fc1332e5609b48a2707a212f9532b0e Mon Sep 17 00:00:00 2001 From: SanketDG Date: Tue, 12 May 2015 14:29:00 +0530 Subject: [PATCH 04/54] fix thefuck-alias --- thefuck/shells.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index 2ed5c483..90b560e5 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -32,7 +32,7 @@ class Generic(object): return command_script def app_alias(self): - return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" + print "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" def _get_history_file_name(self): return '' From e8de4ee7e88173954e2e397e253ac484b8751397 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 12 May 2015 14:22:20 +0200 Subject: [PATCH 05/54] #185 Fix python 3 --- thefuck/shells.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index 90b560e5..7b3aafd2 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -32,7 +32,7 @@ class Generic(object): return command_script def app_alias(self): - print "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" + return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n" def _get_history_file_name(self): return '' @@ -114,7 +114,7 @@ def to_shell(command): def app_alias(): - return _get_shell().app_alias() + print(_get_shell().app_alias()) def put_to_history(command): From 8ac4dafe6d89f0d405ca4acad140d75482a00dc4 Mon Sep 17 00:00:00 2001 From: mcarton Date: Tue, 12 May 2015 19:41:00 +0200 Subject: [PATCH 06/54] Add a git_stash rule --- README.md | 1 + thefuck/rules/git_stash.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 thefuck/rules/git_stash.py diff --git a/README.md b/README.md index d3016f9e..ef74cc79 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_checkout` – creates the branch before checking-out; * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; +* `git_stash` – stashes you local modifications before rebase; * `has_exists_script` – prepends `./` when script/binary exists; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; diff --git a/thefuck/rules/git_stash.py b/thefuck/rules/git_stash.py new file mode 100644 index 00000000..a95d8f49 --- /dev/null +++ b/thefuck/rules/git_stash.py @@ -0,0 +1,7 @@ +def match(command, settings): + return ('git' in command.script + and 'Please commit or stash them.' in command.stderr) + + +def get_new_command(command, settings): + return 'git stash && ' + command.script From 65c624ad52ff577cd0b78b34b64130c91c39589f Mon Sep 17 00:00:00 2001 From: mcarton Date: Wed, 13 May 2015 09:47:31 +0200 Subject: [PATCH 07/54] Improve the git_stash rule --- thefuck/rules/git_stash.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/thefuck/rules/git_stash.py b/thefuck/rules/git_stash.py index a95d8f49..58bfbb38 100644 --- a/thefuck/rules/git_stash.py +++ b/thefuck/rules/git_stash.py @@ -1,6 +1,7 @@ def match(command, settings): - return ('git' in command.script - and 'Please commit or stash them.' in command.stderr) + # catches "Please commit or stash them" and "Please, commit your changes or + # stash them before you can switch branches." + return 'git' in command.script and 'or stash them' in command.stderr def get_new_command(command, settings): From 14d14c5ac6543370e7ce1bd6d51316a4b150ad02 Mon Sep 17 00:00:00 2001 From: mcarton Date: Wed, 13 May 2015 09:49:45 +0200 Subject: [PATCH 08/54] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef74cc79..63092266 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_checkout` – creates the branch before checking-out; * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; -* `git_stash` – stashes you local modifications before rebase; +* `git_stash` – stashes you local modifications before rebasing or switching branch; * `has_exists_script` – prepends `./` when script/binary exists; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; From 7b29b54ac7ceffcbff966eb5685dec1f81cf399a Mon Sep 17 00:00:00 2001 From: Hugh Macdonald Date: Wed, 13 May 2015 15:55:33 +0100 Subject: [PATCH 09/54] Add initial tcsh support. Still require better history support --- README.md | 5 +++++ thefuck/shells.py | 27 ++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 63092266..e548f918 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,11 @@ Or in your `.zshrc`: alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R' ``` +If you are using `tcsh`: +```tcsh +alias fuck 'set fuckedCmd=`history -h 2 | head -n 1` && eval `thefuck ${fuckedCmd}`' +``` + Alternatively, you can redirect the output of `thefuck-alias`: ```bash diff --git a/thefuck/shells.py b/thefuck/shells.py index 7b3aafd2..940f196f 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -92,9 +92,34 @@ class Zsh(Generic): return u': {}:0;{}\n'.format(int(time()), command_script) +class Tcsh(Generic): + def app_alias(self): + return "\nalias fuck 'set fucked_cmd=`history -h 2 | head -n 1` && eval `thefuck ${fucked_cmd}`'\n" + + def _parse_alias(self, alias): + name, value = alias.split("\t", 1) + return name, value + + def _get_aliases(self): + proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '\t' in alias) + + def _get_history_file_name(self): + return os.environ.get("HISTFILE", + os.path.expanduser('~/.history')) + + def _get_history_line(self, command_script): + return u'#+{}\n{}\n'.format(int(time()), command_script) + + shells = defaultdict(lambda: Generic(), { 'bash': Bash(), - 'zsh': Zsh()}) + 'zsh': Zsh(), + '-csh': Tcsh(), + 'tcsh': Tcsh()}) def _get_shell(): From 239f91b670810e127ae4551c055acb9df7ba0a38 Mon Sep 17 00:00:00 2001 From: Hugh Macdonald Date: Wed, 13 May 2015 15:56:50 +0100 Subject: [PATCH 10/54] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e548f918..6b507ee4 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R' If you are using `tcsh`: ```tcsh -alias fuck 'set fuckedCmd=`history -h 2 | head -n 1` && eval `thefuck ${fuckedCmd}`' +alias fuck 'set fucked_cmd=`history -h 2 | head -n 1` && eval `thefuck ${fucked_cmd}`' ``` Alternatively, you can redirect the output of `thefuck-alias`: From d2e511fa2c591738ba83cb3ba199536cff709785 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Wed, 13 May 2015 10:36:00 -0300 Subject: [PATCH 11/54] refact(brew_install): remove an unused import Signed-off-by: Pablo Santiago Blum de Aguiar --- thefuck/rules/brew_install.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/thefuck/rules/brew_install.py b/thefuck/rules/brew_install.py index 0f339506..597a8f8b 100644 --- a/thefuck/rules/brew_install.py +++ b/thefuck/rules/brew_install.py @@ -3,8 +3,6 @@ import os import re from subprocess import check_output -import thefuck.logs - # Formulars are base on each local system's status brew_formulas = [] try: From 9cf41f8e439d89d4f8614069e70fa86b3d91be39 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Wed, 13 May 2015 10:36:46 -0300 Subject: [PATCH 12/54] fix(brew_install): make subprocess.check_output return str This fix makes the `brew_install` rule work on Python 3. Signed-off-by: Pablo Santiago Blum de Aguiar --- thefuck/rules/brew_install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/thefuck/rules/brew_install.py b/thefuck/rules/brew_install.py index 597a8f8b..39deb236 100644 --- a/thefuck/rules/brew_install.py +++ b/thefuck/rules/brew_install.py @@ -6,7 +6,8 @@ from subprocess import check_output # Formulars are base on each local system's status brew_formulas = [] try: - brew_path_prefix = check_output(['brew', '--prefix']).strip() + brew_path_prefix = check_output(['brew', '--prefix'], + universal_newlines=True).strip() brew_formula_path = brew_path_prefix + '/Library/Formula' for file_name in os.listdir(brew_formula_path): From 3c4f9d50a945a13c813884c795c78e8634940cf7 Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 15 May 2015 18:03:17 +0200 Subject: [PATCH 13/54] Add a `no_such_file` rule --- README.md | 1 + thefuck/rules/no_such_file.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 thefuck/rules/no_such_file.py diff --git a/README.md b/README.md index 6b507ee4..9cf7237f 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,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`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; +* `no_such_file` – creates missing directories with `mv` and `cp` commands; * `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `pacman` – installs app with `pacman` or `yaourt` if it is not installed; * `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; diff --git a/thefuck/rules/no_such_file.py b/thefuck/rules/no_such_file.py new file mode 100644 index 00000000..157a41a6 --- /dev/null +++ b/thefuck/rules/no_such_file.py @@ -0,0 +1,26 @@ +import re + + +patterns = ( + r"mv: cannot move '[^']*' to '([^']*)': No such file or directory", + r"cp: cannot create regular file '([^']*)': No such file or directory", +) + + +def match(command, settings): + for pattern in patterns: + if re.search(pattern, command.stderr): + return True + + return False + + +def get_new_command(command, settings): + for pattern in patterns: + file = re.findall(pattern, command.stderr) + + if file: + file = file[0] + dir = file[0:file.rfind('/')] + + return 'mkdir -p {} && {}'.format(dir, command.script) From 5504aa44a1b47d6533c65c0f5884f11eda06359b Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 15 May 2015 18:03:33 +0200 Subject: [PATCH 14/54] Add tests for the `no_such_file` rule --- tests/rules/test_no_such_file.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/rules/test_no_such_file.py diff --git a/tests/rules/test_no_such_file.py b/tests/rules/test_no_such_file.py new file mode 100644 index 00000000..ba35477c --- /dev/null +++ b/tests/rules/test_no_such_file.py @@ -0,0 +1,19 @@ +import pytest +from thefuck.rules.no_such_file import match, get_new_command +from tests.utils import Command + + +@pytest.mark.parametrize('command', [ + Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"), + Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"), + ]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"), 'mkdir -p bar && mv foo bar/foo'), + (Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"), 'mkdir -p bar && mv foo bar/'), + ]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command From 08a2065119b00f57a12549affd242d7f2e18f9a3 Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 15 May 2015 18:08:43 +0200 Subject: [PATCH 15/54] Add missing cases for the `no_such_file` rule --- thefuck/rules/no_such_file.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/thefuck/rules/no_such_file.py b/thefuck/rules/no_such_file.py index 157a41a6..9a0f3b45 100644 --- a/thefuck/rules/no_such_file.py +++ b/thefuck/rules/no_such_file.py @@ -3,7 +3,9 @@ import re patterns = ( r"mv: cannot move '[^']*' to '([^']*)': No such file or directory", + r"mv: cannot move '[^']*' to '([^']*)': Not a directory", r"cp: cannot create regular file '([^']*)': No such file or directory", + r"cp: cannot create regular file '([^']*)': Not a directory", ) From 744f17d9055d55b87e55eab1c5aa4a0cc910b313 Mon Sep 17 00:00:00 2001 From: mcarton Date: Fri, 15 May 2015 18:39:03 +0200 Subject: [PATCH 16/54] Add a `whois` rule --- thefuck/rules/whois.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 thefuck/rules/whois.py diff --git a/thefuck/rules/whois.py b/thefuck/rules/whois.py new file mode 100644 index 00000000..f019758e --- /dev/null +++ b/thefuck/rules/whois.py @@ -0,0 +1,30 @@ +from urllib.parse import urlparse + + +def match(command, settings): + """ + What the `whois` command returns depends on the 'Whois server' it contacted + and is not consistent through different servers. But there can be only two + types of errors I can think of with `whois`: + - `whois https://en.wikipedia.org/` → `whois en.wikipedia.org`; + - `whois en.wikipedia.org` → `whois wikipedia.org`. + So we match any `whois` command and then: + - if there is a slash: keep only the FQDN; + - if there is no slash but there is a point: removes the left-most + subdomain. + + We cannot either remove all subdomains because we cannot know which part is + the subdomains and which is the domain, consider: + - www.google.fr → subdomain: www, domain: 'google.fr'; + - google.co.uk → subdomain: None, domain; 'google.co.uk'. + """ + return 'whois' in command.script + + +def get_new_command(command, settings): + url = command.script.split()[1] + + if '/' in command.script: + return 'whois ' + urlparse(url).netloc + elif '.' in command.script: + return 'whois ' + '.'.join(urlparse(url).path.split('.')[1:]) From fc8f1b11361c2aff3e42cc2d2e39b07c505aa3d9 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Fri, 15 May 2015 15:53:37 -0300 Subject: [PATCH 17/54] fix(pacman): make the entire rule py2-compatible One reference to subprocess.DEVNULL remained. Signed-off-by: Pablo Santiago Blum de Aguiar --- thefuck/rules/pacman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/rules/pacman.py b/thefuck/rules/pacman.py index 41157986..71c6e698 100644 --- a/thefuck/rules/pacman.py +++ b/thefuck/rules/pacman.py @@ -17,7 +17,7 @@ def __get_pkgfile(command): try: return subprocess.check_output( ['pkgfile', '-b', '-v', command.script.split(" ")[0]], - universal_newlines=True, stderr=subprocess.DEVNULL + universal_newlines=True, stderr=DEVNULL ).split() except subprocess.CalledProcessError: return None From d5bd57fb4978d4a204f51d65e1e526197f41e955 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Fri, 15 May 2015 19:09:14 -0300 Subject: [PATCH 18/54] Adding rule for forgotten '-r' when grepping folders --- tests/rules/test_grep_recursive.py | 12 ++++++++++++ thefuck/rules/grep_recursive.py | 7 +++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/rules/test_grep_recursive.py create mode 100644 thefuck/rules/grep_recursive.py diff --git a/tests/rules/test_grep_recursive.py b/tests/rules/test_grep_recursive.py new file mode 100644 index 00000000..0e3dae1d --- /dev/null +++ b/tests/rules/test_grep_recursive.py @@ -0,0 +1,12 @@ +from thefuck.rules.grep_recursive import match, get_new_command +from tests.utils import Command + + +def test_match(): + assert match(Command('grep blah .', stderr='grep: .: Is a directory'), None) + assert not match(Command(), None) + + +def test_get_new_command(): + assert get_new_command( + Command('grep blah .'), None) == 'grep -r blah .' diff --git a/thefuck/rules/grep_recursive.py b/thefuck/rules/grep_recursive.py new file mode 100644 index 00000000..ed0b6fdb --- /dev/null +++ b/thefuck/rules/grep_recursive.py @@ -0,0 +1,7 @@ +def match(command, settings): + return (command.script.startswith('grep') + and 'is a directory' in command.stderr.lower()) + + +def get_new_command(command, settings): + return 'grep -r {}'.format(command.script[5:]) From 5f2b2433b15ca3941a75bc57c45637a5e7f5edce Mon Sep 17 00:00:00 2001 From: mcarton Date: Sat, 16 May 2015 15:25:32 +0200 Subject: [PATCH 19/54] Cleanup `pacman` rule --- thefuck/rules/pacman.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/thefuck/rules/pacman.py b/thefuck/rules/pacman.py index 71c6e698..12fa9430 100644 --- a/thefuck/rules/pacman.py +++ b/thefuck/rules/pacman.py @@ -1,16 +1,5 @@ import subprocess -from thefuck.utils import DEVNULL - - -def __command_available(command): - try: - subprocess.check_output([command], stderr=DEVNULL) - return True - except subprocess.CalledProcessError: - # command exists but is not happy to be called without any argument - return True - except OSError: - return False +from thefuck.utils import DEVNULL, which def __get_pkgfile(command): @@ -33,11 +22,11 @@ def get_new_command(command, settings): return '{} -S {} && {}'.format(pacman, package, command.script) -if not __command_available('pkgfile'): +if not which('pkgfile'): enabled_by_default = False -elif __command_available('yaourt'): +elif which('yaourt'): pacman = 'yaourt' -elif __command_available('pacman'): +elif which('pacman'): pacman = 'sudo pacman' else: enabled_by_default = False From 6539c853b4838c7c6ea049604ddb5bcd024ad6ba Mon Sep 17 00:00:00 2001 From: mcarton Date: Sat, 16 May 2015 15:36:27 +0200 Subject: [PATCH 20/54] Add tests for the `whois` rule --- tests/rules/test_whois.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/rules/test_whois.py diff --git a/tests/rules/test_whois.py b/tests/rules/test_whois.py new file mode 100644 index 00000000..b911106e --- /dev/null +++ b/tests/rules/test_whois.py @@ -0,0 +1,19 @@ +import pytest +from thefuck.rules.whois import match, get_new_command +from tests.utils import Command + + +@pytest.mark.parametrize('command', [ + Command(script='whois https://en.wikipedia.org/wiki/Main_Page'), + Command(script='whois https://en.wikipedia.org/'), + Command(script='whois en.wikipedia.org')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('whois https://en.wikipedia.org/wiki/Main_Page'), 'whois en.wikipedia.org'), + (Command('whois https://en.wikipedia.org/'), 'whois en.wikipedia.org'), + (Command('whois en.wikipedia.org'), 'whois wikipedia.org')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command From bb4b42d2f1b483a263937e7c90253bfb476df0e6 Mon Sep 17 00:00:00 2001 From: mcarton Date: Sat, 16 May 2015 15:37:00 +0200 Subject: [PATCH 21/54] Add the `whois` rule in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9cf7237f..1ac70f45 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `ssh_known_hosts` – removes host from `known_hosts` on warning; * `sudo` – prepends `sudo` to previous command if it failed because of permissions; * `switch_layout` – switches command from your local layout to en; +* `whois` – fixes `whois` command; * `apt_get` – installs app from apt if it not installed; * `brew_install` – fixes formula name for `brew install`; * `composer_not_command` – fixes composer command name. From d854320acc27f95984857673af20a6931a118e60 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Wed, 13 May 2015 16:17:53 -0300 Subject: [PATCH 22/54] refact(shells): add specific `app_alias` methods for Bash and Zsh Signed-off-by: Pablo Santiago Blum de Aguiar --- thefuck/shells.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/thefuck/shells.py b/thefuck/shells.py index 940f196f..50d9145e 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -49,6 +49,9 @@ class Generic(object): class Bash(Generic): + def app_alias(self): + return "\nalias fuck='eval $(thefuck $(fc -ln -1)); history -r'\n" + def _parse_alias(self, alias): name, value = alias.replace('alias ', '', 1).split('=', 1) if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": @@ -71,6 +74,9 @@ class Bash(Generic): class Zsh(Generic): + def app_alias(self): + return "\nalias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'\n" + def _parse_alias(self, alias): name, value = alias.split('=', 1) if value[0] == value[-1] == '"' or value[0] == value[-1] == "'": From 3d0d4be4a9ce51b67e1278b47ca5c593834b3d59 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Tue, 12 May 2015 21:13:51 -0300 Subject: [PATCH 23/54] refact(shells): add `and_` method to assemble expressions involving AND Signed-off-by: Pablo Santiago Blum de Aguiar --- tests/rules/__init__.py | 0 tests/rules/conftest.py | 6 ++++++ thefuck/rules/apt_get.py | 5 ++++- thefuck/rules/cd_mkdir.py | 4 +++- thefuck/rules/git_add.py | 4 +++- thefuck/rules/git_checkout.py | 4 +++- thefuck/rules/git_stash.py | 6 +++++- thefuck/rules/no_such_file.py | 4 +++- thefuck/rules/pacman.py | 4 +++- thefuck/shells.py | 7 +++++++ 10 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 tests/rules/__init__.py create mode 100644 tests/rules/conftest.py diff --git a/tests/rules/__init__.py b/tests/rules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/rules/conftest.py b/tests/rules/conftest.py new file mode 100644 index 00000000..94152a6c --- /dev/null +++ b/tests/rules/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture(autouse=True) +def generic_shell(monkeypatch): + monkeypatch.setattr('thefuck.shells.and_', lambda *x: ' && '.join(x)) diff --git a/thefuck/rules/apt_get.py b/thefuck/rules/apt_get.py index 4d5eca6b..b78bb743 100644 --- a/thefuck/rules/apt_get.py +++ b/thefuck/rules/apt_get.py @@ -1,3 +1,5 @@ +from thefuck import shells + try: import CommandNotFound except ImportError: @@ -20,4 +22,5 @@ def get_new_command(command, settings): c = CommandNotFound.CommandNotFound() pkgs = c.getPackages(command.script.split(" ")[0]) name, _ = pkgs[0] - return "sudo apt-get install {} && {}".format(name, command.script) + formatme = shells.and_('sudo apt-get install {}', '{}') + return formatme.format(name, command.script) diff --git a/thefuck/rules/cd_mkdir.py b/thefuck/rules/cd_mkdir.py index 7aa1d9da..168a2ce0 100644 --- a/thefuck/rules/cd_mkdir.py +++ b/thefuck/rules/cd_mkdir.py @@ -1,4 +1,5 @@ import re +from thefuck import shells from thefuck.utils import sudo_support @@ -11,4 +12,5 @@ def match(command, settings): @sudo_support def get_new_command(command, settings): - return re.sub(r'^cd (.*)', 'mkdir -p \\1 && cd \\1', command.script) + repl = shells.and_('mkdir -p \\1', 'cd \\1') + return re.sub(r'^cd (.*)', repl, command.script) diff --git a/thefuck/rules/git_add.py b/thefuck/rules/git_add.py index 66c7f1dc..bc05d011 100644 --- a/thefuck/rules/git_add.py +++ b/thefuck/rules/git_add.py @@ -1,4 +1,5 @@ import re +from thefuck import shells def match(command, settings): @@ -12,4 +13,5 @@ def get_new_command(command, settings): r"error: pathspec '([^']*)' " "did not match any file\(s\) known to git.", command.stderr)[0] - return 'git add -- {} && {}'.format(missing_file, command.script) + formatme = shells.and_('git add -- {}', '{}') + return formatme.format(missing_file, command.script) diff --git a/thefuck/rules/git_checkout.py b/thefuck/rules/git_checkout.py index 271562b8..6c9d259f 100644 --- a/thefuck/rules/git_checkout.py +++ b/thefuck/rules/git_checkout.py @@ -1,4 +1,5 @@ import re +from thefuck import shells def match(command, settings): @@ -12,4 +13,5 @@ def get_new_command(command, settings): r"error: pathspec '([^']*)' " "did not match any file\(s\) known to git.", command.stderr)[0] - return 'git branch {} && {}'.format(missing_file, command.script) + formatme = shells.and_('git branch {}', '{}') + return formatme.format(missing_file, command.script) diff --git a/thefuck/rules/git_stash.py b/thefuck/rules/git_stash.py index 58bfbb38..9e9034a3 100644 --- a/thefuck/rules/git_stash.py +++ b/thefuck/rules/git_stash.py @@ -1,3 +1,6 @@ +from thefuck import shells + + def match(command, settings): # catches "Please commit or stash them" and "Please, commit your changes or # stash them before you can switch branches." @@ -5,4 +8,5 @@ def match(command, settings): def get_new_command(command, settings): - return 'git stash && ' + command.script + formatme = shells.and_('git stash', '{}') + return formatme.format(command.script) diff --git a/thefuck/rules/no_such_file.py b/thefuck/rules/no_such_file.py index 9a0f3b45..44572f19 100644 --- a/thefuck/rules/no_such_file.py +++ b/thefuck/rules/no_such_file.py @@ -1,4 +1,5 @@ import re +from thefuck import shells patterns = ( @@ -25,4 +26,5 @@ def get_new_command(command, settings): file = file[0] dir = file[0:file.rfind('/')] - return 'mkdir -p {} && {}'.format(dir, command.script) + formatme = shells.and_('mkdir -p {}', '{}') + return formatme.format(dir, command.script) diff --git a/thefuck/rules/pacman.py b/thefuck/rules/pacman.py index 71c6e698..2ec507e2 100644 --- a/thefuck/rules/pacman.py +++ b/thefuck/rules/pacman.py @@ -1,4 +1,5 @@ import subprocess +from thefuck import shells from thefuck.utils import DEVNULL @@ -30,7 +31,8 @@ def match(command, settings): def get_new_command(command, settings): package = __get_pkgfile(command)[0] - return '{} -S {} && {}'.format(pacman, package, command.script) + formatme = shells.and_('{} -S {}', '{}') + return formatme.format(pacman, package, command.script) if not __command_available('pkgfile'): diff --git a/thefuck/shells.py b/thefuck/shells.py index 50d9145e..87715d6b 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -47,6 +47,9 @@ class Generic(object): with open(history_file_name, 'a') as history: history.write(self._get_history_line(command_script)) + def and_(self, *commands): + return ' && '.join(commands) + class Bash(Generic): def app_alias(self): @@ -150,3 +153,7 @@ def app_alias(): def put_to_history(command): return _get_shell().put_to_history(command) + + +def and_(*commands): + return _get_shell().and_(*commands) From 179839c32fab0241b2ba9dc4c70d938eabfcea23 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Thu, 14 May 2015 17:34:40 -0300 Subject: [PATCH 24/54] test(rules): test other rules involving `shells.and_()` Signed-off-by: Pablo Santiago Blum de Aguiar --- tests/rules/test_apt_get.py | 59 ++++++++++++++++++++++++++++++++ tests/rules/test_git_add.py | 39 +++++++++++++++++++++ tests/rules/test_git_checkout.py | 37 ++++++++++++++++++++ tests/rules/test_git_stash.py | 39 +++++++++++++++++++++ tests/rules/test_pacman.py | 53 ++++++++++++++++++++++++++++ 5 files changed, 227 insertions(+) create mode 100644 tests/rules/test_apt_get.py create mode 100644 tests/rules/test_git_add.py create mode 100644 tests/rules/test_git_checkout.py create mode 100644 tests/rules/test_git_stash.py create mode 100644 tests/rules/test_pacman.py diff --git a/tests/rules/test_apt_get.py b/tests/rules/test_apt_get.py new file mode 100644 index 00000000..56ad8208 --- /dev/null +++ b/tests/rules/test_apt_get.py @@ -0,0 +1,59 @@ +import pytest +from mock import Mock, patch +from thefuck.rules import apt_get +from thefuck.rules.apt_get import match, get_new_command +from tests.utils import Command + + +# python-commandnotfound is available in ubuntu 14.04+ +@pytest.mark.skipif(not getattr(apt_get, 'enabled_by_default', True), + reason='Skip if python-commandnotfound is not available') +@pytest.mark.parametrize('command', [ + Command(script='vim', stderr='vim: command not found')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command, return_value', [ + (Command(script='vim', stderr='vim: command not found'), + [('vim', 'main'), ('vim-tiny', 'main')])]) +@patch('thefuck.rules.apt_get.CommandNotFound', create=True) +@patch.multiple(apt_get, create=True, apt_get='apt_get') +def test_match_mocked(cmdnf_mock, command, return_value): + get_packages = Mock(return_value=return_value) + cmdnf_mock.CommandNotFound.return_value = Mock(getPackages=get_packages) + assert match(command, None) + assert cmdnf_mock.CommandNotFound.called + assert get_packages.called + + +@pytest.mark.parametrize('command', [ + Command(script='vim', stderr=''), Command()]) +def test_not_match(command): + assert not match(command, None) + + +# python-commandnotfound is available in ubuntu 14.04+ +@pytest.mark.skipif(not getattr(apt_get, 'enabled_by_default', True), + reason='Skip if python-commandnotfound is not available') +@pytest.mark.parametrize('command, new_command', [ + (Command('vim'), 'sudo apt-get install vim && vim'), + (Command('convert'), 'sudo apt-get install imagemagick && convert')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command + + +@pytest.mark.parametrize('command, new_command, return_value', [ + (Command('vim'), 'sudo apt-get install vim && vim', + [('vim', 'main'), ('vim-tiny', 'main')]), + (Command('convert'), 'sudo apt-get install imagemagick && convert', + [('imagemagick', 'main'), + ('graphicsmagick-imagemagick-compat', 'universe')])]) +@patch('thefuck.rules.apt_get.CommandNotFound', create=True) +@patch.multiple(apt_get, create=True, apt_get='apt_get') +def test_get_new_command_mocked(cmdnf_mock, command, new_command, return_value): + get_packages = Mock(return_value=return_value) + cmdnf_mock.CommandNotFound.return_value = Mock(getPackages=get_packages) + assert get_new_command(command, None) == new_command + assert cmdnf_mock.CommandNotFound.called + assert get_packages.called diff --git a/tests/rules/test_git_add.py b/tests/rules/test_git_add.py new file mode 100644 index 00000000..8bad9bb6 --- /dev/null +++ b/tests/rules/test_git_add.py @@ -0,0 +1,39 @@ +import pytest +from thefuck.rules.git_add import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def did_not_match(target, did_you_forget=True): + error = ("error: pathspec '{}' did not match any " + "file(s) known to git.".format(target)) + if did_you_forget: + error = ("{}\nDid you forget to 'git add'?'".format(error)) + return error + + +@pytest.mark.parametrize('command', [ + Command(script='git submodule update unknown', + stderr=did_not_match('unknown')), + Command(script='git commit unknown', + stderr=did_not_match('unknown'))]) # Older versions of Git +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command', [ + Command(script='git submodule update known', stderr=('')), + Command(script='git commit known', stderr=('')), + Command(script='git commit unknown', # Newer versions of Git + stderr=did_not_match('unknown', False))]) +def test_not_match(command): + assert not match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('git submodule update unknown', stderr=did_not_match('unknown')), + 'git add -- unknown && git submodule update unknown'), + (Command('git commit unknown', stderr=did_not_match('unknown')), # Old Git + 'git add -- unknown && git commit unknown')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command diff --git a/tests/rules/test_git_checkout.py b/tests/rules/test_git_checkout.py new file mode 100644 index 00000000..a540b62d --- /dev/null +++ b/tests/rules/test_git_checkout.py @@ -0,0 +1,37 @@ +import pytest +from thefuck.rules.git_checkout import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def did_not_match(target, did_you_forget=False): + error = ("error: pathspec '{}' did not match any " + "file(s) known to git.".format(target)) + if did_you_forget: + error = ("{}\nDid you forget to 'git add'?'".format(error)) + return error + + +@pytest.mark.parametrize('command', [ + Command(script='git checkout unknown', stderr=did_not_match('unknown')), + Command(script='git commit unknown', stderr=did_not_match('unknown'))]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command', [ + Command(script='git submodule update unknown', + stderr=did_not_match('unknown', True)), + Command(script='git checkout known', stderr=('')), + Command(script='git commit known', stderr=(''))]) +def test_not_match(command): + assert not match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command(script='git checkout unknown', stderr=did_not_match('unknown')), + 'git branch unknown && git checkout unknown'), + (Command('git commit unknown', stderr=did_not_match('unknown')), + 'git branch unknown && git commit unknown')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command diff --git a/tests/rules/test_git_stash.py b/tests/rules/test_git_stash.py new file mode 100644 index 00000000..c62a48aa --- /dev/null +++ b/tests/rules/test_git_stash.py @@ -0,0 +1,39 @@ +import pytest +from thefuck.rules.git_stash import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def cherry_pick_error(): + return ('error: Your local changes would be overwritten by cherry-pick.\n' + 'hint: Commit your changes or stash them to proceed.\n' + 'fatal: cherry-pick failed') + + +@pytest.fixture +def rebase_error(): + return ('Cannot rebase: Your index contains uncommitted changes.\n' + 'Please commit or stash them.') + + +@pytest.mark.parametrize('command', [ + Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error()), + Command(script='git rebase -i HEAD~7', stderr=rebase_error())]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command', [ + Command(script='git cherry-pick a1b2c3d', stderr=('')), + Command(script='git rebase -i HEAD~7', stderr=(''))]) +def test_not_match(command): + assert not match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error), + 'git stash && git cherry-pick a1b2c3d'), + (Command('git rebase -i HEAD~7', stderr=rebase_error), + 'git stash && git rebase -i HEAD~7')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command diff --git a/tests/rules/test_pacman.py b/tests/rules/test_pacman.py new file mode 100644 index 00000000..8d719cd6 --- /dev/null +++ b/tests/rules/test_pacman.py @@ -0,0 +1,53 @@ +import pytest +from mock import patch +from thefuck.rules import pacman +from thefuck.rules.pacman import match, get_new_command +from tests.utils import Command + + +pacman_cmd = getattr(pacman, 'pacman', 'pacman') + + +@pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True), + reason='Skip if pacman is not available') +@pytest.mark.parametrize('command', [ + Command(script='vim', stderr='vim: command not found')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command, return_value', [ + (Command(script='vim', stderr='vim: command not found'), 'vim foo bar')]) +@patch('thefuck.rules.pacman.subprocess') +@patch.multiple(pacman, create=True, pacman=pacman_cmd) +def test_match_mocked(subp_mock, command, return_value): + subp_mock.check_output.return_value = return_value + assert match(command, None) + assert subp_mock.check_output.called + + +@pytest.mark.parametrize('command', [ + Command(script='vim', stderr=''), Command()]) +def test_not_match(command): + assert not match(command, None) + + +@pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True), + reason='Skip if pacman is not available') +@pytest.mark.parametrize('command, new_command', [ + (Command('vim'), '{} -S vim && vim'.format(pacman_cmd)), + (Command('convert'), '{} -S imagemagick && convert'.format(pacman_cmd))]) +def test_get_new_command(command, new_command, mocker): + assert get_new_command(command, None) == new_command + + +@pytest.mark.parametrize('command, new_command, return_value', [ + (Command('vim'), '{} -S vim && vim'.format(pacman_cmd), 'vim foo bar'), + (Command('convert'), '{} -S imagemagick && convert'.format(pacman_cmd), + 'imagemagick foo bar')]) +@patch('thefuck.rules.pacman.subprocess') +@patch.multiple(pacman, create=True, pacman=pacman_cmd) +def test_get_new_command_mocked(subp_mock, command, new_command, return_value): + subp_mock.check_output.return_value = return_value + assert get_new_command(command, None) == new_command + assert subp_mock.check_output.called From 9ade21bf0ac8a95252d82b649b7ee186762a749f Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Fri, 15 May 2015 11:54:38 -0300 Subject: [PATCH 25/54] refact(travis): enable verbose mode for tests on travis Signed-off-by: Pablo Santiago Blum de Aguiar --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b124bb3a..c14b697a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,4 @@ python: install: - python setup.py develop - pip install -r requirements.txt -script: py.test +script: py.test -v From f04c4396eb3cb75cf9f3a9e25ece51d2cc9f50c2 Mon Sep 17 00:00:00 2001 From: mcarton Date: Sat, 16 May 2015 18:57:42 +0200 Subject: [PATCH 26/54] Fix Python 2.7 support --- thefuck/rules/whois.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/thefuck/rules/whois.py b/thefuck/rules/whois.py index f019758e..f53cdde8 100644 --- a/thefuck/rules/whois.py +++ b/thefuck/rules/whois.py @@ -1,4 +1,5 @@ -from urllib.parse import urlparse +# -*- encoding: utf-8 -*- +from six.moves.urllib.parse import urlparse def match(command, settings): From 9ef346468c58411f1e2334f60cf05047d54b950a Mon Sep 17 00:00:00 2001 From: mmussomele Date: Sat, 16 May 2015 21:42:21 -0700 Subject: [PATCH 27/54] added cd_correction.py --- thefuck/rules/cd_correction.py | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 thefuck/rules/cd_correction.py diff --git a/thefuck/rules/cd_correction.py b/thefuck/rules/cd_correction.py new file mode 100644 index 00000000..95219933 --- /dev/null +++ b/thefuck/rules/cd_correction.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +__author__ = "mmussomele" + +"""Attempts to spellcheck and correct failed cd commands""" + +import os +import cd_mkdir +from thefuck.utils import sudo_support + +MAX_ALLOWED_STR_DIST = 5 + +def _get_sub_dirs(parent): + """Returns a list of the child directories of the given parent directory""" + return [child for child in os.listdir(parent) if os.path.isdir(os.path.join(parent, child))] + +def _dam_lev_dist(): + """Returns a Damerau-Levenshtein distance calculator.""" + cache = {} + def _calculator(first, second): + """ + Calculates the Damerau-Levenshtein distance of two strings. + See: http://en.wikipedia.org/wiki/Damerau-Levenshtein_distance#Algorithm + """ + if (first, second) in cache: + return cache[(first, second)] + else: + l_first = len(first) + l_second = len(second) + distances = [[0 for _ in range(l_second + 1)] for _ in range(l_first + 1)] + for i in range(l_first + 1): + distances[i][0] = i + for j in range(1, l_second + 1): + distances[0][j] = j + for i in range(l_first): + for j in range(l_second): + if first[i] == second[j]: + cost = 0 + else: + cost = 1 + distances[i+1][j+1] = min(distances[i][j+1] + 1, + distances[i+1][j] + 1, + distances[i][j] + cost) + if i and j and first[i] == second[j-1] and first[i-1] == second[j]: + distances[i][j] = min(distances[i+1][j+1], + distances[i-1][j-1] + cost) + cache[(first, second)] = distances[l_first][l_second] + return distances[l_first][l_second] + return _calculator + +_dam_lev_dist = _dam_lev_dist() + +@sudo_support +def match(command, settings): + """Match function copied from cd_mkdir.py""" + return (command.script.startswith('cd ') + and ('no such file or directory' in command.stderr.lower() + or 'cd: can\'t cd to' in command.stderr.lower())) + +@sudo_support +def get_new_command(command, settings): + """ + Attempt to rebuild the path string by spellchecking the directories. + If it fails (i.e. no directories are a close enough match), then it + defaults to the rules of cd_mkdir. + Change sensitivity to matching by changing MAX_ALLOWED_STR_DIST. + Higher values allow for larger discrepancies in path names. + """ + dest = command.script.split()[1].split(os.sep) + if dest[-1] == '': + dest = dest[:-1] + cwd = os.getcwd() + for directory in dest: + if directory == ".": + continue + elif directory == "..": + cwd = os.path.split(cwd)[0] + continue + best_match = min(_get_sub_dirs(cwd), key=lambda x: _dam_lev_dist(directory, x)) + if _dam_lev_dist(directory, best_match) > MAX_ALLOWED_STR_DIST: + return cd_mkdir.get_new_command(command, settings) + else: + cwd = os.path.join(cwd, best_match) + return "cd {0}".format(cwd) + +enabled_by_default = True \ No newline at end of file From a54c97f624548161123e954ad3e9a01cb84b92a6 Mon Sep 17 00:00:00 2001 From: mmussomele Date: Sat, 16 May 2015 21:47:15 -0700 Subject: [PATCH 28/54] added newline to end of cd_correction.py --- thefuck/rules/cd_correction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/rules/cd_correction.py b/thefuck/rules/cd_correction.py index 95219933..96bee90c 100644 --- a/thefuck/rules/cd_correction.py +++ b/thefuck/rules/cd_correction.py @@ -82,4 +82,4 @@ def get_new_command(command, settings): cwd = os.path.join(cwd, best_match) return "cd {0}".format(cwd) -enabled_by_default = True \ No newline at end of file +enabled_by_default = True From 252859e63a1f41d79899d289da763b973df6a395 Mon Sep 17 00:00:00 2001 From: mmussomele Date: Sat, 16 May 2015 23:53:08 -0700 Subject: [PATCH 29/54] fixed accidentally correcting to some directories with short name length --- thefuck/rules/cd_correction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/thefuck/rules/cd_correction.py b/thefuck/rules/cd_correction.py index 96bee90c..98a2655a 100644 --- a/thefuck/rules/cd_correction.py +++ b/thefuck/rules/cd_correction.py @@ -76,7 +76,8 @@ def get_new_command(command, settings): cwd = os.path.split(cwd)[0] continue best_match = min(_get_sub_dirs(cwd), key=lambda x: _dam_lev_dist(directory, x)) - if _dam_lev_dist(directory, best_match) > MAX_ALLOWED_STR_DIST: + best_dist = _dam_lev_dist(directory, best_match) + if best_dist > MAX_ALLOWED_STR_DIST or best_dist >= len(best_match): return cd_mkdir.get_new_command(command, settings) else: cwd = os.path.join(cwd, best_match) From 8d256390a1215540bcba0186796e15265168a62d Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Fri, 8 May 2015 20:09:50 -0300 Subject: [PATCH 30/54] refact(shells): use os.path.basename to get the name of the shell Signed-off-by: Pablo Santiago Blum de Aguiar --- thefuck/shells.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index 87715d6b..eeb20dca 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -136,7 +136,7 @@ def _get_shell(): shell = Process(os.getpid()).parent().cmdline()[0] except TypeError: shell = Process(os.getpid()).parent.cmdline[0] - return shells[shell] + return shells[os.path.basename(shell)] def from_shell(command): From 1b5c935f30371d2cf147de83c327b8a7495b2c10 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Fri, 8 May 2015 20:09:50 -0300 Subject: [PATCH 31/54] feat(shells): add specific actions for the Fish shell Signed-off-by: Pablo Santiago Blum de Aguiar --- tests/test_shells.py | 22 ++++++++++++++++++++++ thefuck/shells.py | 26 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/tests/test_shells.py b/tests/test_shells.py index 449496c9..f5490777 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -50,6 +50,28 @@ class TestBash(object): write.assert_called_once_with('ls\n') +@pytest.mark.usefixtures('isfile') +class TestFish(object): + @pytest.mark.parametrize('before, after', [ + ('pwd', 'pwd'), + ('ll', 'll')]) # Fish has no aliases but functions + def test_from_shell(self, before, after): + assert shells.Fish().from_shell(before) == after + + def test_to_shell(self): + assert shells.Fish().to_shell('pwd') == 'pwd' + + def test_put_to_history(self, builtins_open, mocker): + mocker.patch('thefuck.shells.time', + return_value=1430707243.3517463) + shells.Fish().put_to_history('ls') + builtins_open.return_value.__enter__.return_value. \ + write.assert_called_once_with('- cmd: ls\n when: 1430707243\n') + + def test_and_(self): + assert shells.Fish().and_('foo', 'bar') == 'foo; and bar' + + @pytest.mark.usefixtures('isfile') class TestZsh(object): @pytest.fixture(autouse=True) diff --git a/thefuck/shells.py b/thefuck/shells.py index eeb20dca..5443c652 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -76,6 +76,31 @@ class Bash(Generic): return u'{}\n'.format(command_script) +class Fish(Generic): + def app_alias(self): + return ("function fuck -d 'Correct your previous console command'\n" + " set -l exit_code $status\n" + " set -l eval_script" + " (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n" + " set -l fucked_up_commandd $history[1]\n" + " thefuck $fucked_up_commandd > $eval_script\n" + " . $eval_script\n" + " rm $eval_script\n" + " if test $exit_code -ne 0\n" + " history --delete $fucked_up_commandd\n" + " end\n" + "end") + + def _get_history_file_name(self): + return os.path.expanduser('~/.config/fish/fish_history') + + def _get_history_line(self, command_script): + return u'- cmd: {}\n when: {}\n'.format(command_script, int(time())) + + def and_(self, *commands): + return '; and '.join(commands) + + class Zsh(Generic): def app_alias(self): return "\nalias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'\n" @@ -126,6 +151,7 @@ class Tcsh(Generic): shells = defaultdict(lambda: Generic(), { 'bash': Bash(), + 'fish': Fish(), 'zsh': Zsh(), '-csh': Tcsh(), 'tcsh': Tcsh()}) From 8fdcff776a37579c55281c2107a178ea77bf4317 Mon Sep 17 00:00:00 2001 From: mmussomele Date: Sun, 17 May 2015 09:03:19 -0700 Subject: [PATCH 32/54] reimplemented using native string matching --- thefuck/rules/cd_correction.py | 51 +++++----------------------------- 1 file changed, 7 insertions(+), 44 deletions(-) diff --git a/thefuck/rules/cd_correction.py b/thefuck/rules/cd_correction.py index 98a2655a..770eff52 100644 --- a/thefuck/rules/cd_correction.py +++ b/thefuck/rules/cd_correction.py @@ -5,50 +5,15 @@ __author__ = "mmussomele" import os import cd_mkdir +from difflib import get_close_matches from thefuck.utils import sudo_support -MAX_ALLOWED_STR_DIST = 5 +MAX_ALLOWED_DIFF = 0.6 def _get_sub_dirs(parent): """Returns a list of the child directories of the given parent directory""" return [child for child in os.listdir(parent) if os.path.isdir(os.path.join(parent, child))] -def _dam_lev_dist(): - """Returns a Damerau-Levenshtein distance calculator.""" - cache = {} - def _calculator(first, second): - """ - Calculates the Damerau-Levenshtein distance of two strings. - See: http://en.wikipedia.org/wiki/Damerau-Levenshtein_distance#Algorithm - """ - if (first, second) in cache: - return cache[(first, second)] - else: - l_first = len(first) - l_second = len(second) - distances = [[0 for _ in range(l_second + 1)] for _ in range(l_first + 1)] - for i in range(l_first + 1): - distances[i][0] = i - for j in range(1, l_second + 1): - distances[0][j] = j - for i in range(l_first): - for j in range(l_second): - if first[i] == second[j]: - cost = 0 - else: - cost = 1 - distances[i+1][j+1] = min(distances[i][j+1] + 1, - distances[i+1][j] + 1, - distances[i][j] + cost) - if i and j and first[i] == second[j-1] and first[i-1] == second[j]: - distances[i][j] = min(distances[i+1][j+1], - distances[i-1][j-1] + cost) - cache[(first, second)] = distances[l_first][l_second] - return distances[l_first][l_second] - return _calculator - -_dam_lev_dist = _dam_lev_dist() - @sudo_support def match(command, settings): """Match function copied from cd_mkdir.py""" @@ -62,8 +27,7 @@ def get_new_command(command, settings): Attempt to rebuild the path string by spellchecking the directories. If it fails (i.e. no directories are a close enough match), then it defaults to the rules of cd_mkdir. - Change sensitivity to matching by changing MAX_ALLOWED_STR_DIST. - Higher values allow for larger discrepancies in path names. + Change sensitivity by changing MAX_ALLOWED_DIFF. Default value is 0.6 """ dest = command.script.split()[1].split(os.sep) if dest[-1] == '': @@ -75,12 +39,11 @@ def get_new_command(command, settings): elif directory == "..": cwd = os.path.split(cwd)[0] continue - best_match = min(_get_sub_dirs(cwd), key=lambda x: _dam_lev_dist(directory, x)) - best_dist = _dam_lev_dist(directory, best_match) - if best_dist > MAX_ALLOWED_STR_DIST or best_dist >= len(best_match): - return cd_mkdir.get_new_command(command, settings) + best_matches = get_close_matches(directory, _get_sub_dirs(cwd), cutoff=MAX_ALLOWED_DIFF) + if len(best_matches): + cwd = os.path.join(cwd, best_matches[0]) else: - cwd = os.path.join(cwd, best_match) + return cd_mkdir.get_new_command(command, settings) return "cd {0}".format(cwd) enabled_by_default = True From 3c673e097238fae27ba5f4786485d58f4c9c5af5 Mon Sep 17 00:00:00 2001 From: mmussomele Date: Sun, 17 May 2015 09:52:42 -0700 Subject: [PATCH 33/54] fixed extra check --- thefuck/rules/cd_correction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/rules/cd_correction.py b/thefuck/rules/cd_correction.py index 770eff52..442ccf24 100644 --- a/thefuck/rules/cd_correction.py +++ b/thefuck/rules/cd_correction.py @@ -40,7 +40,7 @@ def get_new_command(command, settings): cwd = os.path.split(cwd)[0] continue best_matches = get_close_matches(directory, _get_sub_dirs(cwd), cutoff=MAX_ALLOWED_DIFF) - if len(best_matches): + if best_matches: cwd = os.path.join(cwd, best_matches[0]) else: return cd_mkdir.get_new_command(command, settings) From afcee5844bee3b13c5d1c70bd9d5c7df1081fcc9 Mon Sep 17 00:00:00 2001 From: mcarton Date: Sun, 17 May 2015 20:35:06 +0200 Subject: [PATCH 34/54] Fix pacman tests on Arch Linux --- tests/rules/test_pacman.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/rules/test_pacman.py b/tests/rules/test_pacman.py index 8d719cd6..b9742e7f 100644 --- a/tests/rules/test_pacman.py +++ b/tests/rules/test_pacman.py @@ -7,6 +7,18 @@ from tests.utils import Command pacman_cmd = getattr(pacman, 'pacman', 'pacman') +PKGFILE_OUTPUT_CONVERT = ''' +extra/imagemagick 6.9.1.0-1\t/usr/bin/convert +''' + +PKGFILE_OUTPUT_VIM = ''' +extra/gvim 7.4.712-1 \t/usr/bin/vim +extra/gvim-python3 7.4.712-1\t/usr/bin/vim +extra/vim 7.4.712-1 \t/usr/bin/vim +extra/vim-minimal 7.4.712-1 \t/usr/bin/vim +extra/vim-python3 7.4.712-1 \t/usr/bin/vim +''' + @pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True), reason='Skip if pacman is not available') @@ -17,7 +29,7 @@ def test_match(command): @pytest.mark.parametrize('command, return_value', [ - (Command(script='vim', stderr='vim: command not found'), 'vim foo bar')]) + (Command(script='vim', stderr='vim: command not found'), PKGFILE_OUTPUT_VIM)]) @patch('thefuck.rules.pacman.subprocess') @patch.multiple(pacman, create=True, pacman=pacman_cmd) def test_match_mocked(subp_mock, command, return_value): @@ -35,16 +47,17 @@ def test_not_match(command): @pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True), reason='Skip if pacman is not available') @pytest.mark.parametrize('command, new_command', [ - (Command('vim'), '{} -S vim && vim'.format(pacman_cmd)), - (Command('convert'), '{} -S imagemagick && convert'.format(pacman_cmd))]) + (Command('vim'), '{} -S extra/gvim && vim'.format(pacman_cmd)), + (Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd))]) def test_get_new_command(command, new_command, mocker): assert get_new_command(command, None) == new_command @pytest.mark.parametrize('command, new_command, return_value', [ - (Command('vim'), '{} -S vim && vim'.format(pacman_cmd), 'vim foo bar'), - (Command('convert'), '{} -S imagemagick && convert'.format(pacman_cmd), - 'imagemagick foo bar')]) + (Command('vim'), '{} -S extra/gvim && vim'.format(pacman_cmd), + PKGFILE_OUTPUT_VIM), + (Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd), + PKGFILE_OUTPUT_CONVERT)]) @patch('thefuck.rules.pacman.subprocess') @patch.multiple(pacman, create=True, pacman=pacman_cmd) def test_get_new_command_mocked(subp_mock, command, new_command, return_value): From 6590da623fb85c262fb8efe30f4ddbfed1ead376 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 19 May 2015 15:46:23 +0300 Subject: [PATCH 35/54] #205 Fix import in `cd_correction` --- thefuck/rules/cd_correction.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/thefuck/rules/cd_correction.py b/thefuck/rules/cd_correction.py index 442ccf24..bdd7c34b 100644 --- a/thefuck/rules/cd_correction.py +++ b/thefuck/rules/cd_correction.py @@ -4,22 +4,25 @@ __author__ = "mmussomele" """Attempts to spellcheck and correct failed cd commands""" import os -import cd_mkdir from difflib import get_close_matches from thefuck.utils import sudo_support +from thefuck.rules import cd_mkdir MAX_ALLOWED_DIFF = 0.6 + def _get_sub_dirs(parent): """Returns a list of the child directories of the given parent directory""" return [child for child in os.listdir(parent) if os.path.isdir(os.path.join(parent, child))] + @sudo_support def match(command, settings): """Match function copied from cd_mkdir.py""" return (command.script.startswith('cd ') - and ('no such file or directory' in command.stderr.lower() - or 'cd: can\'t cd to' in command.stderr.lower())) + and ('no such file or directory' in command.stderr.lower() + or 'cd: can\'t cd to' in command.stderr.lower())) + @sudo_support def get_new_command(command, settings): @@ -44,6 +47,7 @@ def get_new_command(command, settings): cwd = os.path.join(cwd, best_matches[0]) else: return cd_mkdir.get_new_command(command, settings) - return "cd {0}".format(cwd) + return "cd {0}".format(cwd) + enabled_by_default = True From 051f5fcb8900c5e04dfd2c4876879a3628cdf933 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 19 May 2015 15:48:17 +0300 Subject: [PATCH 36/54] Bump to 1.41 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 147d9422..ec86c8eb 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.40' +VERSION = '1.41' setup(name='thefuck', From ce6855fd97bda2fd456cd2842f50d44cb111995e Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 20 May 2015 02:40:36 +0300 Subject: [PATCH 37/54] Add `git_pull` rule --- README.md | 1 + tests/rules/test_git_pull.py | 29 +++++++++++++++++++++++++++++ thefuck/rules/git_pull.py | 12 ++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 tests/rules/test_git_pull.py create mode 100644 thefuck/rules/git_pull.py diff --git a/README.md b/README.md index 1ac70f45..f2406b19 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `git_checkout` – creates the branch before checking-out; * `git_no_command` – fixes wrong git commands like `git brnch`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; +* `git_pull` – sets upstream before executing previous `git pull`; * `git_stash` – stashes you local modifications before rebasing or switching branch; * `has_exists_script` – prepends `./` when script/binary exists; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; diff --git a/tests/rules/test_git_pull.py b/tests/rules/test_git_pull.py new file mode 100644 index 00000000..87725f5a --- /dev/null +++ b/tests/rules/test_git_pull.py @@ -0,0 +1,29 @@ +import pytest +from thefuck.rules.git_pull import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(): + return '''There is no tracking information for the current branch. +Please specify which branch you want to merge with. +See git-pull(1) for details + + git pull + +If you wish to set tracking information for this branch you can do so with: + + git branch --set-upstream-to=/ master + +''' + + +def test_match(stderr): + assert match(Command('git pull', stderr=stderr), None) + assert not match(Command('git pull'), None) + assert not match(Command('ls', stderr=stderr), None) + + +def test_get_new_command(stderr): + assert get_new_command(Command('git pull', stderr=stderr), None) \ + == "git branch --set-upstream-to=origin/master master && git pull" diff --git a/thefuck/rules/git_pull.py b/thefuck/rules/git_pull.py new file mode 100644 index 00000000..cee34616 --- /dev/null +++ b/thefuck/rules/git_pull.py @@ -0,0 +1,12 @@ +def match(command, settings): + return ('git' in command.script + and 'pull' in command.script + and 'set-upstream' in command.stderr) + + +def get_new_command(command, settings): + line = command.stderr.split('\n')[-3].strip() + branch = line.split(' ')[-1] + set_upstream = line.replace('', 'origin')\ + .replace('', branch) + return u'{} && {}'.format(set_upstream, command.script) From b63ce26853c1f6e1c62bfe7be5778a453a89836a Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 20 May 2015 02:50:08 +0300 Subject: [PATCH 38/54] Reorganize list of rules in readme --- README.md | 25 +++++++++++++++---------- thefuck/rules/grep_recursive.py | 2 +- thefuck/rules/ls_lah.py | 3 --- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f2406b19..a1a176ce 100644 --- a/README.md +++ b/README.md @@ -145,41 +145,46 @@ sudo 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: -* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; -* `cpp11` – add missing `-std=c++11` to `g++` or `clang++`; -* `cd_parent` – changes `cd..` to `cd ..`; +* `cd_correction` – spellchecks and correct failed cd commands; * `cd_mkdir` – creates directories before cd'ing into them; +* `cd_parent` – changes `cd..` to `cd ..`; +* `composer_not_command` – fixes composer command name; * `cp_omitting_directory` – adds `-a` when you `cp` directory; +* `cpp11` – add missing `-std=c++11` to `g++` or `clang++`; * `dry` – fix repetitions like "git git push"; * `fix_alt_space` – replaces Alt+Space with Space character; * `git_add` – fix *"Did you forget to 'git add'?"*; * `git_checkout` – creates the branch before checking-out; * `git_no_command` – fixes wrong git commands like `git brnch`; -* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `git_pull` – sets upstream before executing previous `git pull`; +* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `git_stash` – stashes you local modifications before rebasing or switching branch; +* `grep_recursive` – adds `-r` when you trying to grep directory; * `has_exists_script` – prepends `./` when script/binary exists; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; +* `ls_lah` – adds -lah to ls; +* `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_such_file` – creates missing directories with `mv` and `cp` commands; -* `man_no_space` – fixes man commands without spaces, for example `mandiff`; -* `pacman` – installs app with `pacman` or `yaourt` if it is not installed; * `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; -* `sl_ls` – changes `sl` to `ls`; * `rm_dir` – adds `-rf` when you trying to remove directory; +* `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; * `switch_layout` – switches command from your local layout to en; -* `whois` – fixes `whois` command; +* `whois` – fixes `whois` command. + +Enabled by default only on specific platforms: + * `apt_get` – installs app from apt if it not installed; * `brew_install` – fixes formula name for `brew install`; -* `composer_not_command` – fixes composer command name. +* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; +* `pacman` – installs app with `pacman` or `yaourt` if it is not installed. Bundled, but not enabled by default: -* `ls_lah` – adds -lah to ls; * `rm_root` – adds `--no-preserve-root` to `rm -rf /` command. ## Creating your own rules diff --git a/thefuck/rules/grep_recursive.py b/thefuck/rules/grep_recursive.py index ed0b6fdb..f2876fe1 100644 --- a/thefuck/rules/grep_recursive.py +++ b/thefuck/rules/grep_recursive.py @@ -1,5 +1,5 @@ def match(command, settings): - return (command.script.startswith('grep') + return (command.script.startswith('grep') and 'is a directory' in command.stderr.lower()) diff --git a/thefuck/rules/ls_lah.py b/thefuck/rules/ls_lah.py index 50fe9f5e..7eba5bda 100644 --- a/thefuck/rules/ls_lah.py +++ b/thefuck/rules/ls_lah.py @@ -1,6 +1,3 @@ -enabled_by_default = False - - def match(command, settings): return 'ls' in command.script and not ('ls -' in command.script) From 02d961361874a272da9370af82c5526282689c84 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 20 May 2015 02:50:43 +0300 Subject: [PATCH 39/54] Bump to 1.42 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ec86c8eb..6b742f6d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.41' +VERSION = '1.42' setup(name='thefuck', From 17b9104939b9d6899562b45975dc6afd356b0d6a Mon Sep 17 00:00:00 2001 From: Tevin Zhang Date: Mon, 18 May 2015 18:44:55 +0800 Subject: [PATCH 40/54] better way to get shell --- thefuck/shells.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index 5443c652..e372eb3e 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -153,16 +153,16 @@ shells = defaultdict(lambda: Generic(), { 'bash': Bash(), 'fish': Fish(), 'zsh': Zsh(), - '-csh': Tcsh(), + 'csh': Tcsh(), 'tcsh': Tcsh()}) def _get_shell(): try: - shell = Process(os.getpid()).parent().cmdline()[0] + shell = Process(os.getpid()).parent().name() except TypeError: - shell = Process(os.getpid()).parent.cmdline[0] - return shells[os.path.basename(shell)] + shell = Process(os.getpid()).parent.name() + return shells[shell] def from_shell(command): From d6ce5e1e62f9e18e28cd35be57ab9e0fbab09ad9 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 20 May 2015 16:41:11 +0300 Subject: [PATCH 41/54] #208 `.name` isn't callable in specific psutil --- thefuck/shells.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index e372eb3e..432a3d6a 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -161,7 +161,7 @@ def _get_shell(): try: shell = Process(os.getpid()).parent().name() except TypeError: - shell = Process(os.getpid()).parent.name() + shell = Process(os.getpid()).parent.name return shells[shell] From 53198713265b8b8039e6c572286ea5913c67b37c Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 20 May 2015 16:56:42 +0300 Subject: [PATCH 42/54] #209 add `get_aliases` to shells --- tests/test_shells.py | 93 +++++++++++++++++++++++++++++++------------- thefuck/shells.py | 16 +++++--- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/tests/test_shells.py b/tests/test_shells.py index f5490777..c08ee67f 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -13,19 +13,33 @@ def isfile(mocker): class TestGeneric(object): - def test_from_shell(self): - assert shells.Generic().from_shell('pwd') == 'pwd' + @pytest.fixture + def shell(self): + return shells.Generic() - def test_to_shell(self): - assert shells.Generic().to_shell('pwd') == 'pwd' + def test_from_shell(self, shell): + assert shell.from_shell('pwd') == 'pwd' - def test_put_to_history(self, builtins_open): - assert shells.Generic().put_to_history('ls') is None + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' + + def test_put_to_history(self, builtins_open, shell): + assert shell.put_to_history('ls') is None assert builtins_open.call_count == 0 + def test_and_(self, shell): + assert shell.and_('ls', 'cd') == 'ls && cd' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == {} + @pytest.mark.usefixtures('isfile') class TestBash(object): + @pytest.fixture + def shell(self): + return shells.Bash() + @pytest.fixture(autouse=True) def Popen(self, mocker): mock = mocker.patch('thefuck.shells.Popen') @@ -38,42 +52,61 @@ class TestBash(object): @pytest.mark.parametrize('before, after', [ ('pwd', 'pwd'), ('ll', 'ls -alF')]) - def test_from_shell(self, before, after): - assert shells.Bash().from_shell(before) == after + def test_from_shell(self, before, after, shell): + assert shell.from_shell(before) == after - def test_to_shell(self): - assert shells.Bash().to_shell('pwd') == 'pwd' + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' - def test_put_to_history(self, builtins_open): - shells.Bash().put_to_history('ls') + def test_put_to_history(self, builtins_open, shell): + shell.put_to_history('ls') builtins_open.return_value.__enter__.return_value. \ write.assert_called_once_with('ls\n') + def test_and_(self, shell): + assert shell.and_('ls', 'cd') == 'ls && cd' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == {'l': 'ls -CF', + 'la': 'ls -A', + 'll': 'ls -alF'} + @pytest.mark.usefixtures('isfile') class TestFish(object): + @pytest.fixture + def shell(self): + return shells.Fish() + @pytest.mark.parametrize('before, after', [ ('pwd', 'pwd'), ('ll', 'll')]) # Fish has no aliases but functions - def test_from_shell(self, before, after): - assert shells.Fish().from_shell(before) == after + def test_from_shell(self, before, after, shell): + assert shell.from_shell(before) == after - def test_to_shell(self): - assert shells.Fish().to_shell('pwd') == 'pwd' + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' - def test_put_to_history(self, builtins_open, mocker): + def test_put_to_history(self, builtins_open, mocker, shell): mocker.patch('thefuck.shells.time', return_value=1430707243.3517463) - shells.Fish().put_to_history('ls') + shell.put_to_history('ls') builtins_open.return_value.__enter__.return_value. \ write.assert_called_once_with('- cmd: ls\n when: 1430707243\n') - def test_and_(self): - assert shells.Fish().and_('foo', 'bar') == 'foo; and bar' + def test_and_(self, shell): + assert shell.and_('foo', 'bar') == 'foo; and bar' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == {} @pytest.mark.usefixtures('isfile') class TestZsh(object): + @pytest.fixture + def shell(self): + return shells.Zsh() + @pytest.fixture(autouse=True) def Popen(self, mocker): mock = mocker.patch('thefuck.shells.Popen') @@ -86,15 +119,23 @@ class TestZsh(object): @pytest.mark.parametrize('before, after', [ ('pwd', 'pwd'), ('ll', 'ls -alF')]) - def test_from_shell(self, before, after): - assert shells.Zsh().from_shell(before) == after + def test_from_shell(self, before, after, shell): + assert shell.from_shell(before) == after - def test_to_shell(self): - assert shells.Zsh().to_shell('pwd') == 'pwd' + def test_to_shell(self, shell): + assert shell.to_shell('pwd') == 'pwd' - def test_put_to_history(self, builtins_open, mocker): + def test_put_to_history(self, builtins_open, mocker, shell): mocker.patch('thefuck.shells.time', return_value=1430707243.3517463) - shells.Zsh().put_to_history('ls') + shell.put_to_history('ls') builtins_open.return_value.__enter__.return_value. \ write.assert_called_once_with(': 1430707243:0;ls\n') + + def test_and_(self, shell): + assert shell.and_('ls', 'cd') == 'ls && cd' + + def test_get_aliases(self, shell): + assert shell.get_aliases() == {'l': 'ls -CF', + 'la': 'ls -A', + 'll': 'ls -alF'} diff --git a/thefuck/shells.py b/thefuck/shells.py index 432a3d6a..ee49ea0d 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -1,5 +1,5 @@ """Module with shell specific actions, each shell class should -implement `from_shell`, `to_shell`, `app_alias` and `put_to_history` +implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and `get_aliases` methods. """ @@ -12,11 +12,11 @@ from .utils import DEVNULL class Generic(object): - def _get_aliases(self): + def get_aliases(self): return {} def _expand_aliases(self, command_script): - aliases = self._get_aliases() + aliases = self.get_aliases() binary = command_script.split(' ')[0] if binary in aliases: return command_script.replace(binary, aliases[binary], 1) @@ -61,7 +61,7 @@ class Bash(Generic): value = value[1:-1] return name, value - def _get_aliases(self): + def get_aliases(self): proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) return dict( self._parse_alias(alias) @@ -111,7 +111,7 @@ class Zsh(Generic): value = value[1:-1] return name, value - def _get_aliases(self): + def get_aliases(self): proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) return dict( self._parse_alias(alias) @@ -134,7 +134,7 @@ class Tcsh(Generic): name, value = alias.split("\t", 1) return name, value - def _get_aliases(self): + def get_aliases(self): proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) return dict( self._parse_alias(alias) @@ -183,3 +183,7 @@ def put_to_history(command): def and_(*commands): return _get_shell().and_(*commands) + + +def get_aliases(): + return _get_shell().get_aliases().keys() From 2c3df1ad47f56eea63c931d6df5abb44e5a7b837 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 20 May 2015 16:58:05 +0300 Subject: [PATCH 43/54] #209 add support of aliases to `no_command` --- tests/rules/test_no_command.py | 4 ++-- thefuck/rules/no_command.py | 9 +++++---- thefuck/shells.py | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/rules/test_no_command.py b/tests/rules/test_no_command.py index 64a2423a..68ccf3f7 100644 --- a/tests/rules/test_no_command.py +++ b/tests/rules/test_no_command.py @@ -3,7 +3,7 @@ from thefuck.rules.no_command import match, get_new_command def test_match(): - with patch('thefuck.rules.no_command._get_all_bins', + with patch('thefuck.rules.no_command._get_all_callables', return_value=['vim', 'apt-get']): assert match(Mock(stderr='vom: not found', script='vom file.py'), None) assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None) @@ -11,7 +11,7 @@ def test_match(): def test_get_new_command(): - with patch('thefuck.rules.no_command._get_all_bins', + with patch('thefuck.rules.no_command._get_all_callables', return_value=['vim', 'apt-get']): assert get_new_command( Mock(stderr='vom: not found', diff --git a/thefuck/rules/no_command.py b/thefuck/rules/no_command.py index 1a152c99..3b310495 100644 --- a/thefuck/rules/no_command.py +++ b/thefuck/rules/no_command.py @@ -2,6 +2,7 @@ from difflib import get_close_matches import os from pathlib import Path from thefuck.utils import sudo_support +from thefuck.shells import get_aliases def _safe(fn, fallback): @@ -11,25 +12,25 @@ def _safe(fn, fallback): return fallback -def _get_all_bins(): +def _get_all_callables(): return [exe.name for path in os.environ.get('PATH', '').split(':') for exe in _safe(lambda: list(Path(path).iterdir()), []) - if not _safe(exe.is_dir, True)] + if not _safe(exe.is_dir, True)] + get_aliases() @sudo_support def match(command, settings): return 'not found' in command.stderr and \ bool(get_close_matches(command.script.split(' ')[0], - _get_all_bins())) + _get_all_callables())) @sudo_support def get_new_command(command, settings): old_command = command.script.split(' ')[0] new_command = get_close_matches(old_command, - _get_all_bins())[0] + _get_all_callables())[0] return ' '.join([new_command] + command.script.split(' ')[1:]) diff --git a/thefuck/shells.py b/thefuck/shells.py index ee49ea0d..2749a1c5 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -186,4 +186,4 @@ def and_(*commands): def get_aliases(): - return _get_shell().get_aliases().keys() + return list(_get_shell().get_aliases().keys()) From 1f48d5e12a4e103dfea7ce33b82ef3b25565346d Mon Sep 17 00:00:00 2001 From: mcarton Date: Wed, 20 May 2015 17:55:48 +0200 Subject: [PATCH 44/54] Add a rule to change man section --- README.md | 1 + tests/rules/test_man.py | 26 ++++++++++++++++++++++++++ thefuck/rules/man.py | 13 +++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 tests/rules/test_man.py create mode 100644 thefuck/rules/man.py diff --git a/README.md b/README.md index a1a176ce..7b89f107 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `has_exists_script` – prepends `./` when script/binary exists; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `ls_lah` – adds -lah to ls; +* `man` – change manual section; * `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; diff --git a/tests/rules/test_man.py b/tests/rules/test_man.py new file mode 100644 index 00000000..b2e7f281 --- /dev/null +++ b/tests/rules/test_man.py @@ -0,0 +1,26 @@ +import pytest +from thefuck.rules.man import match, get_new_command +from tests.utils import Command + +@pytest.mark.parametrize('command', [ + Command('man read'), + Command('man 2 read'), + Command('man 3 read'), + Command('man -s2 read'), + Command('man -s3 read'), + Command('man -s 2 read'), + Command('man -s 3 read')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('man read'), 'man 3 read'), + (Command('man 2 read'), 'man 3 read'), + (Command('man 3 read'), 'man 2 read'), + (Command('man -s2 read'), 'man -s3 read'), + (Command('man -s3 read'), 'man -s2 read'), + (Command('man -s 2 read'), 'man -s 3 read'), + (Command('man -s 3 read'), 'man -s 2 read')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command diff --git a/thefuck/rules/man.py b/thefuck/rules/man.py new file mode 100644 index 00000000..13ff0c6a --- /dev/null +++ b/thefuck/rules/man.py @@ -0,0 +1,13 @@ +def match(command, settings): + return command.script.startswith('man') + + +def get_new_command(command, settings): + if '3' in command.script: + return command.script.replace("3", "2") + if '2' in command.script: + return command.script.replace("2", "3") + + split_cmd = command.script.split() + split_cmd.insert(1, ' 3 ') + return "".join(split_cmd) From 44c06c483ef779a56d3de2899264cf1355e456e4 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Wed, 20 May 2015 13:35:31 -0300 Subject: [PATCH 45/54] fix(whois): check if there's at least one argument to `whois` This avoids thefuck failing when there's no arguments. It fails with: ``` ... File "thefuck/rules/whois.py", line 26, in get_new_command url = command.script.split()[1] IndexError: list index out of range ``` Signed-off-by: Pablo Santiago Blum de Aguiar --- tests/rules/test_whois.py | 4 ++++ thefuck/rules/whois.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/rules/test_whois.py b/tests/rules/test_whois.py index b911106e..18548ce5 100644 --- a/tests/rules/test_whois.py +++ b/tests/rules/test_whois.py @@ -11,6 +11,10 @@ def test_match(command): assert match(command, None) +def test_not_match(): + assert not match(Command(script='whois'), None) + + @pytest.mark.parametrize('command, new_command', [ (Command('whois https://en.wikipedia.org/wiki/Main_Page'), 'whois en.wikipedia.org'), (Command('whois https://en.wikipedia.org/'), 'whois en.wikipedia.org'), diff --git a/thefuck/rules/whois.py b/thefuck/rules/whois.py index f53cdde8..d27ecc16 100644 --- a/thefuck/rules/whois.py +++ b/thefuck/rules/whois.py @@ -19,7 +19,7 @@ def match(command, settings): - www.google.fr → subdomain: www, domain: 'google.fr'; - google.co.uk → subdomain: None, domain; 'google.co.uk'. """ - return 'whois' in command.script + return 'whois' in command.script and len(command.script.split()) > 1 def get_new_command(command, settings): From e7d7b80c09baa6dd7ba747f5223f29ea0473d1c9 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 21 May 2015 00:49:56 +0300 Subject: [PATCH 46/54] Add rule for django south ghost migrations --- README.md | 1 + tests/rules/test_django_south_ghost.py | 53 ++++++++++++++++++++++++++ thefuck/rules/django_south_ghost.py | 8 ++++ 3 files changed, 62 insertions(+) create mode 100644 tests/rules/test_django_south_ghost.py create mode 100644 thefuck/rules/django_south_ghost.py diff --git a/README.md b/README.md index 7b89f107..3e1e413f 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `cp_omitting_directory` – adds `-a` when you `cp` directory; * `cpp11` – add missing `-std=c++11` to `g++` or `clang++`; * `dry` – fix repetitions like "git git push"; +* `django_south_ghost` – adds `--delete-ghost-migrations` to failed because ghosts django south migration; * `fix_alt_space` – replaces Alt+Space with Space character; * `git_add` – fix *"Did you forget to 'git add'?"*; * `git_checkout` – creates the branch before checking-out; diff --git a/tests/rules/test_django_south_ghost.py b/tests/rules/test_django_south_ghost.py new file mode 100644 index 00000000..af87eef5 --- /dev/null +++ b/tests/rules/test_django_south_ghost.py @@ -0,0 +1,53 @@ +import pytest +from thefuck.rules.django_south_ghost import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(): + return '''Traceback (most recent call last): + File "/home/nvbn/work/.../bin/python", line 42, in + exec(compile(__file__f.read(), __file__, "exec")) + File "/home/nvbn/work/.../app/manage.py", line 34, in + execute_from_command_line(sys.argv) + File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 443, in execute_from_command_line + utility.execute() + File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 382, in execute + self.fetch_command(subcommand).run_from_argv(self.argv) + File "/home/nvbn/work/.../lib/django/core/management/base.py", line 196, in run_from_argv + self.execute(*args, **options.__dict__) + File "/home/nvbn/work/.../lib/django/core/management/base.py", line 232, in execute + output = self.handle(*args, **options) + File "/home/nvbn/work/.../app/lib/south/management/commands/migrate.py", line 108, in handle + ignore_ghosts = ignore_ghosts, + File "/home/nvbn/work/.../app/lib/south/migration/__init__.py", line 193, in migrate_app + applied_all = check_migration_histories(applied_all, delete_ghosts, ignore_ghosts) + File "/home/nvbn/work/.../app/lib/south/migration/__init__.py", line 88, in check_migration_histories + raise exceptions.GhostMigrations(ghosts) +south.exceptions.GhostMigrations: + + ! These migrations are in the database but not on disk: + + + + + + + + ! I'm not trusting myself; either fix this yourself by fiddling + ! with the south_migrationhistory table, or pass --delete-ghost-migrations + ! to South to have it delete ALL of these records (this may not be good). +''' + + +def test_match(stderr): + assert match(Command('./manage.py migrate', stderr=stderr), None) + assert match(Command('python manage.py migrate', stderr=stderr), None) + assert not match(Command('./manage.py migrate'), None) + assert not match(Command('app migrate', stderr=stderr), None) + assert not match(Command('./manage.py test', stderr=stderr), None) + + +def test_get_new_command(): + assert get_new_command(Command('./manage.py migrate auth'), None)\ + == './manage.py migrate auth --delete-ghost-migrations' diff --git a/thefuck/rules/django_south_ghost.py b/thefuck/rules/django_south_ghost.py new file mode 100644 index 00000000..d3290c4a --- /dev/null +++ b/thefuck/rules/django_south_ghost.py @@ -0,0 +1,8 @@ +def match(command, settings): + return 'manage.py' in command.script and \ + 'migrate' in command.script \ + and 'or pass --delete-ghost-migrations' in command.stderr + + +def get_new_command(command, settings): + return u'{} --delete-ghost-migrations'.format(command.script) From c65fdd0f814689729867c7f0ae4025ee3615a5c5 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 21 May 2015 00:55:23 +0300 Subject: [PATCH 47/54] Add rule for django south inconsistent migrations --- README.md | 1 + tests/rules/test_django_south_ghost.py | 14 ++++----- tests/rules/test_django_south_merge.py | 43 ++++++++++++++++++++++++++ thefuck/rules/django_south_merge.py | 8 +++++ 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 tests/rules/test_django_south_merge.py create mode 100644 thefuck/rules/django_south_merge.py diff --git a/README.md b/README.md index 3e1e413f..4993bae1 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `cpp11` – add missing `-std=c++11` to `g++` or `clang++`; * `dry` – fix repetitions like "git git push"; * `django_south_ghost` – adds `--delete-ghost-migrations` to failed because ghosts django south migration; +* `django_south_merge` – adds `--merge` to inconsistent django south migration; * `fix_alt_space` – replaces Alt+Space with Space character; * `git_add` – fix *"Did you forget to 'git add'?"*; * `git_checkout` – creates the branch before checking-out; diff --git a/tests/rules/test_django_south_ghost.py b/tests/rules/test_django_south_ghost.py index af87eef5..70fc8903 100644 --- a/tests/rules/test_django_south_ghost.py +++ b/tests/rules/test_django_south_ghost.py @@ -27,13 +27,13 @@ def stderr(): south.exceptions.GhostMigrations: ! These migrations are in the database but not on disk: - - - - - - - + + + + + + + ! I'm not trusting myself; either fix this yourself by fiddling ! with the south_migrationhistory table, or pass --delete-ghost-migrations ! to South to have it delete ALL of these records (this may not be good). diff --git a/tests/rules/test_django_south_merge.py b/tests/rules/test_django_south_merge.py new file mode 100644 index 00000000..c0426122 --- /dev/null +++ b/tests/rules/test_django_south_merge.py @@ -0,0 +1,43 @@ +import pytest +from thefuck.rules.django_south_merge import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def stderr(): + return '''Running migrations for app: + ! Migration app:0003_auto... should not have been applied before app:0002_auto__add_field_query_due_date_ but was. +Traceback (most recent call last): + File "/home/nvbn/work/.../bin/python", line 42, in + exec(compile(__file__f.read(), __file__, "exec")) + File "/home/nvbn/work/.../app/manage.py", line 34, in + execute_from_command_line(sys.argv) + File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 443, in execute_from_command_line + utility.execute() + File "/home/nvbn/work/.../lib/django/core/management/__init__.py", line 382, in execute + self.fetch_command(subcommand).run_from_argv(self.argv) + File "/home/nvbn/work/.../lib/django/core/management/base.py", line 196, in run_from_argv + self.execute(*args, **options.__dict__) + File "/home/nvbn/work/.../lib/django/core/management/base.py", line 232, in execute + output = self.handle(*args, **options) + File "/home/nvbn/work/.../app/lib/south/management/commands/migrate.py", line 108, in handle + ignore_ghosts = ignore_ghosts, + File "/home/nvbn/work/.../app/lib/south/migration/__init__.py", line 207, in migrate_app + raise exceptions.InconsistentMigrationHistory(problems) +south.exceptions.InconsistentMigrationHistory: Inconsistent migration history +The following options are available: + --merge: will just attempt the migration ignoring any potential dependency conflicts. +''' + + +def test_match(stderr): + assert match(Command('./manage.py migrate', stderr=stderr), None) + assert match(Command('python manage.py migrate', stderr=stderr), None) + assert not match(Command('./manage.py migrate'), None) + assert not match(Command('app migrate', stderr=stderr), None) + assert not match(Command('./manage.py test', stderr=stderr), None) + + +def test_get_new_command(): + assert get_new_command(Command('./manage.py migrate auth'), None) \ + == './manage.py migrate auth --merge' diff --git a/thefuck/rules/django_south_merge.py b/thefuck/rules/django_south_merge.py new file mode 100644 index 00000000..bef05970 --- /dev/null +++ b/thefuck/rules/django_south_merge.py @@ -0,0 +1,8 @@ +def match(command, settings): + return 'manage.py' in command.script and \ + 'migrate' in command.script \ + and '--merge: will just attempt the migration' in command.stderr + + +def get_new_command(command, settings): + return u'{} --merge'.format(command.script) From d088dac0f47c577f0b7d960044a9ff71e91d3374 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 21 May 2015 01:07:41 +0300 Subject: [PATCH 48/54] Bump to 1.43 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6b742f6d..32f0de69 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.42' +VERSION = '1.43' setup(name='thefuck', From 6cf430cc238275033a41bbe17081dc721db8fc30 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Wed, 20 May 2015 23:53:08 -0300 Subject: [PATCH 49/54] refact(man): do not match if there's no argument to man If there's no argument to man, a call to thefuck should just give no fuck. Signed-off-by: Pablo Santiago Blum de Aguiar --- tests/rules/test_man.py | 8 ++++++++ thefuck/rules/man.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/rules/test_man.py b/tests/rules/test_man.py index b2e7f281..883d7366 100644 --- a/tests/rules/test_man.py +++ b/tests/rules/test_man.py @@ -2,6 +2,7 @@ import pytest from thefuck.rules.man import match, get_new_command from tests.utils import Command + @pytest.mark.parametrize('command', [ Command('man read'), Command('man 2 read'), @@ -14,6 +15,13 @@ def test_match(command): assert match(command, None) +@pytest.mark.parametrize('command', [ + Command('man'), + Command('man ')]) +def test_not_match(command): + assert not match(command, None) + + @pytest.mark.parametrize('command, new_command', [ (Command('man read'), 'man 3 read'), (Command('man 2 read'), 'man 3 read'), diff --git a/thefuck/rules/man.py b/thefuck/rules/man.py index 13ff0c6a..0b15c5fc 100644 --- a/thefuck/rules/man.py +++ b/thefuck/rules/man.py @@ -1,5 +1,5 @@ def match(command, settings): - return command.script.startswith('man') + return command.script.strip().startswith('man ') def get_new_command(command, settings): From 2bebfabf8d5a9c61b0344048c6c5b254fb65ff50 Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Thu, 21 May 2015 23:55:34 -0300 Subject: [PATCH 50/54] refact(shells): cache aliases to speed up subsequent calls Signed-off-by: Pablo Santiago Blum de Aguiar --- thefuck/shells.py | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/thefuck/shells.py b/thefuck/shells.py index 2749a1c5..61217f1b 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -12,8 +12,10 @@ from .utils import DEVNULL class Generic(object): + _aliases = {} + def get_aliases(self): - return {} + return self._aliases def _expand_aliases(self, command_script): aliases = self.get_aliases() @@ -62,11 +64,15 @@ class Bash(Generic): return name, value def get_aliases(self): - proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) - return dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '=' in alias) + if not self._aliases: + proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, + shell=True) + self._aliases = dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '=' in alias) + + return self._aliases def _get_history_file_name(self): return os.environ.get("HISTFILE", @@ -112,11 +118,15 @@ class Zsh(Generic): return name, value def get_aliases(self): - proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) - return dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '=' in alias) + if not self._aliases: + proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, + shell=True) + self._aliases = dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '=' in alias) + + return self._aliases def _get_history_file_name(self): return os.environ.get("HISTFILE", @@ -135,11 +145,15 @@ class Tcsh(Generic): return name, value def get_aliases(self): - proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True) - return dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '\t' in alias) + if not self._aliases: + proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, + shell=True) + self._aliases = dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '\t' in alias) + + return self._aliases def _get_history_file_name(self): return os.environ.get("HISTFILE", From 551e35e3b66bda5793d0a6a579032de33a29c05e Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Thu, 21 May 2015 23:55:49 -0300 Subject: [PATCH 51/54] refact(shells): add support to Fish functions Signed-off-by: Pablo Santiago Blum de Aguiar --- tests/test_shells.py | 10 +++++++++- thefuck/shells.py | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_shells.py b/tests/test_shells.py index c08ee67f..675cdd35 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -78,6 +78,12 @@ class TestFish(object): def shell(self): return shells.Fish() + @pytest.fixture(autouse=True) + def Popen(self, mocker): + mock = mocker.patch('thefuck.shells.Popen') + mock.return_value.stdout.read.return_value = (b'funced\nfuncsave\ngrep') + return mock + @pytest.mark.parametrize('before, after', [ ('pwd', 'pwd'), ('ll', 'll')]) # Fish has no aliases but functions @@ -98,7 +104,9 @@ class TestFish(object): assert shell.and_('foo', 'bar') == 'foo; and bar' def test_get_aliases(self, shell): - assert shell.get_aliases() == {} + assert shell.get_aliases() == {'funced': 'funced', + 'funcsave': 'funcsave', + 'grep': 'grep'} @pytest.mark.usefixtures('isfile') diff --git a/thefuck/shells.py b/thefuck/shells.py index 61217f1b..154311c8 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -97,6 +97,15 @@ class Fish(Generic): " end\n" "end") + def get_aliases(self): + if not self._aliases: + proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL, + shell=True) + functions = proc.stdout.read().decode('utf-8').strip().split('\n') + self._aliases = dict((function, function) for function in functions) + + return self._aliases + def _get_history_file_name(self): return os.path.expanduser('~/.config/fish/fish_history') From 190e47ecdbbfbe4ca56726af26cfee66c060908c Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 22 May 2015 17:07:01 +0300 Subject: [PATCH 52/54] #215 Use memoize decorator for caching --- tests/test_utils.py | 10 ++++++- thefuck/shells.py | 65 +++++++++++++++++++-------------------------- thefuck/utils.py | 17 ++++++++++++ 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 24d6b196..d180eb56 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import pytest from mock import Mock -from thefuck.utils import sudo_support, wrap_settings +from thefuck.utils import sudo_support, wrap_settings, memoize from thefuck.types import Settings from tests.utils import Command @@ -24,3 +24,11 @@ def test_sudo_support(return_value, command, called, result): fn = Mock(return_value=return_value, __name__='') assert sudo_support(fn)(Command(command), None) == result fn.assert_called_once_with(Command(called), None) + + +def test_memoize(): + fn = Mock(__name__='fn') + memoized = memoize(fn) + memoized() + memoized() + fn.assert_called_once_with() diff --git a/thefuck/shells.py b/thefuck/shells.py index 154311c8..6bf4bd7d 100644 --- a/thefuck/shells.py +++ b/thefuck/shells.py @@ -8,14 +8,13 @@ from subprocess import Popen, PIPE from time import time import os from psutil import Process -from .utils import DEVNULL +from .utils import DEVNULL, memoize class Generic(object): - _aliases = {} def get_aliases(self): - return self._aliases + return {} def _expand_aliases(self, command_script): aliases = self.get_aliases() @@ -63,16 +62,14 @@ class Bash(Generic): value = value[1:-1] return name, value + @memoize def get_aliases(self): - if not self._aliases: - proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, - shell=True) - self._aliases = dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '=' in alias) - - return self._aliases + proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, + shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '=' in alias) def _get_history_file_name(self): return os.environ.get("HISTFILE", @@ -97,14 +94,12 @@ class Fish(Generic): " end\n" "end") + @memoize def get_aliases(self): - if not self._aliases: - proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL, - shell=True) - functions = proc.stdout.read().decode('utf-8').strip().split('\n') - self._aliases = dict((function, function) for function in functions) - - return self._aliases + proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL, + shell=True) + functions = proc.stdout.read().decode('utf-8').strip().split('\n') + return {function: function for function in functions} def _get_history_file_name(self): return os.path.expanduser('~/.config/fish/fish_history') @@ -126,16 +121,14 @@ class Zsh(Generic): value = value[1:-1] return name, value + @memoize def get_aliases(self): - if not self._aliases: - proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, - shell=True) - self._aliases = dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '=' in alias) - - return self._aliases + proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, + shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '=' in alias) def _get_history_file_name(self): return os.environ.get("HISTFILE", @@ -153,16 +146,14 @@ class Tcsh(Generic): name, value = alias.split("\t", 1) return name, value + @memoize def get_aliases(self): - if not self._aliases: - proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, - shell=True) - self._aliases = dict( - self._parse_alias(alias) - for alias in proc.stdout.read().decode('utf-8').split('\n') - if alias and '\t' in alias) - - return self._aliases + proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, + shell=True) + return dict( + self._parse_alias(alias) + for alias in proc.stdout.read().decode('utf-8').split('\n') + if alias and '\t' in alias) def _get_history_file_name(self): return os.environ.get("HISTFILE", diff --git a/thefuck/utils.py b/thefuck/utils.py index 3247111d..8e5c9bda 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -1,5 +1,6 @@ from functools import wraps import os +import pickle import six from .types import Command @@ -62,3 +63,19 @@ def sudo_support(fn): else: return result return wrapper + + +def memoize(fn): + """Caches previous calls to the function.""" + memo = {} + + @wraps(fn) + def wrapper(*args, **kwargs): + key = pickle.dumps((args, kwargs)) + if key not in memo: + memo[key] = fn(*args, **kwargs) + + return memo[key] + + return wrapper + From d3146aa0aca57522820f5bdb0a22f5b248de407e Mon Sep 17 00:00:00 2001 From: Cami Diez Date: Sat, 23 May 2015 23:18:15 +0800 Subject: [PATCH 53/54] Addressed Issue #210 --- tests/rules/test_open.py | 25 +++++++++++++++++++++++++ thefuck/rules/open.py | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/rules/test_open.py create mode 100644 thefuck/rules/open.py diff --git a/tests/rules/test_open.py b/tests/rules/test_open.py new file mode 100644 index 00000000..ea350b55 --- /dev/null +++ b/tests/rules/test_open.py @@ -0,0 +1,25 @@ +import pytest +from thefuck.rules.open import match, get_new_command +from tests.utils import Command + + +@pytest.mark.parametrize('command', [ + Command(script='open foo.com'), + Command(script='open foo.ly'), + Command(script='open foo.org'), + Command(script='open foo.net'), + Command(script='open foo.se'), + Command(script='open foo.io')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command, new_command', [ + (Command('open foo.com'), 'open http://foo.com'), + (Command('open foo.ly'), 'open http://foo.ly'), + (Command('open foo.org'), 'open http://foo.org'), + (Command('open foo.net'), 'open http://foo.net'), + (Command('open foo.se'), 'open http://foo.se'), + (Command('open foo.io'), 'open http://foo.io')]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command diff --git a/thefuck/rules/open.py b/thefuck/rules/open.py new file mode 100644 index 00000000..c799b0a4 --- /dev/null +++ b/thefuck/rules/open.py @@ -0,0 +1,24 @@ +# Opens URL's in the default web browser +# +# Example: +# > open github.com +# The file ~/github.com does not exist. +# Perhaps you meant 'http://github.com'? +# +# + +def match(command, settings): + return (command.script.startswith ('open') + and ( + # Wanted to use this: + # 'http' in command.stderr + '.com' in command.script + or '.net' in command.script + or '.org' in command.script + or '.ly' in command.script + or '.io' in command.script + or '.se' in command.script + or '.edu' in command.script)) + +def get_new_command(command, settings): + return 'open http://' + command.script[5:] From 718cadb85a02faa81af7f3a5b175f018fe38599b Mon Sep 17 00:00:00 2001 From: Vladimir Iakovlev Date: Sat, 23 May 2015 18:49:20 +0300 Subject: [PATCH 54/54] #216 add `open` rule to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4993bae1..43389f32 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_such_file` – creates missing directories with `mv` and `cp` commands; +* `open` – prepends `http` to address passed to `open`; * `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `rm_dir` – adds `-rf` when you trying to remove directory;