1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-01 15:42:06 +00:00

Compare commits

...

44 Commits
2.5.4 ... 2.7

Author SHA1 Message Date
nvbn
d20205249b Bump to 2.7 2015-08-11 01:17:06 +03:00
nvbn
b29113c229 #326 Add support of sudo with pipes 2015-08-11 01:15:05 +03:00
nvbn
41a0a766ce Merge branch 'master' of github.com:nvbn/thefuck 2015-08-09 22:56:00 +03:00
nvbn
6222985491 #330 Add support of a single argument 2015-08-09 22:55:48 +03:00
Vladimir Iakovlev
e09e5a9683 Merge pull request #329 from JakobGreen/master
Change failed message 'No fuck given' to the more popular 'No fucks g…
2015-08-08 04:45:42 +03:00
JakobGreen
6883d2dbeb Change failed message 'No fuck given' to the more popular 'No fucks given' 2015-08-07 14:51:51 -06:00
Vladimir Iakovlev
215c64d924 Merge pull request #327 from bugaevc/must-run-as-root
Add one more 'need root' phrase
2015-08-07 18:50:42 +03:00
Sergey Bugaev
ab76f87e01 Add one more 'need root' phrase 2015-08-06 20:33:31 +03:00
nvbn
fd759ea2ac #298 Don't suggest duplicates 2015-08-01 19:16:22 +03:00
nvbn
213e7bf74b #301 Fix UnicodeEncodeError in debug time tracker 2015-08-01 18:56:20 +03:00
Vladimir Iakovlev
1a2c1aa4e9 Merge pull request #325 from mcarton/324
Some adaptations for #324
2015-08-01 00:17:56 +03:00
mcarton
fc48e69921 Adapt the whois rule to #342 2015-07-31 22:19:16 +02:00
mcarton
88732a608e Adapt the tmux rule to #324 2015-07-31 22:19:16 +02:00
mcarton
8374be0872 Adapt the pacman rule to #324 2015-07-31 22:18:59 +02:00
mcarton
3ae01ac65d Adapt the man rule to #324 2015-07-31 21:41:07 +02:00
mcarton
4d467cce95 #324 Remove arrows in case there is only one match 2015-07-31 20:59:49 +02:00
Vladimir Iakovlev
8be353941f Merge pull request #324 from nvbn/298-variants
Add ability to select fixed command from variants
2015-07-31 15:39:57 +03:00
nvbn
d442f959e9 #298 Update readme 2015-07-31 15:36:08 +03:00
nvbn
cb2cddbdd9 #298 Fix zsh tests with BARE 2015-07-31 15:31:51 +03:00
nvbn
8632a29edc #298 Fix tests with BARE 2015-07-31 15:04:06 +03:00
nvbn
36a0a669b0 Bump to 2.6 2015-07-30 20:27:47 +03:00
nvbn
214acf56c5 #298 Wait before checking that history changed 2015-07-30 20:04:40 +03:00
nvbn
da3bc60942 #298 Fix arrow-tests on travis-ci 2015-07-30 18:39:41 +03:00
nvbn
70c89164b0 #298 Add func tests for selecting rule 2015-07-30 18:28:20 +03:00
nvbn
1a76bfd2a3 #298 Always clean-up after building container 2015-07-30 18:17:29 +03:00
Vladimir Iakovlev
b16de9c7c2 Merge pull request #323 from mcarton/fix-file
#320 Add the `fix_file` rule
2015-07-30 18:05:50 +03:00
mcarton
43fead02d3 Test if the file exists in the fix_file rule
This avoid false positives in `match`.
2015-07-30 16:42:00 +02:00
mcarton
de513cacb1 Show user's $EDITOR in output
It looks nicer with confirmation and also checks the user actually has an
$EDITOR.
2015-07-29 21:35:06 +02:00
mcarton
e4b97af73e #320 Add the fix_file rule 2015-07-29 21:03:47 +02:00
nvbn
9d91b96780 #298 Simplify func tests 2015-07-29 16:30:32 +03:00
nvbn
8962cf2ec1 #298 Use eager decorator when we don't need lazines 2015-07-29 16:11:23 +03:00
nvbn
d6e80b7835 #298 Suggest more than one result in *_no_command rules 2015-07-29 16:09:26 +03:00
nvbn
4bc1cc7849 #298 Add support of list results in sudo_support 2015-07-29 15:40:21 +03:00
nvbn
e6af00ef97 #298 Fix selecting command 2015-07-29 15:33:29 +03:00
nvbn
c8550a0ce5 #298 Fix python 2 support 2015-07-29 15:22:24 +03:00
Vladimir Iakovlev
0a40e7f0a9 Merge pull request #321 from mcarton/patch-1
Force the travis image to track the master branch
2015-07-29 15:06:44 +03:00
Martin Carton
9c649c05a9 Force the travis image to track the master branch 2015-07-28 23:59:17 +02:00
nvbn
7933e963d8 #298 Add ability to chose matched rule 2015-07-28 22:04:27 +03:00
nvbn
4fc18cb4e7 Decrease count of psutils calls 2015-07-28 16:26:26 +03:00
Vladimir Iakovlev
5d1dd70652 Merge pull request #319 from scorphus/tsuru-not-command
Add a new `tsuru_not_command` rule
2015-07-28 16:20:46 +03:00
Pablo Santiago Blum de Aguiar
65a25d5448 Add a new tsuru_not_command rule 2015-07-27 22:34:24 -03:00
Pablo Santiago Blum de Aguiar
4e854a575e Move get_all_matched_commands over to utils 2015-07-27 22:29:02 -03:00
nvbn
742200a500 #311 Fix build in travis-ci 2015-07-27 23:38:04 +03:00
nvbn
44cd1fd7e1 #311 Fix installation without pandoc 2015-07-27 23:31:06 +03:00
50 changed files with 1204 additions and 468 deletions

View File

@@ -12,6 +12,8 @@ addons:
- zsh
- fish
- tcsh
- pandoc
- git
env:
- FUNCTIONAL=true BARE=true
install:

View File

@@ -1,4 +1,4 @@
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck)
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg?branch=master)](https://travis-ci.org/nvbn/thefuck)
Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/)
@@ -14,7 +14,7 @@ E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?
➜ fuck
sudo apt-get install vim [enter/ctrl+c]
sudo apt-get install vim [enter/↑/↓/ctrl+c]
[sudo] password for nvbn:
Reading package lists... Done
...
@@ -29,7 +29,7 @@ To push the current branch and set the remote as upstream, use
➜ fuck
git push --set-upstream origin master [enter/ctrl+c]
git push --set-upstream origin master [enter/↑/↓/ctrl+c]
Counting objects: 9, done.
...
```
@@ -42,7 +42,7 @@ No command 'puthon' found, did you mean:
zsh: command not found: puthon
➜ fuck
python [enter/ctrl+c]
python [enter/↑/↓/ctrl+c]
Python 3.4.2 (default, Oct 8 2014, 13:08:17)
...
```
@@ -55,7 +55,7 @@ Did you mean this?
branch
➜ fuck
git branch [enter/ctrl+c]
git branch [enter/↑/↓/ctrl+c]
* master
```
@@ -67,7 +67,7 @@ Did you mean this?
repl
➜ fuck
lein repl [enter/ctrl+c]
lein repl [enter/↑/↓/ctrl+c]
nREPL server started on port 54848 on host 127.0.0.1 - nrepl://127.0.0.1:54848
REPL-y 0.3.1
...
@@ -107,9 +107,9 @@ sudo pip install thefuck
You should place this command in your `.bash_profile`, `.bashrc`, `.zshrc` or other startup script:
```bash
eval "$(thefuck-alias)"
eval "$(thefuck --alias)"
# You can use whatever you want as an alias, like for Mondays:
eval "$(thefuck-alias FUCK)"
eval "$(thefuck --alias FUCK)"
```
[Or in your shell config (Bash, Zsh, Fish, Powershell, tcsh).](https://github.com/nvbn/thefuck/wiki/Shell-aliases)
@@ -146,6 +146,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `docker_not_command` – fixes wrong docker commands like `docker tags`;
* `dry` – fixes repetitions like `git git push`;
* `fix_alt_space` – replaces Alt+Space with Space character;
* `fix_file` – opens a file with an error in your `$EDITOR`;
* `git_add` – fixes *"Did you forget to 'git add'?"*;
* `git_branch_delete` – changes `git branch -d` to `git branch -D`;
* `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch;
@@ -188,6 +189,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `systemctl` – correctly orders parameters of confusing `systemctl`;
* `test.py` – runs `py.test` instead of `test.py`;
* `tsuru_login` – runs `tsuru login` if not authenticated or session expired;
* `tsuru_not_command` – fixes wrong tsuru commands like `tsuru shell`;
* `tmux` – fixes `tmux` commands;
* `whois` – fixes `whois` command.
@@ -211,7 +213,7 @@ in `~/.thefuck/rules`. The rule should contain two functions:
```python
match(command: Command, settings: Settings) -> bool
get_new_command(command: Command, settings: Settings) -> str
get_new_command(command: Command, settings: Settings) -> str | list[str]
```
Also the rule can contain an optional function `side_effect(command: Command, settings: Settings) -> None`

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python
from subprocess import call
import os
import re
@@ -28,4 +29,7 @@ call('git commit -am "Bump to {}"'.format(version), shell=True)
call('git tag {}'.format(version), shell=True)
call('git push', shell=True)
call('git push --tags', shell=True)
call('python setup.py sdist bdist_wheel upload', shell=True)
env = os.environ
env['CONVERT_README'] = 'true'
call('python setup.py sdist bdist_wheel upload', shell=True, env=env)

View File

@@ -1,13 +1,14 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import sys
import os
try:
if os.environ.get('CONVERT_README'):
import pypandoc
long_description = pypandoc.convert('README.md', 'rst')
except:
long_description = open('README.md').read()
else:
long_description = ''
version = sys.version_info[:2]
if version < (2, 7):
@@ -19,7 +20,7 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '2.5.4'
VERSION = '2.7'
install_requires = ['psutil', 'colorama', 'six']
extras_require = {':python_version<"3.4"': ['pathlib']}

View File

@@ -1,10 +1,17 @@
from time import sleep
from pexpect import TIMEOUT
def _set_confirmation(proc, require):
proc.sendline(u'mkdir -p ~/.thefuck')
proc.sendline(
u'echo "require_confirmation = {}" > ~/.thefuck/settings.py'.format(
require))
def with_confirmation(proc):
"""Ensures that command can be fixed when confirmation enabled."""
proc.sendline(u'mkdir -p ~/.thefuck')
proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py')
_set_confirmation(proc, True)
proc.sendline(u'ehco test')
@@ -17,10 +24,10 @@ def with_confirmation(proc):
assert proc.expect([TIMEOUT, u'test'])
def history_changed(proc):
def history_changed(proc, to):
"""Ensures that history changed."""
proc.send('\033[A')
assert proc.expect([TIMEOUT, u'echo test'])
assert proc.expect([TIMEOUT, to])
def history_not_changed(proc):
@@ -29,10 +36,29 @@ def history_not_changed(proc):
assert proc.expect([TIMEOUT, u'fuck'])
def select_command_with_arrows(proc):
"""Ensures that command can be selected with arrow keys."""
_set_confirmation(proc, True)
proc.sendline(u'git h')
assert proc.expect([TIMEOUT, u"git: 'h' is not a git command."])
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'git show'])
proc.send('\033[B')
assert proc.expect([TIMEOUT, u'git push'])
proc.send('\033[B')
assert proc.expect([TIMEOUT, u'git help'])
proc.send('\033[A')
assert proc.expect([TIMEOUT, u'git push'])
proc.send('\n')
assert proc.expect([TIMEOUT, u'Not a git repository'])
def refuse_with_confirmation(proc):
"""Ensures that fix can be refused when confirmation enabled."""
proc.sendline(u'mkdir -p ~/.thefuck')
proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py')
_set_confirmation(proc, True)
proc.sendline(u'ehco test')
@@ -47,8 +73,7 @@ def refuse_with_confirmation(proc):
def without_confirmation(proc):
"""Ensures that command can be fixed when confirmation disabled."""
proc.sendline(u'mkdir -p ~/.thefuck')
proc.sendline(u'echo "require_confirmation = False" > ~/.thefuck/settings.py')
_set_confirmation(proc, False)
proc.sendline(u'ehco test')

View File

@@ -1,51 +1,53 @@
import pytest
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation, history_changed, history_not_changed
refuse_with_confirmation, history_changed, history_not_changed, \
select_command_with_arrows
from tests.functional.utils import spawn, functional, images
containers = images(('ubuntu-python3-bash', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev
RUN apt-get install -yy python3 python3-pip python3-dev git
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
'''),
('ubuntu-python2-bash', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev
RUN apt-get install -yy python python-pip python-dev git
RUN pip2 install -U pip setuptools
'''))
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'bash') as proc:
proc.sendline(u"export PS1='$ '")
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'touch $HISTFILE')
with_confirmation(proc)
history_changed(proc)
@pytest.fixture(params=containers)
def proc(request):
tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'bash')
proc.sendline(u"export PS1='$ '")
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'echo > $HISTFILE')
return proc
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'bash') as proc:
proc.sendline(u"export PS1='$ '")
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'touch $HISTFILE')
refuse_with_confirmation(proc)
history_not_changed(proc)
def test_with_confirmation(proc):
with_confirmation(proc)
history_changed(proc, u'echo test')
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'bash') as proc:
proc.sendline(u"export PS1='$ '")
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'touch $HISTFILE')
without_confirmation(proc)
history_changed(proc)
def test_select_command_with_arrows(proc):
select_command_with_arrows(proc)
history_changed(proc, u'git push')
@functional
def test_refuse_with_confirmation(proc):
refuse_with_confirmation(proc)
history_not_changed(proc)
@functional
def test_without_confirmation(proc):
without_confirmation(proc)
history_changed(proc, u'echo test')

View File

@@ -1,53 +1,59 @@
import pytest
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation
refuse_with_confirmation, select_command_with_arrows
from tests.functional.utils import spawn, functional, images, bare
containers = images(('ubuntu-python3-fish', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev fish
RUN apt-get install -yy python3 python3-pip python3-dev fish git
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
RUN apt-get install -yy fish
'''),
('ubuntu-python2-fish', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev fish
RUN apt-get install -yy python python-pip python-dev git
RUN pip2 install -U pip setuptools
RUN apt-get install -yy fish
'''))
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'fish') as proc:
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish')
proc.sendline(u'fish')
with_confirmation(proc)
@pytest.fixture(params=containers)
def proc(request):
tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'fish')
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish')
proc.sendline(u'fish')
return proc
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'fish') as proc:
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish')
proc.sendline(u'fish')
refuse_with_confirmation(proc)
def test_with_confirmation(proc):
with_confirmation(proc)
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'fish') as proc:
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish')
proc.sendline(u'fish')
without_confirmation(proc)
def test_select_command_with_arrows(proc):
select_command_with_arrows(proc)
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
def test_refuse_with_confirmation(proc):
refuse_with_confirmation(proc)
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
def test_without_confirmation(proc):
without_confirmation(proc)
# TODO: ensure that history changes.

View File

@@ -1,47 +1,51 @@
import pytest
from tests.functional.utils import spawn, functional, images
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation
refuse_with_confirmation, select_command_with_arrows
containers = images(('ubuntu-python3-tcsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev tcsh
RUN apt-get install -yy python3 python3-pip python3-dev git
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
RUN apt-get install -yy tcsh
'''),
('ubuntu-python2-tcsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev tcsh
RUN apt-get install -yy python python-pip python-dev git
RUN pip2 install -U pip setuptools
RUN apt-get install -yy tcsh
'''))
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'tcsh') as proc:
proc.sendline(u'tcsh')
proc.sendline(u'eval `thefuck-alias`')
with_confirmation(proc)
@pytest.fixture(params=containers)
def proc(request):
tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'tcsh')
proc.sendline(u'tcsh')
proc.sendline(u'eval `thefuck-alias`')
return proc
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'tcsh') as proc:
proc.sendline(u'tcsh')
proc.sendline(u'eval `thefuck-alias`')
refuse_with_confirmation(proc)
def test_with_confirmation(proc):
with_confirmation(proc)
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'tcsh') as proc:
proc.sendline(u'tcsh')
proc.sendline(u'eval `thefuck-alias`')
without_confirmation(proc)
def test_select_command_with_arrows(proc):
select_command_with_arrows(proc)
@functional
def test_refuse_with_confirmation(proc):
refuse_with_confirmation(proc)
@functional
def test_without_confirmation(proc):
without_confirmation(proc)
# TODO: ensure that history changes.

View File

@@ -1,51 +1,57 @@
import pytest
from tests.functional.utils import spawn, functional, images
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation, history_changed, history_not_changed
refuse_with_confirmation, history_changed, history_not_changed, select_command_with_arrows
containers = images(('ubuntu-python3-zsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev zsh
RUN apt-get install -yy python3 python3-pip python3-dev git
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
RUN apt-get install -yy zsh
'''),
('ubuntu-python2-zsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev zsh
RUN apt-get install -yy python python-pip python-dev git
RUN pip2 install -U pip setuptools
RUN apt-get install -yy zsh
'''))
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'zsh') as proc:
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'export HISTFILE=~/.zsh_history')
proc.sendline(u'touch $HISTFILE')
with_confirmation(proc)
history_changed(proc)
@pytest.fixture(params=containers)
def proc(request):
tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'zsh')
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'export HISTFILE=~/.zsh_history')
proc.sendline(u'echo > $HISTFILE')
proc.sendline(u'export SAVEHIST=100')
proc.sendline(u'export HISTSIZE=100')
proc.sendline(u'setopt INC_APPEND_HISTORY')
return proc
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'zsh') as proc:
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'export HISTFILE=~/.zsh_history')
proc.sendline(u'touch $HISTFILE')
refuse_with_confirmation(proc)
history_not_changed(proc)
def test_with_confirmation(proc):
with_confirmation(proc)
history_changed(proc, u'echo test')
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'zsh') as proc:
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'export HISTFILE=~/.zsh_history')
proc.sendline(u'touch $HISTFILE')
without_confirmation(proc)
history_changed(proc)
def test_select_command_with_arrows(proc):
select_command_with_arrows(proc)
history_changed(proc, u'git push')
@functional
def test_refuse_with_confirmation(proc):
refuse_with_confirmation(proc)
history_not_changed(proc)
@functional
def test_without_confirmation(proc):
without_confirmation(proc)
history_changed(proc, u'echo test')

View File

@@ -1,5 +1,4 @@
import os
from contextlib import contextmanager
import subprocess
import shutil
from tempfile import mkdtemp
@@ -15,16 +14,17 @@ enabled = os.environ.get('FUNCTIONAL')
def build_container(tag, dockerfile):
tmpdir = mkdtemp()
with Path(tmpdir).joinpath('Dockerfile').open('w') as file:
file.write(dockerfile)
if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir],
cwd=root) != 0:
raise Exception("Can't build a container")
shutil.rmtree(tmpdir)
try:
with Path(tmpdir).joinpath('Dockerfile').open('w') as file:
file.write(dockerfile)
if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir],
cwd=root) != 0:
raise Exception("Can't build a container")
finally:
shutil.rmtree(tmpdir)
@contextmanager
def spawn(tag, dockerfile, cmd):
def spawn(request, tag, dockerfile, cmd):
if bare:
proc = pexpect.spawnu(cmd)
else:
@@ -33,13 +33,12 @@ def spawn(tag, dockerfile, cmd):
proc = pexpect.spawnu('docker run --volume {}:/src --tty=true '
'--interactive=true {} {}'.format(root, tag, cmd))
proc.sendline('pip install /src')
proc.sendline('cd /')
proc.logfile = sys.stdout
try:
yield proc
finally:
proc.terminate(force=bare)
request.addfinalizer(proc.terminate)
return proc
def images(*items):

View File

@@ -22,7 +22,7 @@ def test_match(brew_unknown_cmd):
def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd2):
assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd),
None) == 'brew list'
None) == ['brew list', 'brew install', 'brew uninstall']
assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd2),
None) == 'brew install'
None) == ['brew install', 'brew uninstall', 'brew list']

View File

@@ -122,8 +122,8 @@ def test_not_match(script, stderr):
@pytest.mark.usefixtures('docker_help')
@pytest.mark.parametrize('wrong, fixed', [
('pes', 'ps'),
('tags', 'tag')])
('pes', ['ps', 'push', 'pause']),
('tags', ['tag', 'stats', 'images'])])
def test_get_new_command(wrong, fixed):
command = Command('docker {}'.format(wrong), stderr=stderr(wrong))
assert get_new_command(command, None) == 'docker {}'.format(fixed)
assert get_new_command(command, None) == ['docker {}'.format(x) for x in fixed]

View File

@@ -0,0 +1,188 @@
import pytest
import os
from thefuck.rules.fix_file import match, get_new_command
from tests.utils import Command
# (script, file, line, col (or None), stderr)
tests = (
('gcc a.c', 'a.c', 3, 1,
"""
a.c: In function 'main':
a.c:3:1: error: expected expression before '}' token
}
^
"""),
('clang a.c', 'a.c', 3, 1,
"""
a.c:3:1: error: expected expression
}
^
"""),
('perl a.pl', 'a.pl', 3, None,
"""
syntax error at a.pl line 3, at EOF
Execution of a.pl aborted due to compilation errors.
"""),
('perl a.pl', 'a.pl', 2, None,
"""
Search pattern not terminated at a.pl line 2.
"""),
('sh a.sh', 'a.sh', 2, None,
"""
a.sh: line 2: foo: command not found
"""),
('zsh a.sh', 'a.sh', 2, None,
"""
a.sh:2: command not found: foo
"""),
('bash a.sh', 'a.sh', 2, None,
"""
a.sh: line 2: foo: command not found
"""),
('rustc a.rs', 'a.rs', 2, 5,
"""
a.rs:2:5: 2:6 error: unexpected token: `+`
a.rs:2 +
^
"""),
('cargo build', 'src/lib.rs', 3, 5,
"""
Compiling test v0.1.0 (file:///tmp/fix-error/test)
src/lib.rs:3:5: 3:6 error: unexpected token: `+`
src/lib.rs:3 +
^
Could not compile `test`.
To learn more, run the command again with --verbose.
"""),
('python a.py', 'a.py', 2, None,
"""
File "a.py", line 2
+
^
SyntaxError: invalid syntax
"""),
('python a.py', 'a.py', 8, None,
"""
Traceback (most recent call last):
File "a.py", line 8, in <module>
match("foo")
File "a.py", line 5, in match
m = re.search(None, command)
File "/usr/lib/python3.4/re.py", line 170, in search
return _compile(pattern, flags).search(string)
File "/usr/lib/python3.4/re.py", line 293, in _compile
raise TypeError("first argument must be string or compiled pattern")
TypeError: first argument must be string or compiled pattern
"""
),
('ruby a.rb', 'a.rb', 3, None,
"""
a.rb:3: syntax error, unexpected keyword_end
"""),
('lua a.lua', 'a.lua', 2, None,
"""
lua: a.lua:2: unexpected symbol near '+'
"""),
('fish a.sh', '/tmp/fix-error/a.sh', 2, None,
"""
fish: Unknown command 'foo'
/tmp/fix-error/a.sh (line 2): foo
^
"""),
('./a', './a', 2, None,
"""
awk: ./a:2: BEGIN { print "Hello, world!" + }
awk: ./a:2: ^ syntax error
"""),
('llc a.ll', 'a.ll', 1, None,
"""
llc: a.ll:1:1: error: expected top-level entity
+
^
"""),
('go build a.go', 'a.go', 1, None,
"""
can't load package:
a.go:1:1: expected 'package', found '+'
"""),
('make', 'Makefile', 2, None,
"""
bidule
make: bidule: Command not found
Makefile:2: recipe for target 'target' failed
make: *** [target] Error 127
"""),
('git st', '/home/martin/.config/git/config', 1, None,
"""
fatal: bad config file line 1 in /home/martin/.config/git/config
"""),
('node fuck.js asdf qwer', '/Users/pablo/Workspace/barebones/fuck.js', '2', 5,
"""
/Users/pablo/Workspace/barebones/fuck.js:2
conole.log(arg); // this should read console.log(arg);
^
ReferenceError: conole is not defined
at /Users/pablo/Workspace/barebones/fuck.js:2:5
at Array.forEach (native)
at Object.<anonymous> (/Users/pablo/Workspace/barebones/fuck.js:1:85)
at Module._compile (module.js:460:26)
at Object.Module._extensions..js (module.js:478:10)
at Module.load (module.js:355:32)
at Function.Module._load (module.js:310:12)
at Function.Module.runMain (module.js:501:10)
at startup (node.js:129:16)
at node.js:814:3
"""),
)
@pytest.mark.parametrize('test', tests)
def test_match(mocker, monkeypatch, test):
mocker.patch('os.path.isfile', return_value=True)
monkeypatch.setenv('EDITOR', 'dummy_editor')
assert match(Command(stderr=test[4]), None)
@pytest.mark.parametrize('test', tests)
def test_no_editor(mocker, monkeypatch, test):
mocker.patch('os.path.isfile', return_value=True)
if 'EDITOR' in os.environ:
monkeypatch.delenv('EDITOR')
assert not match(Command(stderr=test[4]), None)
@pytest.mark.parametrize('test', tests)
def test_not_file(mocker, monkeypatch, test):
mocker.patch('os.path.isfile', return_value=False)
monkeypatch.setenv('EDITOR', 'dummy_editor')
assert not match(Command(stderr=test[4]), None)
@pytest.mark.parametrize('test', tests)
def test_get_new_command(monkeypatch, test):
monkeypatch.setenv('EDITOR', 'dummy_editor')
assert (get_new_command(Command(script=test[0], stderr=test[4]), None) ==
'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0]))

View File

@@ -50,8 +50,8 @@ def test_match(git_not_command, git_command, git_not_command_one_of_this):
def test_get_new_command(git_not_command, git_not_command_one_of_this,
git_not_command_closest):
assert get_new_command(Command('git brnch', stderr=git_not_command), None) \
== 'git branch'
== ['git branch']
assert get_new_command(Command('git st', stderr=git_not_command_one_of_this),
None) == 'git status'
None) == ['git stats', 'git stash', 'git stage']
assert get_new_command(Command('git tags', stderr=git_not_command_closest),
None) == 'git tag'
None) == ['git tag', 'git stage']

View File

@@ -25,4 +25,4 @@ def test_get_new_command(mocker):
mocker.patch('thefuck.rules.gulp_not_task.get_gulp_tasks', return_value=[
'serve', 'build', 'default'])
command = Command('gulp srve', stdout('srve'))
assert get_new_command(command, None) == 'gulp serve'
assert get_new_command(command, None) == ['gulp serve', 'gulp default']

View File

@@ -27,8 +27,8 @@ def test_not_match(script, stderr):
@pytest.mark.parametrize('cmd, result', [
('log', 'heroku logs'),
('pge', 'heroku pg')])
('log', ['heroku logs', 'heroku pg']),
('pge', ['heroku pg', 'heroku logs'])])
def test_get_new_command(cmd, result):
command = Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd))
assert get_new_command(command, None) == result

View File

@@ -9,6 +9,7 @@ def is_not_task():
Did you mean this?
repl
jar
'''
@@ -19,4 +20,4 @@ def test_match(is_not_task):
def test_get_new_command(is_not_task):
assert get_new_command(Mock(script='lein rpl --help', stderr=is_not_task),
None) == 'lein repl --help'
None) == ['lein repl --help', 'lein jar --help']

View File

@@ -23,7 +23,7 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command('man read'), 'man 3 read'),
(Command('man read'), ['man 3 read', 'man 2 read']),
(Command('man 2 read'), 'man 3 read'),
(Command('man 3 read'), 'man 2 read'),
(Command('man -s2 read'), 'man -s3 read'),

View File

@@ -22,8 +22,8 @@ def test_get_new_command():
assert get_new_command(
Command(stderr='vom: not found',
script='vom file.py'),
None) == 'vim file.py'
None) == ['vim file.py']
assert get_new_command(
Command(stderr='fucck: not found',
script='fucck'),
Command) == 'fsck'
Command) == ['fsck']

View File

@@ -7,17 +7,13 @@ 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_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
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
'''
extra/vim-python3 7.4.712-1 \t/usr/bin/vim'''
@pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True),
@@ -46,22 +42,37 @@ def test_not_match(command):
assert not match(command, None)
sudo_vim_possibilities = ['{} -S extra/gvim && sudo vim',
'{} -S extra/gvim-python3 && sudo vim',
'{} -S extra/vim && sudo vim',
'{} -S extra/vim-minimal && sudo vim',
'{} -S extra/vim-python3 && sudo vim']
sudo_vim_possibilities = [s.format(pacman_cmd) for s in sudo_vim_possibilities]
vim_possibilities = ['{} -S extra/gvim && vim',
'{} -S extra/gvim-python3 && vim',
'{} -S extra/vim && vim',
'{} -S extra/vim-minimal && vim',
'{} -S extra/vim-python3 && vim']
vim_possibilities = [s.format(pacman_cmd) for s in vim_possibilities]
@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 extra/gvim && vim'.format(pacman_cmd)),
(Command('sudo vim'), '{} -S extra/gvim && sudo vim'.format(pacman_cmd)),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd)),
(Command('sudo convert'), '{} -S extra/imagemagick && sudo convert'.format(pacman_cmd))])
(Command('vim'), vim_possibilities),
(Command('sudo vim'), sudo_vim_possibilities),
(Command('convert'), ['{} -S extra/imagemagick && convert'.format(pacman_cmd)]),
(Command('sudo convert'), ['{} -S extra/imagemagick && sudo 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 extra/gvim && vim'.format(pacman_cmd), PKGFILE_OUTPUT_VIM),
(Command('sudo vim'), '{} -S extra/gvim && sudo vim'.format(pacman_cmd), PKGFILE_OUTPUT_VIM),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd), PKGFILE_OUTPUT_CONVERT),
(Command('sudo convert'), '{} -S extra/imagemagick && sudo convert'.format(pacman_cmd), PKGFILE_OUTPUT_CONVERT)])
(Command('vim'), vim_possibilities, PKGFILE_OUTPUT_VIM),
(Command('sudo vim'), sudo_vim_possibilities, PKGFILE_OUTPUT_VIM),
(Command('convert'), ['{} -S extra/imagemagick && convert'.format(pacman_cmd)], PKGFILE_OUTPUT_CONVERT),
(Command('sudo convert'), ['{} -S extra/imagemagick && sudo 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):

View File

@@ -21,5 +21,9 @@ def test_not_match():
assert not match(Command(), None)
def test_get_new_command():
assert get_new_command(Command('ls'), None) == 'sudo ls'
@pytest.mark.parametrize('before, after', [
('ls', 'sudo ls'),
('echo a > b', 'sudo sh -c "echo a > b"'),
('echo "a" >> b', 'sudo sh -c "echo \\"a\\" >> b"')])
def test_get_new_command(before, after):
assert get_new_command(Command(before), None) == after

View File

@@ -16,4 +16,4 @@ def test_match(tmux_ambiguous):
def test_get_new_command(tmux_ambiguous):
assert get_new_command(Command('tmux list', stderr=tmux_ambiguous), None)\
== 'tmux list-keys'
== ['tmux list-keys', 'tmux list-panes', 'tmux list-windows']

View File

@@ -0,0 +1,90 @@
import pytest
from tests.utils import Command
from thefuck.rules.tsuru_not_command import match, get_new_command
@pytest.mark.parametrize('command', [
Command('tsuru log', stderr=(
'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\tapp-log\n'
'\tlogin\n'
'\tlogout\n'
)),
Command('tsuru app-l', stderr=(
'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\tapp-list\n'
'\tapp-log\n'
)),
Command('tsuru user-list', stderr=(
'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\tteam-user-list\n'
)),
Command('tsuru targetlist', stderr=(
'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\ttarget-list\n'
)),
])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command('tsuru tchururu', stderr=(
'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
)),
Command('tsuru version', stderr='tsuru version 0.16.0.'),
Command('tsuru help', stderr=(
'tsuru version 0.16.0.\n'
'\nUsage: tsuru command [args]\n'
)),
Command('tsuru platform-list', stderr=(
'- java\n'
'- logstashgiro\n'
'- newnode\n'
'- nodejs\n'
'- php\n'
'- python\n'
'- python3\n'
'- ruby\n'
'- ruby20\n'
'- static\n'
)),
Command('tsuru env-get', stderr='Error: App thefuck not found.'),
])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_commands', [
(Command('tsuru log', stderr=(
'tsuru: "log" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\tapp-log\n'
'\tlogin\n'
'\tlogout\n'
)), ['tsuru login', 'tsuru logout', 'tsuru app-log']),
(Command('tsuru app-l', stderr=(
'tsuru: "app-l" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\tapp-list\n'
'\tapp-log\n'
)), ['tsuru app-log', 'tsuru app-list']),
(Command('tsuru user-list', stderr=(
'tsuru: "user-list" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\tteam-user-list\n'
)), ['tsuru team-user-list']),
(Command('tsuru targetlist', stderr=(
'tsuru: "targetlist" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n'
'\ttarget-list\n'
)), ['tsuru target-list']),
])
def test_get_new_command(command, new_commands):
assert get_new_command(command, None) == new_commands

View File

@@ -6,7 +6,7 @@ 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')])
Command(script='whois meta.unix.stackexchange.com')])
def test_match(command):
assert match(command, None)
@@ -15,9 +15,12 @@ def test_not_match():
assert not match(Command(script='whois'), None)
# `whois com` actually makes sense
@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')])
(Command('whois meta.unix.stackexchange.com'), ['whois unix.stackexchange.com',
'whois stackexchange.com',
'whois com'])])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

97
tests/test_corrector.py Normal file
View File

@@ -0,0 +1,97 @@
import pytest
from pathlib import PosixPath, Path
from mock import Mock
from thefuck import corrector, conf, types
from tests.utils import Rule, Command, CorrectedCommand
from thefuck.corrector import make_corrected_commands, get_corrected_commands, remove_duplicates
def test_load_rule(mocker):
match = object()
get_new_command = object()
load_source = mocker.patch(
'thefuck.corrector.load_source',
return_value=Mock(match=match,
get_new_command=get_new_command,
enabled_by_default=True,
priority=900,
requires_output=True))
assert corrector.load_rule(Path('/rules/bash.py'), settings=Mock(priority={})) \
== Rule('bash', match, get_new_command, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py')
class TestGetRules(object):
@pytest.fixture(autouse=True)
def glob(self, mocker):
return mocker.patch('thefuck.corrector.Path.glob', return_value=[])
def _compare_names(self, rules, names):
return [r.name for r in rules] == names
@pytest.mark.parametrize('conf_rules, rules', [
(conf.DEFAULT_RULES, ['bash', 'lisp', 'bash', 'lisp']),
(types.RulesNamesList(['bash']), ['bash', 'bash'])])
def test_get(self, monkeypatch, glob, conf_rules, rules):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
monkeypatch.setattr('thefuck.corrector.load_source',
lambda x, _: Rule(x))
assert self._compare_names(
corrector.get_rules(Path('~'), Mock(rules=conf_rules, priority={})),
rules)
class TestGetMatchedRules(object):
def test_no_match(self):
assert list(corrector.get_matched_rules(
Command('ls'), [Rule('', lambda *_: False)],
Mock(no_colors=True))) == []
def test_match(self):
rule = Rule('', lambda x, _: x.script == 'cd ..')
assert list(corrector.get_matched_rules(
Command('cd ..'), [rule], Mock(no_colors=True))) == [rule]
def test_when_rule_failed(self, capsys):
all(corrector.get_matched_rules(
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')),
requires_output=False)],
Mock(no_colors=True, debug=False)))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
class TestGetCorrectedCommands(object):
def test_with_rule_returns_list(self):
rule = Rule(get_new_command=lambda x, _: [x.script + '!', x.script + '@'],
priority=100)
assert list(make_corrected_commands(Command(script='test'), [rule], None)) \
== [CorrectedCommand(script='test!', priority=100),
CorrectedCommand(script='test@', priority=200)]
def test_with_rule_returns_command(self):
rule = Rule(get_new_command=lambda x, _: x.script + '!',
priority=100)
assert list(make_corrected_commands(Command(script='test'), [rule], None)) \
== [CorrectedCommand(script='test!', priority=100)]
def test_remove_duplicates():
side_effect = lambda *_: None
assert set(remove_duplicates([CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', priority=200),
CorrectedCommand('ls', side_effect, 300)])) \
== {CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', side_effect, 300)}
def test_get_corrected_commands(mocker):
command = Command('test', 'test', 'test')
rules = [Rule(match=lambda *_: False),
Rule(match=lambda *_: True,
get_new_command=lambda x, _: x.script + '!', priority=100),
Rule(match=lambda *_: True,
get_new_command=lambda x, _: [x.script + '@', x.script + ';'],
priority=60)]
mocker.patch('thefuck.corrector.get_rules', return_value=rules)
assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \
== ['test@', 'test!', 'test;']

View File

@@ -1,61 +1,8 @@
import pytest
from subprocess import PIPE
from pathlib import PosixPath, Path
from mock import Mock
from thefuck import main, conf, types
from tests.utils import Rule, Command
def test_load_rule(mocker):
match = object()
get_new_command = object()
load_source = mocker.patch(
'thefuck.main.load_source',
return_value=Mock(match=match,
get_new_command=get_new_command,
enabled_by_default=True,
priority=900,
requires_output=True))
assert main.load_rule(Path('/rules/bash.py')) \
== Rule('bash', match, get_new_command, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py')
class TestGetRules(object):
@pytest.fixture(autouse=True)
def glob(self, mocker):
return mocker.patch('thefuck.main.Path.glob', return_value=[])
def _compare_names(self, rules, names):
return [r.name for r in rules] == names
@pytest.mark.parametrize('conf_rules, rules', [
(conf.DEFAULT_RULES, ['bash', 'lisp', 'bash', 'lisp']),
(types.RulesNamesList(['bash']), ['bash', 'bash'])])
def test_get(self, monkeypatch, glob, conf_rules, rules):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
monkeypatch.setattr('thefuck.main.load_source',
lambda x, _: Rule(x))
assert self._compare_names(
main.get_rules(Path('~'), Mock(rules=conf_rules, priority={})),
rules)
@pytest.mark.parametrize('priority, unordered, ordered', [
({},
[Rule('bash', priority=100), Rule('python', priority=5)],
['python', 'bash']),
({},
[Rule('lisp', priority=9999), Rule('c', priority=conf.DEFAULT_PRIORITY)],
['c', 'lisp']),
({'python': 9999},
[Rule('bash', priority=100), Rule('python', priority=5)],
['bash', 'python'])])
def test_ordered_by_priority(self, monkeypatch, priority, unordered, ordered):
monkeypatch.setattr('thefuck.main._get_loaded_rules',
lambda *_: unordered)
assert self._compare_names(
main.get_rules(Path('~'), Mock(priority=priority)),
ordered)
from thefuck import main
from tests.utils import Command
class TestGetCommand(object):
@@ -79,7 +26,7 @@ class TestGetCommand(object):
def test_get_command_calls(self, Popen):
assert main.get_command(Mock(env={}),
['thefuck', 'apt-get', 'search', 'vim']) \
['thefuck', 'apt-get', 'search', 'vim']) \
== Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
@@ -95,80 +42,3 @@ class TestGetCommand(object):
assert main.get_command(Mock(env={}), args).script == result
else:
assert main.get_command(Mock(env={}), args) is None
class TestGetMatchedRule(object):
def test_no_match(self):
assert main.get_matched_rule(
Command('ls'), [Rule('', lambda *_: False)],
Mock(no_colors=True)) is None
def test_match(self):
rule = Rule('', lambda x, _: x.script == 'cd ..')
assert main.get_matched_rule(
Command('cd ..'), [rule], Mock(no_colors=True)) == rule
def test_when_rule_failed(self, capsys):
main.get_matched_rule(
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))],
Mock(no_colors=True, debug=False))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
class TestRunRule(object):
@pytest.fixture(autouse=True)
def confirm(self, mocker):
return mocker.patch('thefuck.main.confirm', return_value=True)
def test_run_rule(self, capsys):
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
Command(), None)
assert capsys.readouterr() == ('new-command\n', '')
def test_run_rule_with_side_effect(self, capsys):
side_effect = Mock()
settings = Mock(debug=False)
command = Command()
main.run_rule(Rule(get_new_command=lambda *_: 'new-command',
side_effect=side_effect),
command, settings)
assert capsys.readouterr() == ('new-command\n', '')
side_effect.assert_called_once_with(command, settings)
def test_when_not_comfirmed(self, capsys, confirm):
confirm.return_value = False
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
Command(), None)
assert capsys.readouterr() == ('', '')
class TestConfirm(object):
@pytest.fixture
def stdin(self, mocker):
return mocker.patch('sys.stdin.read', return_value='\n')
def test_when_not_required(self, capsys):
assert main.confirm('command', None, Mock(require_confirmation=False))
assert capsys.readouterr() == ('', 'command\n')
def test_with_side_effect_and_without_confirmation(self, capsys):
assert main.confirm('command', Mock(), Mock(require_confirmation=False))
assert capsys.readouterr() == ('', 'command (+side effect)\n')
# `stdin` fixture should be applied after `capsys`
def test_when_confirmation_required_and_confirmed(self, capsys, stdin):
assert main.confirm('command', None, Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]')
# `stdin` fixture should be applied after `capsys`
def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin):
assert main.confirm('command', Mock(), Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command (+side effect) [enter/ctrl+c]')
def test_when_confirmation_required_and_aborted(self, capsys, stdin):
stdin.side_effect = KeyboardInterrupt
assert not main.confirm('command', None, Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n')

124
tests/test_ui.py Normal file
View File

@@ -0,0 +1,124 @@
# -*- encoding: utf-8 -*-
from mock import Mock
import pytest
from itertools import islice
from thefuck import ui
from thefuck.types import CorrectedCommand
@pytest.fixture
def patch_getch(monkeypatch):
def patch(vals):
def getch():
for val in vals:
if val == KeyboardInterrupt:
raise val
else:
yield val
getch_gen = getch()
monkeypatch.setattr('thefuck.ui.getch', lambda: next(getch_gen))
return patch
def test_read_actions(patch_getch):
patch_getch([ # Enter:
'\n',
# Enter:
'\r',
# Ignored:
'x', 'y',
# Up:
'\x1b', '[', 'A',
# Down:
'\x1b', '[', 'B',
# Ctrl+C:
KeyboardInterrupt], )
assert list(islice(ui.read_actions(), 5)) \
== [ui.SELECT, ui.SELECT, ui.PREVIOUS, ui.NEXT, ui.ABORT]
def test_command_selector():
selector = ui.CommandSelector([1, 2, 3])
assert selector.value == 1
changes = []
selector.on_change(changes.append)
selector.next()
assert selector.value == 2
selector.next()
assert selector.value == 3
selector.next()
assert selector.value == 1
selector.previous()
assert selector.value == 3
assert changes == [1, 2, 3, 1, 3]
class TestSelectCommand(object):
@pytest.fixture
def commands_with_side_effect(self):
return [CorrectedCommand('ls', lambda *_: None, 100),
CorrectedCommand('cd', lambda *_: None, 100)]
@pytest.fixture
def commands(self):
return [CorrectedCommand('ls', None, 100),
CorrectedCommand('cd', None, 100)]
def test_without_commands(self, capsys):
assert ui.select_command([], Mock(debug=False, no_color=True)) is None
assert capsys.readouterr() == ('', 'No fucks given\n')
def test_without_confirmation(self, capsys, commands):
assert ui.select_command(commands,
Mock(debug=False, no_color=True,
require_confirmation=False)) == commands[0]
assert capsys.readouterr() == ('', 'ls\n')
def test_without_confirmation_with_side_effects(self, capsys,
commands_with_side_effect):
assert ui.select_command(commands_with_side_effect,
Mock(debug=False, no_color=True,
require_confirmation=False)) \
== commands_with_side_effect[0]
assert capsys.readouterr() == ('', 'ls (+side effect)\n')
def test_with_confirmation(self, capsys, patch_getch, commands):
patch_getch(['\n'])
assert ui.select_command(commands,
Mock(debug=False, no_color=True,
require_confirmation=True)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_one_match(self, capsys, patch_getch, commands):
patch_getch(['\n'])
assert ui.select_command((commands[0],),
Mock(debug=False, no_color=True,
require_confirmation=True)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/ctrl+c]\n')
def test_with_confirmation_abort(self, capsys, patch_getch, commands):
patch_getch([KeyboardInterrupt])
assert ui.select_command(commands,
Mock(debug=False, no_color=True,
require_confirmation=True)) is None
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n')
def test_with_confirmation_with_side_effct(self, capsys, patch_getch,
commands_with_side_effect):
patch_getch(['\n'])
assert ui.select_command(commands_with_side_effect,
Mock(debug=False, no_color=True,
require_confirmation=True))\
== commands_with_side_effect[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_select_second(self, capsys, patch_getch, commands):
patch_getch(['\x1b', '[', 'B', '\n'])
assert ui.select_command(commands,
Mock(debug=False, no_color=True,
require_confirmation=True)) == commands[1]
assert capsys.readouterr() == (
'', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n')

View File

@@ -1,7 +1,8 @@
import pytest
from mock import Mock
from thefuck.utils import git_support, sudo_support, wrap_settings,\
memoize, get_closest, get_all_executables, replace_argument
memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands
from thefuck.types import Settings
from tests.utils import Command
@@ -17,6 +18,7 @@ def test_wrap_settings(override, old, new):
@pytest.mark.parametrize('return_value, command, called, result', [
('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'),
('ls -lah', 'ls', 'ls', 'ls -lah'),
(['ls -lah'], 'sudo ls', 'ls', ['sudo ls -lah']),
(True, 'sudo ls', 'ls', True),
(True, 'ls', 'ls', True),
(False, 'sudo ls', 'ls', False),
@@ -99,3 +101,32 @@ def test_get_all_callables():
(('git brnch', 'brnch', 'branch'), 'git branch')])
def test_replace_argument(args, result):
assert replace_argument(*args) == result
@pytest.mark.parametrize('stderr, result', [
(("git: 'cone' is not a git command. See 'git --help'.\n"
'\n'
'Did you mean one of these?\n'
'\tclone'), ['clone']),
(("git: 're' is not a git command. See 'git --help'.\n"
'\n'
'Did you mean one of these?\n'
'\trebase\n'
'\treset\n'
'\tgrep\n'
'\trm'), ['rebase', 'reset', 'grep', 'rm']),
(('tsuru: "target" is not a tsuru command. See "tsuru help".\n'
'\n'
'Did you mean one of these?\n'
'\tservice-add\n'
'\tservice-bind\n'
'\tservice-doc\n'
'\tservice-info\n'
'\tservice-list\n'
'\tservice-remove\n'
'\tservice-status\n'
'\tservice-unbind'), ['service-add', 'service-bind', 'service-doc',
'service-info', 'service-list', 'service-remove',
'service-status', 'service-unbind'])])
def test_get_all_matched_commands(stderr, result):
assert list(get_all_matched_commands(stderr)) == result

View File

@@ -15,3 +15,7 @@ def Rule(name='', match=lambda *_: True,
return types.Rule(name, match, get_new_command,
enabled_by_default, side_effect,
priority, requires_output)
def CorrectedCommand(script='', side_effect=None, priority=DEFAULT_PRIORITY):
return types.CorrectedCommand(script, side_effect, priority)

87
thefuck/corrector.py Normal file
View File

@@ -0,0 +1,87 @@
import sys
from imp import load_source
from pathlib import Path
from . import conf, types, logs
from .utils import eager
def load_rule(rule, settings):
"""Imports rule module and returns it."""
name = rule.name[:-3]
rule_module = load_source(name, str(rule))
priority = getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY)
return types.Rule(name, rule_module.match,
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True),
getattr(rule_module, 'side_effect', None),
settings.priority.get(name, priority),
getattr(rule_module, 'requires_output', True))
def get_loaded_rules(rules, settings):
"""Yields all available rules."""
for rule in rules:
if rule.name != '__init__.py':
loaded_rule = load_rule(rule, settings)
if loaded_rule in settings.rules:
yield loaded_rule
@eager
def get_rules(user_dir, settings):
"""Returns all enabled rules."""
bundled = Path(__file__).parent \
.joinpath('rules') \
.glob('*.py')
user = user_dir.joinpath('rules').glob('*.py')
return get_loaded_rules(sorted(bundled) + sorted(user), settings)
@eager
def get_matched_rules(command, rules, settings):
"""Returns first matched rule for command."""
script_only = command.stdout is None and command.stderr is None
for rule in rules:
if script_only and rule.requires_output:
continue
try:
with logs.debug_time(u'Trying rule: {};'.format(rule.name),
settings):
if rule.match(command, settings):
yield rule
except Exception:
logs.rule_failed(rule, sys.exc_info(), settings)
def make_corrected_commands(command, rules, settings):
for rule in rules:
new_commands = rule.get_new_command(command, settings)
if not isinstance(new_commands, list):
new_commands = [new_commands]
for n, new_command in enumerate(new_commands):
yield types.CorrectedCommand(script=new_command,
side_effect=rule.side_effect,
priority=(n + 1) * rule.priority)
def remove_duplicates(corrected_commands):
commands = {(command.script, command.side_effect): command
for command in sorted(corrected_commands,
key=lambda command: -command.priority)}
return commands.values()
def get_corrected_commands(command, user_dir, settings):
rules = get_rules(user_dir, settings)
logs.debug(
u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
settings)
matched = get_matched_rules(command, rules, settings)
logs.debug(
u'Matched rules: {}'.format(', '.join(rule.name for rule in matched)),
settings)
corrected_commands = make_corrected_commands(command, matched, settings)
return sorted(remove_duplicates(corrected_commands),
key=lambda corrected_command: corrected_command.priority)

View File

@@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
from contextlib import contextmanager
from datetime import datetime
import sys
@@ -28,27 +30,6 @@ def rule_failed(rule, exc_info, settings):
exception('Rule {}'.format(rule.name), exc_info, settings)
def show_command(new_command, side_effect, settings):
sys.stderr.write('{bold}{command}{reset}{side_effect}\n'.format(
command=new_command,
side_effect=' (+side effect)' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, side_effect, settings):
sys.stderr.write(
'{bold}{command}{reset}{side_effect} '
'[{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command,
side_effect=' (+side effect)' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
sys.stderr.flush()
def failed(msg, settings):
sys.stderr.write('{red}{msg}{reset}\n'.format(
msg=msg,
@@ -56,6 +37,33 @@ def failed(msg, settings):
reset=color(colorama.Style.RESET_ALL, settings)))
def show_corrected_command(corrected_command, settings):
sys.stderr.write('{bold}{script}{reset}{side_effect}\n'.format(
script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_text(corrected_command, multiple_cmds, settings):
if multiple_cmds:
arrows = '{blue}{reset}/{blue}{reset}/'
else:
arrows = ''
sys.stderr.write(
('{clear}{bold}{script}{reset}{side_effect} '
'[{green}enter{reset}/' + arrows + '{red}ctrl+c{reset}]').format(
script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r',
bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings),
blue=color(colorama.Fore.BLUE, settings)))
def debug(msg, settings):
if settings.debug:
sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format(
@@ -71,4 +79,4 @@ def debug_time(msg, settings):
try:
yield
finally:
debug('{} took: {}'.format(msg, datetime.now() - started), settings)
debug(u'{} took: {}'.format(msg, datetime.now() - started), settings)

View File

@@ -1,7 +1,9 @@
from imp import load_source
from argparse import ArgumentParser
from warnings import warn
from pathlib import Path
from os.path import expanduser
from pprint import pformat
import pkg_resources
from subprocess import Popen, PIPE
import os
import sys
@@ -9,6 +11,8 @@ from psutil import Process, TimeoutExpired
import colorama
import six
from . import logs, conf, types, shells
from .corrector import get_corrected_commands
from .ui import select_command
def setup_user_dir():
@@ -21,37 +25,6 @@ def setup_user_dir():
return user_dir
def load_rule(rule):
"""Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule))
return types.Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True),
getattr(rule_module, 'side_effect', None),
getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY),
getattr(rule_module, 'requires_output', True))
def _get_loaded_rules(rules, settings):
"""Yields all available rules."""
for rule in rules:
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
def get_rules(user_dir, settings):
"""Returns all enabled rules."""
bundled = Path(__file__).parent \
.joinpath('rules') \
.glob('*.py')
user = user_dir.joinpath('rules').glob('*.py')
rules = _get_loaded_rules(sorted(bundled) + sorted(user), settings)
return sorted(rules, key=lambda rule: settings.priority.get(
rule.name, rule.priority))
def wait_output(settings, popen):
"""Returns `True` if we can get output of the command in the
`wait_command` time.
@@ -100,51 +73,17 @@ def get_command(settings, args):
return types.Command(script, None, None)
def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command."""
script_only = command.stdout is None and command.stderr is None
for rule in rules:
if script_only and rule.requires_output:
continue
try:
with logs.debug_time(u'Trying rule: {};'.format(rule.name),
settings):
if rule.match(command, settings):
return rule
except Exception:
logs.rule_failed(rule, sys.exc_info(), settings)
def confirm(new_command, side_effect, settings):
"""Returns `True` when running of new command confirmed."""
if not settings.require_confirmation:
logs.show_command(new_command, side_effect, settings)
return True
logs.confirm_command(new_command, side_effect, settings)
try:
sys.stdin.read(1)
return True
except KeyboardInterrupt:
logs.failed('Aborted', settings)
return False
def run_rule(rule, command, settings):
def run_command(command, settings):
"""Runs command from rule for passed command."""
new_command = shells.to_shell(rule.get_new_command(command, settings))
if confirm(new_command, rule.side_effect, settings):
if rule.side_effect:
rule.side_effect(command, settings)
shells.put_to_history(new_command)
print(new_command)
if command.side_effect:
command.side_effect(command, settings)
shells.put_to_history(command.script)
print(command.script)
# Entry points:
def main():
def fix_command():
colorama.init()
user_dir = setup_user_dir()
settings = conf.get_settings(user_dir)
@@ -152,22 +91,41 @@ def main():
logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv)
rules = get_rules(user_dir, settings)
logs.debug(
u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
settings)
matched_rule = get_matched_rule(command, rules, settings)
if matched_rule:
logs.debug(u'Matched rule: {}'.format(matched_rule.name), settings)
run_rule(matched_rule, command, settings)
return
logs.failed('No fuck given', settings)
corrected_commands = get_corrected_commands(command, user_dir, settings)
selected_command = select_command(corrected_commands, settings)
if selected_command:
run_command(selected_command, settings)
def print_alias():
def print_alias(entry_point=True):
if entry_point:
warn('`thefuck-alias` is deprecated, us `thefuck --alias` instead.')
position = 1
else:
position = 2
alias = shells.thefuck_alias()
if len(sys.argv) > 1:
alias = sys.argv[1]
if len(sys.argv) > position:
alias = sys.argv[position]
print(shells.app_alias(alias))
def main():
parser = ArgumentParser(prog='The Fuck')
parser.add_argument('-v', '--version',
action='version',
version='%(prog)s {}'.format(
pkg_resources.require('thefuck')[0].version))
parser.add_argument('-a', '--alias',
action='store_true',
help='[custom-alias-name] prints alias for current shell')
parser.add_argument('command',
nargs='*',
help='command that should be fixed')
known_args = parser.parse_args(sys.argv[1:2])
if known_args.alias:
print_alias(False)
elif known_args.command:
fix_command()
else:
parser.print_usage()

View File

@@ -1,7 +1,7 @@
import os
import re
import subprocess
from thefuck.utils import get_closest, replace_argument
from thefuck.utils import get_closest, replace_command
BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps'
@@ -77,10 +77,6 @@ if brew_path_prefix:
pass
def _get_similar_command(command):
return get_closest(command, brew_commands)
def match(command, settings):
is_proper_command = ('brew' in command.script and
'Unknown command' in command.stderr)
@@ -89,7 +85,7 @@ def match(command, settings):
if is_proper_command:
broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)',
command.stderr)[0]
has_possible_commands = bool(_get_similar_command(broken_cmd))
has_possible_commands = bool(get_closest(broken_cmd, brew_commands))
return has_possible_commands
@@ -97,6 +93,4 @@ def match(command, settings):
def get_new_command(command, settings):
broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)',
command.stderr)[0]
new_cmd = _get_similar_command(broken_cmd)
return replace_argument(command.script, broken_cmd, new_cmd)
return replace_command(command, broken_cmd, brew_commands)

View File

@@ -1,7 +1,7 @@
from itertools import dropwhile, takewhile, islice
import re
import subprocess
from thefuck.utils import get_closest, sudo_support, replace_argument
from thefuck.utils import get_closest, sudo_support, replace_argument, replace_command
@sudo_support
@@ -23,5 +23,4 @@ def get_docker_commands():
def get_new_command(command, settings):
wrong_command = re.findall(
r"docker: '(\w+)' is not a docker command.", command.stderr)[0]
fixed_command = get_closest(wrong_command, get_docker_commands())
return replace_argument(command.script, wrong_command, fixed_command)
return replace_command(command, wrong_command, get_docker_commands())

67
thefuck/rules/fix_file.py Normal file
View File

@@ -0,0 +1,67 @@
import re
import os
from thefuck.utils import memoize
from thefuck import shells
patterns = (
# js, node:
'^ at {file}:{line}:{col}',
# cargo:
'^ {file}:{line}:{col}',
# python, thefuck:
'^ File "{file}", line {line}',
# awk:
'^awk: {file}:{line}:',
# git
'^fatal: bad config file line {line} in {file}',
# llc:
'^llc: {file}:{line}:{col}:',
# lua:
'^lua: {file}:{line}:',
# fish:
'^{file} \(line {line}\):',
# bash, sh, ssh:
'^{file}: line {line}: ',
# ghc, make, ruby, zsh:
'^{file}:{line}:',
# cargo, clang, gcc, go, rustc:
'^{file}:{line}:{col}',
# perl:
'at {file} line {line}',
)
# for the sake of readability do not use named groups above
def _make_pattern(pattern):
pattern = pattern.replace('{file}', '(?P<file>[^:\n]+)')
pattern = pattern.replace('{line}', '(?P<line>[0-9]+)')
pattern = pattern.replace('{col}', '(?P<col>[0-9]+)')
return re.compile(pattern, re.MULTILINE)
patterns = [_make_pattern(p) for p in patterns]
@memoize
def _search(stderr):
for pattern in patterns:
m = re.search(pattern, stderr)
if m:
return m
def match(command, settings):
if 'EDITOR' not in os.environ:
return False
m = _search(command.stderr)
return m and os.path.isfile(m.group('file'))
def get_new_command(command, settings):
m = _search(command.stderr)
# Note: there does not seem to be a standard for columns, so they are just
# ignored for now
return shells.and_('{} {} +{}'.format(os.environ['EDITOR'], m.group('file'), m.group('line')),
command.script)

View File

@@ -1,5 +1,6 @@
import re
from thefuck.utils import get_closest, git_support, replace_argument
from thefuck.utils import (git_support,
get_all_matched_commands, replace_command)
@git_support
@@ -8,20 +9,9 @@ def match(command, settings):
and 'Did you mean' in command.stderr)
def _get_all_git_matched_commands(stderr):
should_yield = False
for line in stderr.split('\n'):
if 'Did you mean' in line:
should_yield = True
elif should_yield and line:
yield line.strip()
@git_support
def get_new_command(command, settings):
broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
command.stderr)[0]
new_cmd = get_closest(broken_cmd,
_get_all_git_matched_commands(command.stderr))
return replace_argument(command.script, broken_cmd, new_cmd)
matched = get_all_matched_commands(command.stderr)
return replace_command(command, broken_cmd, matched)

View File

@@ -1,6 +1,6 @@
import re
import subprocess
from thefuck.utils import get_closest, replace_argument
from thefuck.utils import replace_command
def match(command, script):
@@ -18,5 +18,4 @@ def get_gulp_tasks():
def get_new_command(command, script):
wrong_task = re.findall(r"Task '(\w+)' is not in your gulpfile",
command.stdout)[0]
fixed_task = get_closest(wrong_task, get_gulp_tasks())
return replace_argument(command.script, wrong_task, fixed_task)
return replace_command(command, wrong_task, get_gulp_tasks())

View File

@@ -1,5 +1,5 @@
import re
from thefuck.utils import get_closest, replace_argument
from thefuck.utils import replace_command
def match(command, settings):
@@ -16,5 +16,4 @@ def _get_suggests(stderr):
def get_new_command(command, settings):
wrong = re.findall(r'`(\w+)` is not a heroku command', command.stderr)[0]
correct = get_closest(wrong, _get_suggests(command.stderr))
return replace_argument(command.script, wrong, correct)
return replace_command(command, wrong, _get_suggests(command.stderr))

View File

@@ -1,5 +1,6 @@
import re
from thefuck.utils import sudo_support, replace_argument
from thefuck.utils import sudo_support,\
replace_command, get_all_matched_commands
@sudo_support
@@ -13,6 +14,5 @@ def match(command, settings):
def get_new_command(command, settings):
broken_cmd = re.findall(r"'([^']*)' is not a task",
command.stderr)[0]
new_cmd = re.findall(r'Did you mean this\?\n\s*([^\n]*)',
command.stderr)[0]
return replace_argument(command.script, broken_cmd, new_cmd)
new_cmds = get_all_matched_commands(command.stderr, 'Did you mean this?')
return replace_command(command, broken_cmd, new_cmds)

View File

@@ -8,6 +8,10 @@ def get_new_command(command, settings):
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)
split_cmd2 = command.script.split()
split_cmd3 = split_cmd2[:]
split_cmd2.insert(1, ' 2 ')
split_cmd3.insert(1, ' 3 ')
return ["".join(split_cmd3), "".join(split_cmd2)]

View File

@@ -1,5 +1,5 @@
from difflib import get_close_matches
from thefuck.utils import sudo_support, get_all_executables, get_closest
from thefuck.utils import sudo_support, get_all_executables
@sudo_support
@@ -12,8 +12,9 @@ def match(command, settings):
@sudo_support
def get_new_command(command, settings):
old_command = command.script.split(' ')[0]
new_command = get_closest(old_command, get_all_executables())
return ' '.join([new_command] + command.script.split(' ')[1:])
new_cmds = get_close_matches(old_command, get_all_executables(), cutoff=0.1)
return [' '.join([new_command] + command.script.split(' ')[1:])
for new_command in new_cmds]
priority = 3000

View File

@@ -14,10 +14,12 @@ def __get_pkgfile(command):
command = command.split(" ")[0]
return subprocess.check_output(
packages = subprocess.check_output(
['pkgfile', '-b', '-v', command],
universal_newlines=True, stderr=DEVNULL
).split()
).splitlines()
return [package.split()[0] for package in packages]
except subprocess.CalledProcessError:
return None
@@ -27,10 +29,11 @@ def match(command, settings):
def get_new_command(command, settings):
package = __get_pkgfile(command)[0]
packages = __get_pkgfile(command)
formatme = shells.and_('{} -S {}', '{}')
return formatme.format(pacman, package, command.script)
return [formatme.format(pacman, package, command.script)
for package in packages]
if not which('pkgfile'):

View File

@@ -9,6 +9,7 @@ patterns = ['permission denied',
'This operation requires root.',
'requested operation requires superuser privilege',
'must be run as root',
'must run as root',
'must be superuser',
'must be root',
'need to be root',
@@ -27,4 +28,7 @@ def match(command, settings):
def get_new_command(command, settings):
return u'sudo {}'.format(command.script)
if '>' in command.script:
return u'sudo sh -c "{}"'.format(command.script.replace('"', '\\"'))
else:
return u'sudo {}'.format(command.script)

View File

@@ -1,4 +1,4 @@
from thefuck.utils import get_closest, replace_argument
from thefuck.utils import get_closest, replace_command
import re
@@ -15,6 +15,4 @@ def get_new_command(command, settings):
old_cmd = cmd.group(1)
suggestions = [cmd.strip() for cmd in cmd.group(2).split(',')]
new_cmd = get_closest(old_cmd, suggestions)
return replace_argument(command.script, old_cmd, new_cmd)
return replace_command(command, old_cmd, suggestions)

View File

@@ -0,0 +1,17 @@
import re
from thefuck.utils import (get_closest, replace_argument,
get_all_matched_commands, replace_command)
def match(command, settings):
return (command.script.startswith('tsuru ')
and ' is not a tsuru command. See "tsuru help".' in command.stderr
and '\nDid you mean?\n\t' in command.stderr)
def get_new_command(command, settings):
broken_cmd = re.findall(r'tsuru: "([^"]*)" is not a tsuru command',
command.stderr)[0]
return replace_command(command, broken_cmd,
get_all_matched_commands(command.stderr))

View File

@@ -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 and len(command.script.split()) > 1
return 'whois ' in command.script.strip()
def get_new_command(command, settings):
@@ -28,4 +28,5 @@ def get_new_command(command, settings):
if '/' in command.script:
return 'whois ' + urlparse(url).netloc
elif '.' in command.script:
return 'whois ' + '.'.join(urlparse(url).path.split('.')[1:])
path = urlparse(url).path.split('.')
return ['whois ' + '.'.join(path[n:]) for n in range(1, len(path))]

View File

@@ -224,6 +224,7 @@ shells = defaultdict(lambda: Generic(), {
'tcsh': Tcsh()})
@memoize
def _get_shell():
try:
shell = Process(os.getpid()).parent().name()

View File

@@ -3,6 +3,8 @@ from collections import namedtuple
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
CorrectedCommand = namedtuple('CorrectedCommand', ('script', 'side_effect', 'priority'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default', 'side_effect',
'priority', 'requires_output'))

104
thefuck/ui.py Normal file
View File

@@ -0,0 +1,104 @@
# -*- encoding: utf-8 -*-
import sys
from . import logs
try:
from msvcrt import getch
except ImportError:
def getch():
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
if ch == '\x03': # For compatibility with msvcrt.getch
raise KeyboardInterrupt
return ch
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
SELECT = 0
ABORT = 1
PREVIOUS = 2
NEXT = 3
def read_actions():
"""Yields actions for pressed keys."""
buffer = []
while True:
try:
ch = getch()
except KeyboardInterrupt: # Ctrl+C
yield ABORT
if ch in ('\n', '\r'): # Enter
yield SELECT
buffer.append(ch)
buffer = buffer[-3:]
if buffer == ['\x1b', '[', 'A']: # ↑
yield PREVIOUS
elif buffer == ['\x1b', '[', 'B']: # ↓
yield NEXT
class CommandSelector(object):
def __init__(self, commands):
self._commands = commands
self._index = 0
self._on_change = lambda x: x
def next(self):
self._index = (self._index + 1) % len(self._commands)
self._on_change(self.value)
def previous(self):
self._index = (self._index - 1) % len(self._commands)
self._on_change(self.value)
@property
def value(self):
return self._commands[self._index]
def on_change(self, fn):
self._on_change = fn
fn(self.value)
def select_command(corrected_commands, settings):
"""Returns:
- the first command when confirmation disabled;
- None when ctrl+c pressed;
- selected command.
"""
if not corrected_commands:
logs.failed('No fucks given', settings)
return
selector = CommandSelector(corrected_commands)
if not settings.require_confirmation:
logs.show_corrected_command(selector.value, settings)
return selector.value
multiple_cmds = len(corrected_commands) > 1
selector.on_change(lambda val: logs.confirm_text(val, multiple_cmds, settings))
for action in read_actions():
if action == SELECT:
sys.stderr.write('\n')
return selector.value
elif action == ABORT:
logs.failed('\nAborted', settings)
return
elif action == PREVIOUS:
selector.previous()
elif action == NEXT:
selector.next()

View File

@@ -69,6 +69,8 @@ def sudo_support(fn):
if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result)
elif isinstance(result, list):
return [u'sudo {}'.format(x) for x in result]
else:
return result
return wrapper
@@ -159,3 +161,27 @@ def replace_argument(script, from_, to):
else:
return script.replace(
u' {} '.format(from_), u' {} '.format(to), 1)
def eager(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
return list(fn(*args, **kwargs))
return wrapper
@eager
def get_all_matched_commands(stderr, separator='Did you mean'):
should_yield = False
for line in stderr.split('\n'):
if separator in line:
should_yield = True
elif should_yield and line:
yield line.strip()
def replace_command(command, broken, matched):
"""Helper for *_no_command rules."""
new_cmds = get_close_matches(broken, matched, cutoff=0.1)
return [replace_argument(command.script, broken, new_cmd.strip())
for new_cmd in new_cmds]