1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-01-19 12:24:29 +00:00

Merge pull request #324 from nvbn/298-variants

Add ability to select fixed command from variants
This commit is contained in:
Vladimir Iakovlev 2015-07-31 15:39:57 +03:00
commit 8be353941f
34 changed files with 663 additions and 422 deletions

View File

@ -13,6 +13,7 @@ addons:
- fish - fish
- tcsh - tcsh
- pandoc - pandoc
- git
env: env:
- FUNCTIONAL=true BARE=true - FUNCTIONAL=true BARE=true
install: install:

View File

@ -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? E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?
➜ fuck ➜ fuck
sudo apt-get install vim [enter/ctrl+c] sudo apt-get install vim [enter/↑/↓/ctrl+c]
[sudo] password for nvbn: [sudo] password for nvbn:
Reading package lists... Done Reading package lists... Done
... ...
@ -29,7 +29,7 @@ To push the current branch and set the remote as upstream, use
➜ fuck ➜ fuck
git push --set-upstream origin master [enter/ctrl+c] git push --set-upstream origin master [enter/↑/↓/ctrl+c]
Counting objects: 9, done. Counting objects: 9, done.
... ...
``` ```
@ -42,7 +42,7 @@ No command 'puthon' found, did you mean:
zsh: command not found: puthon zsh: command not found: puthon
➜ fuck ➜ fuck
python [enter/ctrl+c] python [enter/↑/↓/ctrl+c]
Python 3.4.2 (default, Oct 8 2014, 13:08:17) Python 3.4.2 (default, Oct 8 2014, 13:08:17)
... ...
``` ```
@ -55,7 +55,7 @@ Did you mean this?
branch branch
➜ fuck ➜ fuck
git branch [enter/ctrl+c] git branch [enter/↑/↓/ctrl+c]
* master * master
``` ```
@ -67,7 +67,7 @@ Did you mean this?
repl repl
➜ fuck ➜ 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 nREPL server started on port 54848 on host 127.0.0.1 - nrepl://127.0.0.1:54848
REPL-y 0.3.1 REPL-y 0.3.1
... ...
@ -213,7 +213,7 @@ in `~/.thefuck/rules`. The rule should contain two functions:
```python ```python
match(command: Command, settings: Settings) -> bool 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` Also the rule can contain an optional function `side_effect(command: Command, settings: Settings) -> None`

View File

@ -1,10 +1,17 @@
from time import sleep
from pexpect import TIMEOUT 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): def with_confirmation(proc):
"""Ensures that command can be fixed when confirmation enabled.""" """Ensures that command can be fixed when confirmation enabled."""
proc.sendline(u'mkdir -p ~/.thefuck') _set_confirmation(proc, True)
proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py')
proc.sendline(u'ehco test') proc.sendline(u'ehco test')
@ -17,10 +24,10 @@ def with_confirmation(proc):
assert proc.expect([TIMEOUT, u'test']) assert proc.expect([TIMEOUT, u'test'])
def history_changed(proc): def history_changed(proc, to):
"""Ensures that history changed.""" """Ensures that history changed."""
proc.send('\033[A') proc.send('\033[A')
assert proc.expect([TIMEOUT, u'echo test']) assert proc.expect([TIMEOUT, to])
def history_not_changed(proc): def history_not_changed(proc):
@ -29,10 +36,29 @@ def history_not_changed(proc):
assert proc.expect([TIMEOUT, u'fuck']) 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): def refuse_with_confirmation(proc):
"""Ensures that fix can be refused when confirmation enabled.""" """Ensures that fix can be refused when confirmation enabled."""
proc.sendline(u'mkdir -p ~/.thefuck') _set_confirmation(proc, True)
proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py')
proc.sendline(u'ehco test') proc.sendline(u'ehco test')
@ -47,8 +73,7 @@ def refuse_with_confirmation(proc):
def without_confirmation(proc): def without_confirmation(proc):
"""Ensures that command can be fixed when confirmation disabled.""" """Ensures that command can be fixed when confirmation disabled."""
proc.sendline(u'mkdir -p ~/.thefuck') _set_confirmation(proc, False)
proc.sendline(u'echo "require_confirmation = False" > ~/.thefuck/settings.py')
proc.sendline(u'ehco test') proc.sendline(u'ehco test')

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import os import os
from contextlib import contextmanager
import subprocess import subprocess
import shutil import shutil
from tempfile import mkdtemp from tempfile import mkdtemp
@ -15,16 +14,17 @@ enabled = os.environ.get('FUNCTIONAL')
def build_container(tag, dockerfile): def build_container(tag, dockerfile):
tmpdir = mkdtemp() tmpdir = mkdtemp()
try:
with Path(tmpdir).joinpath('Dockerfile').open('w') as file: with Path(tmpdir).joinpath('Dockerfile').open('w') as file:
file.write(dockerfile) file.write(dockerfile)
if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir], if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir],
cwd=root) != 0: cwd=root) != 0:
raise Exception("Can't build a container") raise Exception("Can't build a container")
finally:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
@contextmanager def spawn(request, tag, dockerfile, cmd):
def spawn(tag, dockerfile, cmd):
if bare: if bare:
proc = pexpect.spawnu(cmd) proc = pexpect.spawnu(cmd)
else: else:
@ -33,13 +33,12 @@ def spawn(tag, dockerfile, cmd):
proc = pexpect.spawnu('docker run --volume {}:/src --tty=true ' proc = pexpect.spawnu('docker run --volume {}:/src --tty=true '
'--interactive=true {} {}'.format(root, tag, cmd)) '--interactive=true {} {}'.format(root, tag, cmd))
proc.sendline('pip install /src') proc.sendline('pip install /src')
proc.sendline('cd /')
proc.logfile = sys.stdout proc.logfile = sys.stdout
try: request.addfinalizer(proc.terminate)
yield proc return proc
finally:
proc.terminate(force=bare)
def images(*items): 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): def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd2):
assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd), 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), 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.usefixtures('docker_help')
@pytest.mark.parametrize('wrong, fixed', [ @pytest.mark.parametrize('wrong, fixed', [
('pes', 'ps'), ('pes', ['ps', 'push', 'pause']),
('tags', 'tag')]) ('tags', ['tag', 'stats', 'images'])])
def test_get_new_command(wrong, fixed): def test_get_new_command(wrong, fixed):
command = Command('docker {}'.format(wrong), stderr=stderr(wrong)) 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

@ -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, def test_get_new_command(git_not_command, git_not_command_one_of_this,
git_not_command_closest): git_not_command_closest):
assert get_new_command(Command('git brnch', stderr=git_not_command), None) \ 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), 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), 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=[ mocker.patch('thefuck.rules.gulp_not_task.get_gulp_tasks', return_value=[
'serve', 'build', 'default']) 'serve', 'build', 'default'])
command = Command('gulp srve', stdout('srve')) 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', [ @pytest.mark.parametrize('cmd, result', [
('log', 'heroku logs'), ('log', ['heroku logs', 'heroku pg']),
('pge', 'heroku pg')]) ('pge', ['heroku pg', 'heroku logs'])])
def test_get_new_command(cmd, result): def test_get_new_command(cmd, result):
command = Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd)) command = Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd))
assert get_new_command(command, None) == result assert get_new_command(command, None) == result

View File

@ -9,6 +9,7 @@ def is_not_task():
Did you mean this? Did you mean this?
repl repl
jar
''' '''
@ -19,4 +20,4 @@ def test_match(is_not_task):
def test_get_new_command(is_not_task): def test_get_new_command(is_not_task):
assert get_new_command(Mock(script='lein rpl --help', stderr=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

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

View File

@ -61,30 +61,30 @@ def test_not_match(command):
assert not match(command, None) assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [ @pytest.mark.parametrize('command, new_commands', [
(Command('tsuru log', stderr=( (Command('tsuru log', stderr=(
'tsuru: "log" is not a tsuru command. See "tsuru help".\n' 'tsuru: "log" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n' '\nDid you mean?\n'
'\tapp-log\n' '\tapp-log\n'
'\tlogin\n' '\tlogin\n'
'\tlogout\n' '\tlogout\n'
)), 'tsuru login'), )), ['tsuru login', 'tsuru logout', 'tsuru app-log']),
(Command('tsuru app-l', stderr=( (Command('tsuru app-l', stderr=(
'tsuru: "app-l" is not a tsuru command. See "tsuru help".\n' 'tsuru: "app-l" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n' '\nDid you mean?\n'
'\tapp-list\n' '\tapp-list\n'
'\tapp-log\n' '\tapp-log\n'
)), 'tsuru app-log'), )), ['tsuru app-log', 'tsuru app-list']),
(Command('tsuru user-list', stderr=( (Command('tsuru user-list', stderr=(
'tsuru: "user-list" is not a tsuru command. See "tsuru help".\n' 'tsuru: "user-list" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n' '\nDid you mean?\n'
'\tteam-user-list\n' '\tteam-user-list\n'
)), 'tsuru team-user-list'), )), ['tsuru team-user-list']),
(Command('tsuru targetlist', stderr=( (Command('tsuru targetlist', stderr=(
'tsuru: "targetlist" is not a tsuru command. See "tsuru help".\n' 'tsuru: "targetlist" is not a tsuru command. See "tsuru help".\n'
'\nDid you mean?\n' '\nDid you mean?\n'
'\ttarget-list\n' '\ttarget-list\n'
)), 'tsuru target-list'), )), ['tsuru target-list']),
]) ])
def test_get_new_command(command, new_command): def test_get_new_command(command, new_commands):
assert get_new_command(command, None) == new_command assert get_new_command(command, None) == new_commands

88
tests/test_corrector.py Normal file
View File

@ -0,0 +1,88 @@
import pytest
from pathlib import PosixPath, Path
from mock import Mock
from thefuck import corrector, conf, types
from tests.utils import Rule, Command
from thefuck.corrector import make_corrected_commands, get_corrected_commands
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)) \
== [types.CorrectedCommand(script='test!', priority=100, side_effect=None),
types.CorrectedCommand(script='test@', priority=200, side_effect=None)]
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)) \
== [types.CorrectedCommand(script='test!', priority=100, side_effect=None)]
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 import pytest
from subprocess import PIPE from subprocess import PIPE
from pathlib import PosixPath, Path
from mock import Mock from mock import Mock
from thefuck import main, conf, types from thefuck import main
from tests.utils import Rule, Command from tests.utils import 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)
class TestGetCommand(object): class TestGetCommand(object):
@ -95,80 +42,3 @@ class TestGetCommand(object):
assert main.get_command(Mock(env={}), args).script == result assert main.get_command(Mock(env={}), args).script == result
else: else:
assert main.get_command(Mock(env={}), args) is None 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')

117
tests/test_ui.py Normal file
View File

@ -0,0 +1,117 @@
# -*- 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 fuck 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_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

@ -18,6 +18,7 @@ def test_wrap_settings(override, old, new):
@pytest.mark.parametrize('return_value, command, called, result', [ @pytest.mark.parametrize('return_value, command, called, result', [
('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'), ('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'),
('ls -lah', 'ls', 'ls', 'ls -lah'), ('ls -lah', 'ls', 'ls', 'ls -lah'),
(['ls -lah'], 'sudo ls', 'ls', ['sudo ls -lah']),
(True, 'sudo ls', 'ls', True), (True, 'sudo ls', 'ls', True),
(True, 'ls', 'ls', True), (True, 'ls', 'ls', True),
(False, 'sudo ls', 'ls', False), (False, 'sudo ls', 'ls', False),

80
thefuck/corrector.py Normal file
View File

@ -0,0 +1,80 @@
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 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(corrected_commands,
key=lambda corrected_command: corrected_command.priority)

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime from datetime import datetime
import sys import sys
@ -28,27 +30,6 @@ def rule_failed(rule, exc_info, settings):
exception('Rule {}'.format(rule.name), 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): def failed(msg, settings):
sys.stderr.write('{red}{msg}{reset}\n'.format( sys.stderr.write('{red}{msg}{reset}\n'.format(
msg=msg, msg=msg,
@ -56,6 +37,27 @@ def failed(msg, settings):
reset=color(colorama.Style.RESET_ALL, 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, settings):
sys.stderr.write(
'\033[1K\r{bold}{script}{reset}{side_effect} '
'[{green}enter{reset}/{blue}{reset}/{blue}{reset}/{red}ctrl+c{reset}]'.format(
script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.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),
blue=color(colorama.Fore.BLUE, settings)))
def debug(msg, settings): def debug(msg, settings):
if settings.debug: if settings.debug:
sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format( sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format(

View File

@ -1,4 +1,3 @@
from imp import load_source
from pathlib import Path from pathlib import Path
from os.path import expanduser from os.path import expanduser
from pprint import pformat from pprint import pformat
@ -9,6 +8,8 @@ from psutil import Process, TimeoutExpired
import colorama import colorama
import six import six
from . import logs, conf, types, shells from . import logs, conf, types, shells
from .corrector import get_corrected_commands
from .ui import select_command
def setup_user_dir(): def setup_user_dir():
@ -21,37 +22,6 @@ def setup_user_dir():
return 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): def wait_output(settings, popen):
"""Returns `True` if we can get output of the command in the """Returns `True` if we can get output of the command in the
`wait_command` time. `wait_command` time.
@ -100,46 +70,12 @@ def get_command(settings, args):
return types.Command(script, None, None) return types.Command(script, None, None)
def get_matched_rule(command, rules, settings): def run_command(command, 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):
"""Runs command from rule for passed command.""" """Runs command from rule for passed command."""
new_command = shells.to_shell(rule.get_new_command(command, settings)) if command.side_effect:
if confirm(new_command, rule.side_effect, settings): command.side_effect(command, settings)
if rule.side_effect: shells.put_to_history(command.script)
rule.side_effect(command, settings) print(command.script)
shells.put_to_history(new_command)
print(new_command)
# Entry points: # Entry points:
@ -152,18 +88,10 @@ def main():
logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings) logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv) command = get_command(settings, sys.argv)
rules = get_rules(user_dir, settings) corrected_commands = get_corrected_commands(command, user_dir, settings)
logs.debug( selected_command = select_command(corrected_commands, settings)
u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), if selected_command:
settings) run_command(selected_command, 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)
def print_alias(): def print_alias():

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import re import re
from thefuck.utils import (get_closest, git_support, replace_argument, from thefuck.utils import (git_support,
get_all_matched_commands) get_all_matched_commands, replace_command)
@git_support @git_support
@ -13,7 +13,5 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
broken_cmd = re.findall(r"git: '([^']*)' is not a git command", broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
command.stderr)[0] command.stderr)[0]
new_cmd = get_closest(broken_cmd, matched = get_all_matched_commands(command.stderr)
get_all_matched_commands(command.stderr)) return replace_command(command, broken_cmd, matched)
return replace_argument(command.script, broken_cmd, new_cmd)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import re import re
from thefuck.utils import (get_closest, replace_argument, from thefuck.utils import (get_closest, replace_argument,
get_all_matched_commands) get_all_matched_commands, replace_command)
def match(command, settings): def match(command, settings):
@ -12,7 +12,6 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
broken_cmd = re.findall(r'tsuru: "([^"]*)" is not a tsuru command', broken_cmd = re.findall(r'tsuru: "([^"]*)" is not a tsuru command',
command.stderr)[0] command.stderr)[0]
new_cmd = get_closest(broken_cmd, return replace_command(command, broken_cmd,
get_all_matched_commands(command.stderr)) get_all_matched_commands(command.stderr))
return replace_argument(command.script, broken_cmd, new_cmd)

View File

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

103
thefuck/ui.py Normal file
View File

@ -0,0 +1,103 @@
# -*- 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
if 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 fuck given', settings)
return
selector = CommandSelector(corrected_commands)
if not settings.require_confirmation:
logs.show_corrected_command(selector.value, settings)
return selector.value
selector.on_change(lambda val: logs.confirm_text(val, 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): if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result) return u'sudo {}'.format(result)
elif isinstance(result, list):
return [u'sudo {}'.format(x) for x in result]
else: else:
return result return result
return wrapper return wrapper
@ -161,6 +163,14 @@ def replace_argument(script, from_, to):
u' {} '.format(from_), u' {} '.format(to), 1) 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'): def get_all_matched_commands(stderr, separator='Did you mean'):
should_yield = False should_yield = False
for line in stderr.split('\n'): for line in stderr.split('\n'):
@ -168,3 +178,10 @@ def get_all_matched_commands(stderr, separator='Did you mean'):
should_yield = True should_yield = True
elif should_yield and line: elif should_yield and line:
yield line.strip() 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]