1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-10 03:52:06 +00:00

Compare commits

..

50 Commits
2.8 ... 2.9.1

Author SHA1 Message Date
nvbn
6df772ba05 Bump to 2.9.1 2015-09-05 11:46:34 +03:00
nvbn
e2e8b6fc86 Fix without result 2015-09-05 11:45:39 +03:00
nvbn
faa7ee6030 Bump to 2.9 2015-09-05 11:21:21 +03:00
nvbn
6321f25533 Fix bare run of func tests 2015-09-02 13:52:06 +03:00
nvbn
9a02e821cd Fix python 2 support 2015-09-02 11:54:58 +03:00
nvbn
4129ff2717 #353 Cache aliases in a temporary file 2015-09-02 11:10:03 +03:00
nvbn
ea6600be8b Kill containers after func tests 2015-09-02 10:33:45 +03:00
nvbn
b0195a8748 Reorganize imports 2015-09-02 09:43:40 +03:00
Vladimir Iakovlev
fc35ee657e Merge pull request #354 from mcarton/patch-1
Some README fixes
2015-09-02 09:37:28 +03:00
Vladimir Iakovlev
50207d8180 Merge pull request #352 from mcarton/slow
Fix slowness problems II
2015-09-02 09:36:48 +03:00
Martin Carton
b6855587fa Some README fixes 2015-09-02 00:05:14 +02:00
nvbn
45d849b1ac Use thefuck --alias in func tests 2015-09-01 18:36:25 +03:00
Vladimir Iakovlev
d8027bb499 Merge pull request #348 from myoung34/master
fix usage. why not
2015-09-01 18:21:21 +03:00
nvbn
4932122f71 #349 Add installation of command-not-found in install script 2015-09-01 18:18:28 +03:00
nvbn
8a4f4eea45 #349 Add note about python-commandnotfound dependency 2015-09-01 18:10:53 +03:00
mcarton
ff8d61a4fb Merge branch 'master' of github.com:nvbn/thefuck into slow 2015-09-01 14:43:41 +02:00
nvbn
6dcf9a3a14 Fix python 2 support 2015-09-01 15:32:23 +03:00
mcarton
8b62959fe3 Merge branch 'master' of github.com:nvbn/thefuck into slow 2015-09-01 14:28:30 +02:00
nvbn
21103d1b50 Simplify corrector steps 2015-09-01 14:43:27 +03:00
nvbn
61937e9e8f #334: Wait only for first matched rule; regression: always show arrows 2015-09-01 14:34:41 +03:00
nvbn
5d74344994 Make CorrectedCommand ignore priority when checking equality 2015-09-01 13:03:24 +03:00
nvbn
12394ca842 #334: Don't wait for all rules before showing result 2015-09-01 12:51:41 +03:00
nvbn
ebe53f0d18 Use decorator library 2015-08-27 16:52:26 +03:00
nvbn
0c283ff2b8 #334 Speed-up rules with caching for_app decorator 2015-08-27 16:42:09 +03:00
nvbn
bc78f1bbee git push origin masterMerge branch 'mlk-35_mvn' 2015-08-27 16:11:00 +03:00
nvbn
f2a7364e8c #351 Speed-up mvn rules, pep8 fixes 2015-08-27 16:10:50 +03:00
nvbn
9103c1ffd5 Add is_app/for_app helpers 2015-08-27 16:08:29 +03:00
nvbn
edf77a90ad Merge branch '35_mvn' of https://github.com/mlk/thefuck into mlk-35_mvn 2015-08-27 14:09:03 +03:00
mcarton
27c14a44af Fix tests
Thanks to [scorphus] for his [help].

[scorphus]: https://github.com/scorphus
[help]: https://github.com/nvbn/thefuck/pull/352#issuecomment-135248982
2015-08-27 10:54:42 +02:00
mcarton
514bb7df81 Don't run a shell just to run another shell 2015-08-26 23:38:33 +02:00
mcarton
3bd2c8d4c8 Remove unused imports 2015-08-26 21:43:20 +02:00
mcarton
e5ce000399 Improve the ssh_known_hosts rule import time 2015-08-26 21:43:20 +02:00
mcarton
9fc2bc904c Slightly improve the fix_file rule 2015-08-26 21:43:20 +02:00
mcarton
51f1f44162 Memoize thefuck.utils.which
It is used by some rules to determine if they should be enabled by
default and searches in the $PATH, which can be quiet slow.
2015-08-26 21:43:20 +02:00
mcarton
b0702d309f Improve brew* rules import time 2015-08-26 21:43:16 +02:00
mcarton
2b750bac8b Log rule import times 2015-08-26 19:14:38 +02:00
Michael Lee
8c02658a32 Removed debug statement 2015-08-26 15:02:46 +01:00
Michael Lee
301de75aee #35 - Fuzzy matching on maven lifecycle targets 2015-08-26 14:58:45 +01:00
Michael Lee
0d86fce9be #35 mvn will auto add clean package 2015-08-26 14:01:36 +01:00
nvbn
7be71a0121 #334 Add performance test 2015-08-26 14:34:39 +03:00
nvbn
354ae119c6 Don't duplicate project root in tests 2015-08-26 12:12:52 +03:00
myoung34
a2b2e5b5b8 fix usage. why not 2015-08-25 16:40:22 -05:00
nvbn
b21c9ebb43 Move all app/os specific utils to specific package 2015-08-25 14:09:47 +03:00
nvbn
2e002f666b Move utility functions from archlinux to utils 2015-08-25 13:55:33 +03:00
Vladimir Iakovlev
4163fb5f2e Merge pull request #340 from BuZZ-T/feature/apt-get-search
Adding rule for trying to search using apt-get
2015-08-25 13:48:41 +03:00
nvbn
e72c88e344 #346 Add support of other systems with get-pip 2015-08-25 13:47:30 +03:00
nvbn
f4eebbaaf9 #346 Improve installation script 2015-08-25 12:03:42 +03:00
nvbn
5e5a8e4dfa #346 Restart shell session after install 2015-08-25 10:18:21 +03:00
Bastian Gebhardt
8cbe236845 Adding rule for trying to search using apt-get 2015-08-25 08:09:31 +02:00
Bastian Gebhardt
2b3e8dc62a Adding rule for trying to search using apt-get 2015-08-25 00:20:21 +02:00
100 changed files with 1080 additions and 515 deletions

View File

@@ -99,7 +99,7 @@ Reading package lists... Done
On Ubuntu and OS X you can install `The Fuck` with installation script: On Ubuntu and OS X you can install `The Fuck` with installation script:
```bash ```bash
wget -O - https://raw.githubusercontent.com/nvbn/thefuck/master/install.sh | sh - wget -O - https://raw.githubusercontent.com/nvbn/thefuck/master/install.sh | sh - && $0
``` ```
## Manual installation ## Manual installation
@@ -169,7 +169,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `git_stash` – stashes you local modifications before rebasing or switching branch; * `git_stash` – stashes you local modifications before rebasing or switching branch;
* `go_run` – appends `.go` extension when compiling/running Go programs * `go_run` – appends `.go` extension when compiling/running Go programs
* `grep_recursive` – adds `-r` when you trying to `grep` directory; * `grep_recursive` – adds `-r` when you trying to `grep` directory;
* `gulp_not_task` – fixes misspelled gulp tasks; * `gulp_not_task` – fixes misspelled `gulp` tasks;
* `has_exists_script` – prepends `./` when script/binary exists; * `has_exists_script` – prepends `./` when script/binary exists;
* `heroku_not_command` – fixes wrong `heroku` commands like `heroku log`; * `heroku_not_command` – fixes wrong `heroku` commands like `heroku log`;
* `history` – tries to replace command with most similar command from history; * `history` – tries to replace command with most similar command from history;
@@ -181,13 +181,15 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `man_no_space` – fixes man commands without spaces, for example `mandiff`;
* `mercurial` – fixes wrong `hg` commands; * `mercurial` – fixes wrong `hg` commands;
* `mkdir_p` – adds `-p` when you trying to create directory without parent; * `mkdir_p` – adds `-p` when you trying to create directory without parent;
* `mvn_no_command` – adds `clean package` to `mvn`;
* `mvn_unknown_lifecycle_phase` – fixes misspelled lifecycle phases with `mvn`;
* `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_command` – fixes wrong console commands, for example `vom/vim`;
* `no_such_file` – creates missing directories with `mv` and `cp` commands; * `no_such_file` – creates missing directories with `mv` and `cp` commands;
* `open` – prepends `http` to address passed to `open`; * `open` – prepends `http` to address passed to `open`;
* `pip_unknown_command` – fixes wrong `pip` commands, for example `pip instatl/pip install`; * `pip_unknown_command` – fixes wrong `pip` commands, for example `pip instatl/pip install`;
* `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script;
* `python_execute` – appends missing `.py` when executing Python files; * `python_execute` – appends missing `.py` when executing Python files;
* `quotation_marks` – fixes uneven usage of `'` and `"` when containing args' * `quotation_marks` – fixes uneven usage of `'` and `"` when containing args';
* `rm_dir` – adds `-rf` when you trying to remove directory; * `rm_dir` – adds `-rf` when you trying to remove directory;
* `sed_unterminated_s` – adds missing '/' to `sed`'s `s` commands; * `sed_unterminated_s` – adds missing '/' to `sed`'s `s` commands;
* `sl_ls` – changes `sl` to `ls`; * `sl_ls` – changes `sl` to `ls`;
@@ -199,18 +201,19 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `tsuru_login` – runs `tsuru login` if not authenticated or session expired; * `tsuru_login` – runs `tsuru login` if not authenticated or session expired;
* `tsuru_not_command` – fixes wrong `tsuru` commands like `tsuru shell`; * `tsuru_not_command` – fixes wrong `tsuru` commands like `tsuru shell`;
* `tmux` – fixes `tmux` commands; * `tmux` – fixes `tmux` commands;
* `unknown_command` – fixes hadoop hdfs-style "unknown command" for example adds missing '-' to the command on `hdfs dfs ls`; * `unknown_command` – fixes hadoop hdfs-style "unknown command", for example adds missing '-' to the command on `hdfs dfs ls`;
* `vagrant_up` – starts up the vagrant instance; * `vagrant_up` – starts up the vagrant instance;
* `whois` – fixes `whois` command. * `whois` – fixes `whois` command.
Enabled by default only on specific platforms: Enabled by default only on specific platforms:
* `apt_get` – installs app from apt if it not installed; * `apt_get` – installs app from apt if it not installed (requires `python-commandnotfound` / `python3-commandnotfound`);
* `apt_get_search` – changes trying to search using `apt-get` with searching using `apt-cache`;
* `brew_install` – fixes formula name for `brew install`; * `brew_install` – fixes formula name for `brew install`;
* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`;
* `brew_upgrade` – appends `--all` to `brew upgrade` as per Homebrew's new behaviour; * `brew_upgrade` – appends `--all` to `brew upgrade` as per Homebrew's new behaviour;
* `pacman` – installs app with `pacman` if it is not installed (uses `yaourt` if available). * `pacman` – installs app with `pacman` if it is not installed (uses `yaourt` if available);
* `pacman_not_found` – fix package name with `pacman` or `yaourt`; * `pacman_not_found` – fixes package name with `pacman` or `yaourt`.
Bundled, but not enabled by default: Bundled, but not enabled by default:

View File

@@ -1,11 +1,29 @@
#!/bin/sh #!/bin/sh
should_add_alias () {
[ -f $1 ] && ! grep -q thefuck $1
}
# Install os dependencies: # Install os dependencies:
if [ -f $(which apt-get) ]; then if [ -f $(which apt-get) ]; then
sudo apt-get install python-pip # Debian/ubuntu:
sudo apt-get update -yy
sudo apt-get install -yy python-pip python-dev command-not-found
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
# In case of different python versions:
sudo apt-get install -yy python-commandnotfound
fi
else else
if [ -f $(which brew) ]; then if [ -f $(which brew) ]; then
# OS X:
brew update
brew install python brew install python
else
# Genreic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py
fi fi
fi fi
@@ -14,22 +32,22 @@ sudo pip install -U pip setuptools
sudo pip install -U thefuck sudo pip install -U thefuck
# Setup aliases: # Setup aliases:
if [ -f ~/.bashrc ]; then if should_add_alias ~/.bashrc; then
echo 'eval $(thefuck --alias)' >> ~/.bashrc echo 'eval $(thefuck --alias)' >> ~/.bashrc
fi fi
if [ -f ~/.bash_profile ]; then if should_add_alias ~/.bash_profile; then
echo 'eval $(thefuck --alias)' >> ~/.bash_profile echo 'eval $(thefuck --alias)' >> ~/.bash_profile
fi fi
if [ -f ~/.zshrc ]; then if should_add_alias ~/.zshrc; then
echo 'eval $(thefuck --alias)' >> ~/.zshrc echo 'eval $(thefuck --alias)' >> ~/.zshrc
fi fi
if [ -f ~/.config/fish/config.fish ]; then if should_add_alias ~/.config/fish/config.fish; then
thefuck --alias >> ~/.config/fish/config.fish thefuck --alias >> ~/.config/fish/config.fish
fi fi
if [ -f ~/.tcshrc ]; then if should_add_alias ~/.tcshrc; then
echo 'eval `thefuck --alias`' >> ~/.tcshrc echo 'eval `thefuck --alias`' >> ~/.tcshrc
fi fi

View File

@@ -5,3 +5,4 @@ wheel
setuptools>=17.1 setuptools>=17.1
pexpect pexpect
pypandoc pypandoc
pytest-benchmark

View File

@@ -20,9 +20,9 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version)) ' ({}.{} detected).'.format(*version))
sys.exit(-1) sys.exit(-1)
VERSION = '2.8' VERSION = '2.9.1'
install_requires = ['psutil', 'colorama', 'six'] install_requires = ['psutil', 'colorama', 'six', 'decorator']
extras_require = {':python_version<"3.4"': ['pathlib']} extras_require = {':python_version<"3.4"': ['pathlib']}
setup(name='thefuck', setup(name='thefuck',

View File

@@ -1,6 +1,17 @@
import pytest import pytest
from mock import Mock
@pytest.fixture @pytest.fixture
def no_memoize(monkeypatch): def no_memoize(monkeypatch):
monkeypatch.setattr('thefuck.utils.memoize.disabled', True) monkeypatch.setattr('thefuck.utils.memoize.disabled', True)
@pytest.fixture
def settings():
return Mock(debug=False, no_colors=True)
@pytest.fixture(autouse=True)
def no_cache(monkeypatch):
monkeypatch.setattr('thefuck.utils.cache.disabled', True)

View File

@@ -1,4 +1,3 @@
from time import sleep
from pexpect import TIMEOUT from pexpect import TIMEOUT

View File

@@ -24,7 +24,7 @@ def proc(request):
tag, dockerfile = request.param tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'bash') 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'echo > $HISTFILE') proc.sendline(u'echo > $HISTFILE')
return proc return proc

View File

@@ -24,7 +24,7 @@ RUN apt-get install -yy fish
def proc(request): def proc(request):
tag, dockerfile = request.param tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'fish') proc = spawn(request, tag, dockerfile, u'fish')
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish') proc.sendline(u'thefuck --alias > ~/.config/fish/config.fish')
proc.sendline(u'fish') proc.sendline(u'fish')
return proc return proc

View File

@@ -0,0 +1,25 @@
import pytest
from pexpect import TIMEOUT
from tests.functional.utils import spawn, functional, bare
envs = ((u'bash', 'ubuntu-bash', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy bash
'''), (u'bash', 'generic-bash', u'''
FROM fedora:latest
RUN dnf install -yy python-devel sudo which gcc
'''))
@functional
@pytest.mark.skipif(
bool(bare), reason="Can't be tested in bare run")
@pytest.mark.parametrize('shell, tag, dockerfile', envs)
def test_installation(request, shell, tag, dockerfile):
proc = spawn(request, tag, dockerfile, shell, install=False)
proc.sendline(u'cat /src/install.sh | sh - && $0')
proc.sendline(u'thefuck --version')
assert proc.expect([TIMEOUT, u'The Fuck'], timeout=600)
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'No fucks given'])

View File

@@ -0,0 +1,56 @@
from pexpect import TIMEOUT
import pytest
import time
from tests.functional.utils import spawn, functional, bare
dockerfile = u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev git
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
RUN adduser --disabled-password --gecos '' test
ENV SEED "{seed}"
COPY thefuck /src
WORKDIR /src
RUN pip install .
USER test
RUN echo 'eval $(thefuck --alias)' > /home/test/.bashrc
RUN echo > /home/test/.bash_history
RUN git config --global user.email "you@example.com"
RUN git config --global user.name "Your Name"
'''.format(seed=time.time())
@pytest.fixture
def proc(request):
return spawn(request, 'ubuntu-python3-bash-performance',
dockerfile, u'bash', install=False, copy_src=True)
def plot(proc):
proc.sendline(u'cd /home/test/')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'No fucks given'])
proc.sendline(u'git init')
proc.sendline(u'git add .')
proc.sendline(u'git commit -a -m init')
proc.sendline(u'git brnch')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'git branch'])
proc.send('\n')
assert proc.expect([TIMEOUT, u'master'])
proc.sendline(u'echo test')
proc.sendline(u'echo tst')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'echo test'])
proc.send('\n')
assert proc.expect([TIMEOUT, u'test'])
@functional
@pytest.mark.skipif(
bool(bare), reason='Would lie on a bare run')
@pytest.mark.benchmark(min_rounds=10)
def test_performance(proc, benchmark):
assert benchmark(plot, proc) is None

View File

@@ -25,7 +25,7 @@ def proc(request):
tag, dockerfile = request.param tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'tcsh') 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 return proc

View File

@@ -24,7 +24,7 @@ RUN apt-get install -yy zsh
def proc(request): def proc(request):
tag, dockerfile = request.param tag, dockerfile = request.param
proc = spawn(request, tag, dockerfile, u'zsh') 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'echo > $HISTFILE') proc.sendline(u'echo > $HISTFILE')
proc.sendline(u'export SAVEHIST=100') proc.sendline(u'export SAVEHIST=100')

View File

@@ -1,3 +1,4 @@
import pytest
import os import os
import subprocess import subprocess
import shutil import shutil
@@ -5,39 +6,50 @@ from tempfile import mkdtemp
from pathlib import Path from pathlib import Path
import sys import sys
import pexpect import pexpect
import pytest from tests.utils import root
root = str(Path(__file__).parent.parent.parent.resolve())
bare = os.environ.get('BARE') bare = os.environ.get('BARE')
enabled = os.environ.get('FUNCTIONAL') enabled = os.environ.get('FUNCTIONAL')
def build_container(tag, dockerfile): def build_container(tag, dockerfile, copy_src=False):
tmpdir = mkdtemp() tmpdir = mkdtemp()
try: try:
with Path(tmpdir).joinpath('Dockerfile').open('w') as file: if copy_src:
subprocess.call(['cp', '-a', str(root), tmpdir])
dockerfile_path = Path(tmpdir).joinpath('Dockerfile')
with dockerfile_path.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]) != 0:
cwd=root) != 0:
raise Exception("Can't build a container") raise Exception("Can't build a container")
finally: finally:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
def spawn(request, tag, dockerfile, cmd): def spawn(request, tag, dockerfile, cmd, install=True, copy_src=False):
if bare: if bare:
proc = pexpect.spawnu(cmd) proc = pexpect.spawnu(cmd)
else: else:
tag = 'thefuck/{}'.format(tag) tag = 'thefuck/{}'.format(tag)
build_container(tag, dockerfile) build_container(tag, dockerfile, copy_src)
proc = pexpect.spawnu('docker run --volume {}:/src --tty=true ' proc = pexpect.spawnu('docker run --rm=true --volume {}:/src --tty=true '
'--interactive=true {} {}'.format(root, tag, cmd)) '--interactive=true {} {}'.format(root, tag, cmd))
if install:
proc.sendline('pip install /src') proc.sendline('pip install /src')
proc.sendline('cd /') proc.sendline('cd /')
proc.logfile = sys.stdout proc.logfile = sys.stdout
request.addfinalizer(proc.terminate) def _finalizer():
proc.terminate()
if not bare:
container_id = subprocess.check_output(['docker', 'ps']) \
.decode('utf-8').split('\n')[-2].split()[0]
subprocess.check_call(['docker', 'kill', container_id])
request.addfinalizer(_finalizer)
return proc return proc

View File

@@ -0,0 +1,25 @@
import pytest
from thefuck.rules.apt_get_search import get_new_command, match
from tests.utils import Command
def test_match():
assert match(Command('apt-get search foo'), None)
@pytest.mark.parametrize('command', [
Command('apt-cache search foo'),
Command('aptitude search foo'),
Command('apt search foo'),
Command('apt-get install foo'),
Command('apt-get source foo'),
Command('apt-get clean'),
Command('apt-get remove'),
Command('apt-get update')
])
def test_not_match(command):
assert not match(command, None)
def test_get_new_command():
assert get_new_command(Command('apt-get search foo'), None) == 'apt-cache search foo'

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from thefuck.rules.brew_install import match, get_new_command from thefuck.rules.brew_install import match, get_new_command
from thefuck.rules.brew_install import brew_formulas from thefuck.rules.brew_install import _get_formulas
from tests.utils import Command from tests.utils import Command
@@ -20,9 +20,7 @@ def brew_already_installed():
def _is_not_okay_to_test(): def _is_not_okay_to_test():
if 'elasticsearch' not in brew_formulas: return 'elasticsearch' not in _get_formulas()
return True
return False
@pytest.mark.skipif(_is_not_okay_to_test(), @pytest.mark.skipif(_is_not_okay_to_test(),

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from thefuck.rules.brew_unknown_command import match, get_new_command from thefuck.rules.brew_unknown_command import match, get_new_command
from thefuck.rules.brew_unknown_command import brew_commands from thefuck.rules.brew_unknown_command import _brew_commands
from tests.utils import Command from tests.utils import Command
@@ -16,7 +16,7 @@ def brew_unknown_cmd2():
def test_match(brew_unknown_cmd): def test_match(brew_unknown_cmd):
assert match(Command('brew inst', stderr=brew_unknown_cmd), None) assert match(Command('brew inst', stderr=brew_unknown_cmd), None)
for command in brew_commands: for command in _brew_commands():
assert not match(Command('brew ' + command), None) assert not match(Command('brew ' + command), None)
@@ -24,5 +24,6 @@ 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', 'brew install', 'brew uninstall'] None) == ['brew list', 'brew install', 'brew uninstall']
assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd2), cmds = get_new_command(Command('brew instaa', stderr=brew_unknown_cmd2), None)
None) == ['brew install', 'brew uninstall', 'brew list'] assert 'brew install' in cmds
assert 'brew uninstall' in cmds

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from mock import Mock
from thefuck.rules.lein_not_task import match, get_new_command from thefuck.rules.lein_not_task import match, get_new_command
from tests.utils import Command
@pytest.fixture @pytest.fixture
@@ -14,10 +14,10 @@ Did you mean this?
def test_match(is_not_task): def test_match(is_not_task):
assert match(Mock(script='lein rpl', stderr=is_not_task), None) assert match(Command(script='lein rpl', stderr=is_not_task), None)
assert not match(Mock(script='ls', stderr=is_not_task), None) assert not match(Command(script='ls', stderr=is_not_task), None)
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(Command(script='lein rpl --help', stderr=is_not_task),
None) == ['lein repl --help', 'lein jar --help'] None) == ['lein repl --help', 'lein jar --help']

View File

@@ -1,16 +1,16 @@
from mock import patch, Mock
from thefuck.rules.ls_lah import match, get_new_command from thefuck.rules.ls_lah import match, get_new_command
from tests.utils import Command
def test_match(): def test_match():
assert match(Mock(script='ls'), None) assert match(Command(script='ls'), None)
assert match(Mock(script='ls file.py'), None) assert match(Command(script='ls file.py'), None)
assert match(Mock(script='ls /opt'), None) assert match(Command(script='ls /opt'), None)
assert not match(Mock(script='ls -lah /opt'), None) assert not match(Command(script='ls -lah /opt'), None)
assert not match(Mock(script='pacman -S binutils'), None) assert not match(Command(script='pacman -S binutils'), None)
assert not match(Mock(script='lsof'), None) assert not match(Command(script='lsof'), None)
def test_get_new_command(): def test_get_new_command():
assert get_new_command(Mock(script='ls file.py'), None) == 'ls -lah file.py' assert get_new_command(Command(script='ls file.py'), None) == 'ls -lah file.py'
assert get_new_command(Mock(script='ls'), None) == 'ls -lah' assert get_new_command(Command(script='ls'), None) == 'ls -lah'

View File

@@ -0,0 +1,40 @@
import pytest
from thefuck.rules.mvn_no_command import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='mvn clean', stdout="""
[INFO] Scanning for projects...[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building test 0.2
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ test ---
[INFO] Deleting /home/mlk/code/test/target
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.477s
[INFO] Finished at: Wed Aug 26 13:05:47 BST 2015
[INFO] Final Memory: 6M/240M
[INFO] ------------------------------------------------------------------------
"""),
Command(script='mvn --help'),
Command(script='mvn -v')
])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package', 'mvn clean install']),
(Command(script='mvn -N', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn -N clean package', 'mvn -N clean install'])])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,40 @@
import pytest
from thefuck.rules.mvn_unknown_lifecycle_phase import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='mvn clean', stdout="""
[INFO] Scanning for projects...[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building test 0.2
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ test ---
[INFO] Deleting /home/mlk/code/test/target
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.477s
[INFO] Finished at: Wed Aug 26 13:05:47 BST 2015
[INFO] Final Memory: 6M/240M
[INFO] ------------------------------------------------------------------------
"""),
Command(script='mvn --help'),
Command(script='mvn -v')
])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean', 'mvn compile']),
(Command(script='mvn claen package', stdout='[ERROR] Unknown lifecycle phase "claen". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package'])])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -29,7 +29,7 @@ def test_match(command):
@pytest.mark.parametrize('command, return_value', [ @pytest.mark.parametrize('command, return_value', [
(Command(script='vim', stderr='vim: command not found'), PKGFILE_OUTPUT_VIM), (Command(script='vim', stderr='vim: command not found'), PKGFILE_OUTPUT_VIM),
(Command(script='sudo vim', stderr='sudo: vim: command not found'), PKGFILE_OUTPUT_VIM)]) (Command(script='sudo vim', stderr='sudo: vim: command not found'), PKGFILE_OUTPUT_VIM)])
@patch('thefuck.archlinux.subprocess') @patch('thefuck.specific.archlinux.subprocess')
@patch.multiple(pacman, create=True, pacman=pacman_cmd) @patch.multiple(pacman, create=True, pacman=pacman_cmd)
def test_match_mocked(subp_mock, command, return_value): def test_match_mocked(subp_mock, command, return_value):
subp_mock.check_output.return_value = return_value subp_mock.check_output.return_value = return_value
@@ -75,7 +75,7 @@ def test_get_new_command(command, new_command, mocker):
(Command('convert'), ['{} -S extra/imagemagick && convert'.format(pacman_cmd)], PKGFILE_OUTPUT_CONVERT), (Command('convert'), ['{} -S extra/imagemagick && convert'.format(pacman_cmd)], PKGFILE_OUTPUT_CONVERT),
(Command('sudo'), ['{} -S core/sudo && sudo'.format(pacman_cmd)], PKGFILE_OUTPUT_SUDO), (Command('sudo'), ['{} -S core/sudo && sudo'.format(pacman_cmd)], PKGFILE_OUTPUT_SUDO),
(Command('sudo convert'), ['{} -S extra/imagemagick && sudo convert'.format(pacman_cmd)], PKGFILE_OUTPUT_CONVERT)]) (Command('sudo convert'), ['{} -S extra/imagemagick && sudo convert'.format(pacman_cmd)], PKGFILE_OUTPUT_CONVERT)])
@patch('thefuck.archlinux.subprocess') @patch('thefuck.specific.archlinux.subprocess')
@patch.multiple(pacman, create=True, pacman=pacman_cmd) @patch.multiple(pacman, create=True, pacman=pacman_cmd)
def test_get_new_command_mocked(subp_mock, command, new_command, return_value): def test_get_new_command_mocked(subp_mock, command, new_command, return_value):
subp_mock.check_output.return_value = return_value subp_mock.check_output.return_value = return_value

View File

@@ -22,7 +22,7 @@ def test_match(command):
Command(script='yaourt -S llc', stderr='error: target not found: llc'), Command(script='yaourt -S llc', stderr='error: target not found: llc'),
Command(script='pacman llc', stderr='error: target not found: llc'), Command(script='pacman llc', stderr='error: target not found: llc'),
Command(script='sudo pacman llc', stderr='error: target not found: llc')]) Command(script='sudo pacman llc', stderr='error: target not found: llc')])
@patch('thefuck.archlinux.subprocess') @patch('thefuck.specific.archlinux.subprocess')
def test_match_mocked(subp_mock, command): def test_match_mocked(subp_mock, command):
subp_mock.check_output.return_value = PKGFILE_OUTPUT_LLC subp_mock.check_output.return_value = PKGFILE_OUTPUT_LLC
assert match(command, None) assert match(command, None)
@@ -42,7 +42,7 @@ def test_get_new_command(command, fixed):
(Command(script='yaourt -S llc', stderr='error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']), (Command(script='yaourt -S llc', stderr='error: target not found: llc'), ['yaourt -S extra/llvm', 'yaourt -S extra/llvm35']),
(Command(script='pacman -S llc', stderr='error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']), (Command(script='pacman -S llc', stderr='error: target not found: llc'), ['pacman -S extra/llvm', 'pacman -S extra/llvm35']),
(Command(script='sudo pacman -S llc', stderr='error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])]) (Command(script='sudo pacman -S llc', stderr='error: target not found: llc'), ['sudo pacman -S extra/llvm', 'sudo pacman -S extra/llvm35'])])
@patch('thefuck.archlinux.subprocess') @patch('thefuck.specific.archlinux.subprocess')
def test_get_new_command_mocked(subp_mock, command, fixed): def test_get_new_command_mocked(subp_mock, command, fixed):
subp_mock.check_output.return_value = PKGFILE_OUTPUT_LLC subp_mock.check_output.return_value = PKGFILE_OUTPUT_LLC
assert get_new_command(command, None) == fixed assert get_new_command(command, None) == fixed

View File

View File

@@ -0,0 +1,29 @@
import pytest
from thefuck.specific.git import git_support
from tests.utils import Command
@pytest.mark.parametrize('called, command, stderr', [
('git co', 'git checkout', "19:22:36.299340 git.c:282 trace: alias expansion: co => 'checkout'"),
('git com file', 'git commit --verbose file',
"19:23:25.470911 git.c:282 trace: alias expansion: com => 'commit' '--verbose'")])
def test_git_support(called, command, stderr):
@git_support
def fn(command, settings): return command.script
assert fn(Command(script=called, stderr=stderr), None) == command
@pytest.mark.parametrize('command, is_git', [
('git pull', True),
('hub pull', True),
('git push --set-upstream origin foo', True),
('hub push --set-upstream origin foo', True),
('ls', False),
('cat git', False),
('cat hub', False)])
def test_git_support_match(command, is_git):
@git_support
def fn(command, settings): return True
assert fn(Command(script=command), None) == is_git

View File

@@ -0,0 +1,20 @@
import pytest
from mock import Mock
from thefuck.specific.sudo import sudo_support
from tests.utils import Command
@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),
(False, 'ls', 'ls', False)])
def test_sudo_support(return_value, command, called, result):
def fn(command, settings):
assert command == Command(called)
return return_value
assert sudo_support(fn)(Command(command), None) == result

View File

@@ -3,7 +3,7 @@ from pathlib import PosixPath, Path
from mock import Mock from mock import Mock
from thefuck import corrector, conf, types from thefuck import corrector, conf, types
from tests.utils import Rule, Command, CorrectedCommand from tests.utils import Rule, Command, CorrectedCommand
from thefuck.corrector import make_corrected_commands, get_corrected_commands, remove_duplicates from thefuck.corrector import make_corrected_commands, get_corrected_commands
def test_load_rule(mocker): def test_load_rule(mocker):
@@ -41,49 +41,37 @@ class TestGetRules(object):
rules) rules)
class TestGetMatchedRules(object): class TestIsRuleMatch(object):
def test_no_match(self): def test_no_match(self, settings):
assert list(corrector.get_matched_rules( assert not corrector.is_rule_match(
Command('ls'), [Rule('', lambda *_: False)], Command('ls'), Rule('', lambda *_: False), settings)
Mock(no_colors=True))) == []
def test_match(self): def test_match(self, settings):
rule = Rule('', lambda x, _: x.script == 'cd ..') rule = Rule('', lambda x, _: x.script == 'cd ..')
assert list(corrector.get_matched_rules( assert corrector.is_rule_match(Command('cd ..'), rule, settings)
Command('cd ..'), [rule], Mock(no_colors=True))) == [rule]
def test_when_rule_failed(self, capsys): def test_when_rule_failed(self, capsys, settings):
all(corrector.get_matched_rules( rule = Rule('test', Mock(side_effect=OSError('Denied')),
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')), requires_output=False)
requires_output=False)], assert not corrector.is_rule_match(
Mock(no_colors=True, debug=False))) Command('ls'), rule, settings)
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:' assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
class TestGetCorrectedCommands(object): class TestMakeCorrectedCommands(object):
def test_with_rule_returns_list(self): def test_with_rule_returns_list(self):
rule = Rule(get_new_command=lambda x, _: [x.script + '!', x.script + '@'], rule = Rule(get_new_command=lambda x, _: [x.script + '!', x.script + '@'],
priority=100) priority=100)
assert list(make_corrected_commands(Command(script='test'), [rule], None)) \ assert list(make_corrected_commands(Command(script='test'), rule, None)) \
== [CorrectedCommand(script='test!', priority=100), == [CorrectedCommand(script='test!', priority=100),
CorrectedCommand(script='test@', priority=200)] CorrectedCommand(script='test@', priority=200)]
def test_with_rule_returns_command(self): def test_with_rule_returns_command(self):
rule = Rule(get_new_command=lambda x, _: x.script + '!', rule = Rule(get_new_command=lambda x, _: x.script + '!',
priority=100) priority=100)
assert list(make_corrected_commands(Command(script='test'), [rule], None)) \ assert list(make_corrected_commands(Command(script='test'), rule, None)) \
== [CorrectedCommand(script='test!', priority=100)] == [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): def test_get_corrected_commands(mocker):
command = Command('test', 'test', 'test') command = Command('test', 'test', 'test')
rules = [Rule(match=lambda *_: False), rules = [Rule(match=lambda *_: False),
@@ -94,4 +82,4 @@ def test_get_corrected_commands(mocker):
priority=60)] priority=60)]
mocker.patch('thefuck.corrector.get_rules', return_value=rules) mocker.patch('thefuck.corrector.get_rules', return_value=rules)
assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \ assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \
== ['test@', 'test!', 'test;'] == ['test!', 'test@', 'test;']

View File

@@ -1,12 +1,11 @@
from pathlib import Path from tests.utils import root
def test_readme(): def test_readme():
project_root = Path(__file__).parent.parent with root.joinpath('README.md').open() as f:
with project_root.joinpath('README.md').open() as f:
readme = f.read() readme = f.read()
bundled = project_root \ bundled = root \
.joinpath('thefuck') \ .joinpath('thefuck') \
.joinpath('rules') \ .joinpath('rules') \
.glob('*.py') .glob('*.py')

View File

@@ -1,5 +1,6 @@
from thefuck.types import RulesNamesList, Settings from thefuck.types import RulesNamesList, Settings, \
from tests.utils import Rule SortedCorrectedCommandsSequence
from tests.utils import Rule, CorrectedCommand
def test_rules_names_list(): def test_rules_names_list():
@@ -15,3 +16,47 @@ def test_update_settings():
assert new_settings.key == 'val' assert new_settings.key == 'val'
assert new_settings.unset == 'unset-value' assert new_settings.unset == 'unset-value'
assert settings.key == 'val' assert settings.key == 'val'
class TestSortedCorrectedCommandsSequence(object):
def test_realises_generator_only_on_demand(self, settings):
should_realise = False
def gen():
yield CorrectedCommand('git commit')
yield CorrectedCommand('git branch', priority=200)
assert should_realise
yield CorrectedCommand('git checkout', priority=100)
commands = SortedCorrectedCommandsSequence(gen(), settings)
assert commands[0] == CorrectedCommand('git commit')
should_realise = True
assert commands[1] == CorrectedCommand('git checkout', priority=100)
assert commands[2] == CorrectedCommand('git branch', priority=200)
def test_remove_duplicates(self, settings):
side_effect = lambda *_: None
seq = SortedCorrectedCommandsSequence(
iter([CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', priority=200),
CorrectedCommand('ls', side_effect, 300)]),
settings)
assert set(seq) == {CorrectedCommand('ls', priority=100),
CorrectedCommand('ls', side_effect, 300)}
def test_with_blank(self, settings):
seq = SortedCorrectedCommandsSequence(iter([]), settings)
assert list(seq) == []
class TestCorrectedCommand(object):
def test_equality(self):
assert CorrectedCommand('ls', None, 100) == \
CorrectedCommand('ls', None, 200)
assert CorrectedCommand('ls', None, 100) != \
CorrectedCommand('ls', lambda *_: _, 100)
def test_hashable(self):
assert {CorrectedCommand('ls', None, 100),
CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')}

View File

@@ -4,7 +4,7 @@ from mock import Mock
import pytest import pytest
from itertools import islice from itertools import islice
from thefuck import ui from thefuck import ui
from thefuck.types import CorrectedCommand from thefuck.types import CorrectedCommand, SortedCorrectedCommandsSequence
@pytest.fixture @pytest.fixture
@@ -58,14 +58,18 @@ def test_command_selector():
class TestSelectCommand(object): class TestSelectCommand(object):
@pytest.fixture @pytest.fixture
def commands_with_side_effect(self): def commands_with_side_effect(self, settings):
return [CorrectedCommand('ls', lambda *_: None, 100), return SortedCorrectedCommandsSequence(
CorrectedCommand('cd', lambda *_: None, 100)] iter([CorrectedCommand('ls', lambda *_: None, 100),
CorrectedCommand('cd', lambda *_: None, 100)]),
settings)
@pytest.fixture @pytest.fixture
def commands(self): def commands(self, settings):
return [CorrectedCommand('ls', None, 100), return SortedCorrectedCommandsSequence(
CorrectedCommand('cd', None, 100)] iter([CorrectedCommand('ls', None, 100),
CorrectedCommand('cd', None, 100)]),
settings)
def test_without_commands(self, capsys): def test_without_commands(self, capsys):
assert ui.select_command([], Mock(debug=False, no_color=True)) is None assert ui.select_command([], Mock(debug=False, no_color=True)) is None
@@ -92,13 +96,6 @@ class TestSelectCommand(object):
require_confirmation=True)) == commands[0] require_confirmation=True)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') 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): def test_with_confirmation_abort(self, capsys, patch_getch, commands):
patch_getch([KeyboardInterrupt]) patch_getch([KeyboardInterrupt])
assert ui.select_command(commands, assert ui.select_command(commands,

View File

@@ -1,8 +1,9 @@
import pytest import pytest
from mock import Mock from mock import Mock
from thefuck.utils import git_support, sudo_support, wrap_settings,\ import six
from thefuck.utils import wrap_settings, \
memoize, get_closest, get_all_executables, replace_argument, \ memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands get_all_matched_commands, is_app, for_app, cache
from thefuck.types import Settings from thefuck.types import Settings
from tests.utils import Command from tests.utils import Command
@@ -16,43 +17,6 @@ def test_wrap_settings(override, old, new):
assert wrap_settings(override)(fn)(None, Settings(old)) == new assert wrap_settings(override)(fn)(None, Settings(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),
(False, 'ls', 'ls', False)])
def test_sudo_support(return_value, command, called, result):
fn = Mock(return_value=return_value, __name__='')
assert sudo_support(fn)(Command(command), None) == result
fn.assert_called_once_with(Command(called), None)
@pytest.mark.parametrize('called, command, stderr', [
('git co', 'git checkout', "19:22:36.299340 git.c:282 trace: alias expansion: co => 'checkout'"),
('git com file', 'git commit --verbose file', "19:23:25.470911 git.c:282 trace: alias expansion: com => 'commit' '--verbose'")])
def test_git_support(called, command, stderr):
@git_support
def fn(command, settings): return command.script
assert fn(Command(script=called, stderr=stderr), None) == command
@pytest.mark.parametrize('command, is_git', [
('git pull', True),
('hub pull', True),
('git push --set-upstream origin foo', True),
('hub push --set-upstream origin foo', True),
('ls', False),
('cat git', False),
('cat hub', False)])
def test_git_support_match(command, is_git):
@git_support
def fn(command, settings): return True
assert fn(Command(script=command), None) == is_git
def test_memoize(): def test_memoize():
fn = Mock(__name__='fn') fn = Mock(__name__='fn')
memoized = memoize(fn) memoized = memoize(fn)
@@ -71,7 +35,6 @@ def test_no_memoize():
class TestGetClosest(object): class TestGetClosest(object):
def test_when_can_match(self): def test_when_can_match(self):
assert 'branch' == get_closest('brnch', ['branch', 'status']) assert 'branch' == get_closest('brnch', ['branch', 'status'])
@@ -131,3 +94,89 @@ def test_replace_argument(args, result):
'service-status', 'service-unbind'])]) 'service-status', 'service-unbind'])])
def test_get_all_matched_commands(stderr, result): def test_get_all_matched_commands(stderr, result):
assert list(get_all_matched_commands(stderr)) == result assert list(get_all_matched_commands(stderr)) == result
@pytest.mark.usefixtures('no_memoize')
@pytest.mark.parametrize('script, names, result', [
('git diff', ['git', 'hub'], True),
('hub diff', ['git', 'hub'], True),
('hg diff', ['git', 'hub'], False)])
def test_is_app(script, names, result):
assert is_app(Command(script), *names) == result
@pytest.mark.usefixtures('no_memoize')
@pytest.mark.parametrize('script, names, result', [
('git diff', ['git', 'hub'], True),
('hub diff', ['git', 'hub'], True),
('hg diff', ['git', 'hub'], False)])
def test_for_app(script, names, result):
@for_app(*names)
def match(command, settings):
return True
assert match(Command(script), None) == result
class TestCache(object):
@pytest.fixture(autouse=True)
def enable_cache(self, monkeypatch):
monkeypatch.setattr('thefuck.utils.cache.disabled', False)
@pytest.fixture
def shelve(self, mocker):
value = {}
class _Shelve(object):
def __init__(self, path):
pass
def __setitem__(self, k, v):
value[k] = v
def __getitem__(self, k):
return value[k]
def get(self, k, v=None):
return value.get(k, v)
def close(self):
return
mocker.patch('thefuck.utils.shelve.open', new_callable=lambda: _Shelve)
return value
@pytest.fixture(autouse=True)
def mtime(self, mocker):
mocker.patch('thefuck.utils.os.path.getmtime', return_value=0)
@pytest.fixture
def fn(self):
@cache('~/.bashrc')
def fn():
return 'test'
return fn
@pytest.fixture
def key(self):
if six.PY3:
return 'tests.test_utils.<function TestCache.fn.<locals>.fn '
else:
return 'tests.test_utils.<function fn '
def test_with_blank_cache(self, shelve, fn, key):
assert shelve == {}
assert fn() == 'test'
assert shelve == {key: {'etag': '0', 'value': 'test'}}
def test_with_filled_cache(self, shelve, fn, key):
cache_value = {key: {'etag': '0', 'value': 'new-value'}}
shelve.update(cache_value)
assert fn() == 'new-value'
assert shelve == cache_value
def test_when_etag_changed(self, shelve, fn, key):
shelve.update({key: {'etag': '-1', 'value': 'old-value'}})
assert fn() == 'test'
assert shelve == {key: {'etag': '0', 'value': 'test'}}

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from thefuck import types from thefuck import types
from thefuck.conf import DEFAULT_PRIORITY from thefuck.conf import DEFAULT_PRIORITY
@@ -19,3 +20,6 @@ def Rule(name='', match=lambda *_: True,
def CorrectedCommand(script='', side_effect=None, priority=DEFAULT_PRIORITY): def CorrectedCommand(script='', side_effect=None, priority=DEFAULT_PRIORITY):
return types.CorrectedCommand(script, side_effect, priority) return types.CorrectedCommand(script, side_effect, priority)
root = Path(__file__).parent.parent.resolve()

View File

@@ -2,12 +2,12 @@ import sys
from imp import load_source from imp import load_source
from pathlib import Path from pathlib import Path
from . import conf, types, logs from . import conf, types, logs
from .utils import eager
def load_rule(rule, settings): def load_rule(rule, settings):
"""Imports rule module and returns it.""" """Imports rule module and returns it."""
name = rule.name[:-3] name = rule.name[:-3]
with logs.debug_time(u'Importing rule: {};'.format(name), settings):
rule_module = load_source(name, str(rule)) rule_module = load_source(name, str(rule))
priority = getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY) priority = getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY)
return types.Rule(name, rule_module.match, return types.Rule(name, rule_module.match,
@@ -27,61 +27,45 @@ def get_loaded_rules(rules, settings):
yield loaded_rule yield loaded_rule
@eager
def get_rules(user_dir, settings): def get_rules(user_dir, settings):
"""Returns all enabled rules.""" """Returns all enabled rules."""
bundled = Path(__file__).parent \ bundled = Path(__file__).parent \
.joinpath('rules') \ .joinpath('rules') \
.glob('*.py') .glob('*.py')
user = user_dir.joinpath('rules').glob('*.py') user = user_dir.joinpath('rules').glob('*.py')
return get_loaded_rules(sorted(bundled) + sorted(user), settings) return sorted(get_loaded_rules(sorted(bundled) + sorted(user), settings),
key=lambda rule: rule.priority)
@eager def is_rule_match(command, rule, settings):
def get_matched_rules(command, rules, settings):
"""Returns first matched rule for command.""" """Returns first matched rule for command."""
script_only = command.stdout is None and command.stderr is None script_only = command.stdout is None and command.stderr is None
for rule in rules:
if script_only and rule.requires_output: if script_only and rule.requires_output:
continue return False
try: try:
with logs.debug_time(u'Trying rule: {};'.format(rule.name), with logs.debug_time(u'Trying rule: {};'.format(rule.name),
settings): settings):
if rule.match(command, settings): if rule.match(command, settings):
yield rule return True
except Exception: except Exception:
logs.rule_failed(rule, sys.exc_info(), settings) logs.rule_failed(rule, sys.exc_info(), settings)
def make_corrected_commands(command, rules, settings): def make_corrected_commands(command, rule, settings):
for rule in rules:
new_commands = rule.get_new_command(command, settings) new_commands = rule.get_new_command(command, settings)
if not isinstance(new_commands, list): if not isinstance(new_commands, list):
new_commands = [new_commands] new_commands = (new_commands,)
for n, new_command in enumerate(new_commands): for n, new_command in enumerate(new_commands):
yield types.CorrectedCommand(script=new_command, yield types.CorrectedCommand(script=new_command,
side_effect=rule.side_effect, side_effect=rule.side_effect,
priority=(n + 1) * rule.priority) 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): def get_corrected_commands(command, user_dir, settings):
rules = get_rules(user_dir, settings) corrected_commands = (
logs.debug( corrected for rule in get_rules(user_dir, settings)
u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), if is_rule_match(command, rule, settings)
settings) for corrected in make_corrected_commands(command, rule, settings))
matched = get_matched_rules(command, rules, settings) return types.SortedCorrectedCommandsSequence(corrected_commands, 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

@@ -45,15 +45,11 @@ def show_corrected_command(corrected_command, settings):
reset=color(colorama.Style.RESET_ALL, settings))) reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_text(corrected_command, multiple_cmds, settings): def confirm_text(corrected_command, settings):
if multiple_cmds:
arrows = '{blue}{reset}/{blue}{reset}/'
else:
arrows = ''
sys.stderr.write( sys.stderr.write(
('{clear}{bold}{script}{reset}{side_effect} ' ('{clear}{bold}{script}{reset}{side_effect} '
'[{green}enter{reset}/' + arrows + '{red}ctrl+c{reset}]').format( '[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
'/{red}ctrl+c{reset}]').format(
script=corrected_command.script, script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '', side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r', clear='\033[1K\r',

View File

@@ -117,7 +117,7 @@ def print_alias(entry_point=True):
def main(): def main():
parser = ArgumentParser(prog='The Fuck') parser = ArgumentParser(prog='thefuck')
parser.add_argument('-v', '--version', parser.add_argument('-v', '--version',
action='version', action='version',
version='%(prog)s {}'.format( version='%(prog)s {}'.format(

View File

@@ -0,0 +1,11 @@
import re
from thefuck.utils import for_app
@for_app('apt-get')
def match(command, settings):
return command.script.startswith('apt-get search')
def get_new_command(command, settings):
return re.sub(r'^apt-get', 'apt-cache', command.script)

View File

@@ -1,38 +1,38 @@
import os import os
import re import re
from subprocess import check_output from thefuck.utils import get_closest, replace_argument, which
from thefuck.utils import get_closest, replace_argument from thefuck.specific.brew import get_brew_path_prefix
# Formulars are base on each local system's status
brew_formulas = [] enabled_by_default = bool(which('brew'))
def _get_formulas():
# Formulas are based on each local system's status
try: try:
brew_path_prefix = check_output(['brew', '--prefix'], brew_path_prefix = get_brew_path_prefix()
universal_newlines=True).strip()
brew_formula_path = brew_path_prefix + '/Library/Formula' brew_formula_path = brew_path_prefix + '/Library/Formula'
for file_name in os.listdir(brew_formula_path): for file_name in os.listdir(brew_formula_path):
if file_name.endswith('.rb'): if file_name.endswith('.rb'):
brew_formulas.append(file_name.replace('.rb', '')) yield file_name[:-3]
except: except:
pass pass
def _get_similar_formula(formula_name): def _get_similar_formula(formula_name):
return get_closest(formula_name, brew_formulas, 1, 0.85) return get_closest(formula_name, _get_formulas(), 1, 0.85)
def match(command, settings): def match(command, settings):
is_proper_command = ('brew install' in command.script and is_proper_command = ('brew install' in command.script and
'No available formula' in command.stderr) 'No available formula' in command.stderr)
has_possible_formulas = False
if is_proper_command: if is_proper_command:
formula = re.findall(r'Error: No available formula for ([a-z]+)', formula = re.findall(r'Error: No available formula for ([a-z]+)',
command.stderr)[0] command.stderr)[0]
has_possible_formulas = bool(_get_similar_formula(formula)) return bool(_get_similar_formula(formula))
return False
return has_possible_formulas
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,31 +1,20 @@
import os import os
import re import re
import subprocess
from thefuck.utils import get_closest, replace_command from thefuck.utils import get_closest, replace_command
from thefuck.specific.brew import get_brew_path_prefix
BREW_CMD_PATH = '/Library/Homebrew/cmd' BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps' TAP_PATH = '/Library/Taps'
TAP_CMD_PATH = '/%s/%s/cmd' TAP_CMD_PATH = '/%s/%s/cmd'
def _get_brew_path_prefix():
"""To get brew path"""
try:
return subprocess.check_output(['brew', '--prefix'],
universal_newlines=True).strip()
except:
return None
def _get_brew_commands(brew_path_prefix): def _get_brew_commands(brew_path_prefix):
"""To get brew default commands on local environment""" """To get brew default commands on local environment"""
brew_cmd_path = brew_path_prefix + BREW_CMD_PATH brew_cmd_path = brew_path_prefix + BREW_CMD_PATH
commands = [name.replace('.rb', '') for name in os.listdir(brew_cmd_path) return [name[:-3] for name in os.listdir(brew_cmd_path)
if name.endswith('.rb')] if name.endswith('.rb')]
return commands
def _get_brew_tap_specific_commands(brew_path_prefix): def _get_brew_tap_specific_commands(brew_path_prefix):
"""To get tap's specific commands """To get tap's specific commands
@@ -51,10 +40,7 @@ def _get_brew_tap_specific_commands(brew_path_prefix):
def _is_brew_tap_cmd_naming(name): def _is_brew_tap_cmd_naming(name):
if name.startswith('brew-') and name.endswith('.rb'): return name.startswith('brew-') and name.endswith('.rb')
return True
return False
def _get_directory_names_only(path): def _get_directory_names_only(path):
@@ -62,35 +48,33 @@ def _get_directory_names_only(path):
if os.path.isdir(os.path.join(path, d))] if os.path.isdir(os.path.join(path, d))]
brew_path_prefix = _get_brew_path_prefix() def _brew_commands():
brew_path_prefix = get_brew_path_prefix()
# Failback commands for testing (Based on Homebrew 0.9.5)
brew_commands = ['info', 'home', 'options', 'install', 'uninstall',
'search', 'list', 'update', 'upgrade', 'pin', 'unpin',
'doctor', 'create', 'edit']
if brew_path_prefix: if brew_path_prefix:
try: try:
brew_commands = _get_brew_commands(brew_path_prefix) \ return _get_brew_commands(brew_path_prefix) \
+ _get_brew_tap_specific_commands(brew_path_prefix) + _get_brew_tap_specific_commands(brew_path_prefix)
except OSError: except OSError:
pass pass
# Failback commands for testing (Based on Homebrew 0.9.5)
return ['info', 'home', 'options', 'install', 'uninstall',
'search', 'list', 'update', 'upgrade', 'pin', 'unpin',
'doctor', 'create', 'edit']
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)
has_possible_commands = False
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_closest(broken_cmd, brew_commands)) return bool(get_closest(broken_cmd, _brew_commands()))
return False
return has_possible_commands
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]
return replace_command(command, broken_cmd, brew_commands) return replace_command(command, broken_cmd, _brew_commands())

View File

@@ -1,10 +1,10 @@
import re import re
from thefuck.utils import replace_argument from thefuck.utils import replace_argument, for_app
@for_app('cargo')
def match(command, settings): def match(command, settings):
return ('cargo' in command.script return ('No such subcommand' in command.stderr
and 'No such subcommand' in command.stderr
and 'Did you mean' in command.stderr) and 'Did you mean' in command.stderr)

View File

@@ -2,8 +2,9 @@
import os import os
from difflib import get_close_matches from difflib import get_close_matches
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir from thefuck.rules import cd_mkdir
from thefuck.utils import for_app
__author__ = "mmussomele" __author__ = "mmussomele"
@@ -16,6 +17,7 @@ def _get_sub_dirs(parent):
@sudo_support @sudo_support
@for_app('cd')
def match(command, settings): def match(command, settings):
"""Match function copied from cd_mkdir.py""" """Match function copied from cd_mkdir.py"""
return (command.script.startswith('cd ') return (command.script.startswith('cd ')

View File

@@ -1,12 +1,13 @@
import re import re
from thefuck import shells from thefuck import shells
from thefuck.utils import sudo_support from thefuck.utils import for_app
from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support
@for_app('cd')
def match(command, settings): def match(command, settings):
return (command.script.startswith('cd ') return (('no such file or directory' in command.stderr.lower()
and ('no such file or directory' in command.stderr.lower()
or 'cd: can\'t cd to' in command.stderr.lower())) or 'cd: can\'t cd to' in command.stderr.lower()))

View File

@@ -1,10 +1,10 @@
import re import re
from thefuck.utils import replace_argument from thefuck.utils import replace_argument, for_app
@for_app('composer')
def match(command, settings): def match(command, settings):
return ('composer' in command.script return (('did you mean this?' in command.stderr.lower()
and ('did you mean this?' in command.stderr.lower()
or 'did you mean one of these?' in command.stderr.lower())) or 'did you mean one of these?' in command.stderr.lower()))

View File

@@ -1,12 +1,13 @@
import re import re
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app
@sudo_support @sudo_support
@for_app('cp')
def match(command, settings): def match(command, settings):
stderr = command.stderr.lower() stderr = command.stderr.lower()
return command.script.startswith('cp ') \ return 'omitting directory' in stderr or 'is a directory' in stderr
and ('omitting directory' in stderr or 'is a directory' in stderr)
@sudo_support @sudo_support

View File

@@ -1,8 +1,11 @@
from thefuck.utils import for_app
@for_app(['g++', 'clang++'])
def match(command, settings): def match(command, settings):
return (('g++' in command.script or 'clang++' in command.script) and return ('This file requires compiler and library support for the '
('This file requires compiler and library support for the '
'ISO C++ 2011 standard.' in command.stderr or 'ISO C++ 2011 standard.' in command.stderr or
'-Wc++11-extensions' in command.stderr)) '-Wc++11-extensions' in command.stderr)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,6 +1,7 @@
from thefuck import shells
import os import os
import tarfile import tarfile
from thefuck import shells
from thefuck.utils import for_app
def _is_tar_extract(cmd): def _is_tar_extract(cmd):
@@ -23,9 +24,9 @@ def _tar_file(cmd):
return (c, c[0:len(c) - len(ext)]) return (c, c[0:len(c) - len(ext)])
@for_app('tar')
def match(command, settings): def match(command, settings):
return (command.script.startswith('tar') return ('-C' not in command.script
and '-C' not in command.script
and _is_tar_extract(command.script) and _is_tar_extract(command.script)
and _tar_file(command.script) is not None) and _tar_file(command.script) is not None)

View File

@@ -1,5 +1,6 @@
import os import os
import zipfile import zipfile
from thefuck.utils import for_app
def _is_bad_zip(file): def _is_bad_zip(file):
@@ -20,9 +21,9 @@ def _zip_file(command):
return '{}.zip'.format(c) return '{}.zip'.format(c)
@for_app('unzip')
def match(command, settings): def match(command, settings):
return (command.script.startswith('unzip') return ('-d' not in command.script
and '-d' not in command.script
and _is_bad_zip(_zip_file(command))) and _is_bad_zip(_zip_file(command)))

View File

@@ -1,13 +1,14 @@
from itertools import dropwhile, takewhile, islice from itertools import dropwhile, takewhile, islice
import re import re
import subprocess import subprocess
from thefuck.utils import sudo_support, replace_command from thefuck.utils import replace_command, for_app
from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support
@for_app('docker')
def match(command, settings): def match(command, settings):
return command.script.startswith('docker') \ return 'is not a docker command' in command.stderr
and 'is not a docker command' in command.stderr
def get_docker_commands(): def get_docker_commands():

View File

@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
import re import re
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support

View File

@@ -35,17 +35,17 @@ patterns = (
# for the sake of readability do not use named groups above # for the sake of readability do not use named groups above
def _make_pattern(pattern): def _make_pattern(pattern):
pattern = pattern.replace('{file}', '(?P<file>[^:\n]+)') pattern = pattern.replace('{file}', '(?P<file>[^:\n]+)') \
pattern = pattern.replace('{line}', '(?P<line>[0-9]+)') .replace('{line}', '(?P<line>[0-9]+)') \
pattern = pattern.replace('{col}', '(?P<col>[0-9]+)') .replace('{col}', '(?P<col>[0-9]+)')
return re.compile(pattern, re.MULTILINE) return re.compile(pattern, re.MULTILINE)
patterns = [_make_pattern(p) for p in patterns] patterns = [_make_pattern(p).search for p in patterns]
@memoize @memoize
def _search(stderr): def _search(stderr):
for pattern in patterns: for pattern in patterns:
m = re.search(pattern, stderr) m = pattern(stderr)
if m and os.path.isfile(m.group('file')): if m and os.path.isfile(m.group('file')):
return m return m

View File

@@ -1,14 +1,15 @@
import re import re
from thefuck import utils, shells from thefuck import shells
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('did not match any file(s) known to git.' in command.stderr return ('did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" in command.stderr) and "Did you forget to 'git add'?" in command.stderr)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
missing_file = re.findall( missing_file = re.findall(
r"error: pathspec '([^']*)' " r"error: pathspec '([^']*)' "

View File

@@ -1,13 +1,13 @@
from thefuck import utils
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('branch -d' in command.script return ('branch -d' in command.script
and 'If you are sure you want to delete it' in command.stderr) and 'If you are sure you want to delete it' in command.stderr)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return replace_argument(command.script, '-d', '-D') return replace_argument(command.script, '-d', '-D')

View File

@@ -1,12 +1,13 @@
from thefuck import utils, shells from thefuck import shells
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
# catches "git branch list" in place of "git branch" # catches "git branch list" in place of "git branch"
return command.script.split()[1:] == 'branch list'.split() return command.script.split()[1:] == 'branch list'.split()
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return shells.and_('git branch --delete list', 'git branch') return shells.and_('git branch --delete list', 'git branch')

View File

@@ -2,9 +2,10 @@ import re
import subprocess import subprocess
from thefuck import shells, utils from thefuck import shells, utils
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('did not match any file(s) known to git.' in command.stderr return ('did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" not in command.stderr) and "Did you forget to 'git add'?" not in command.stderr)
@@ -23,7 +24,7 @@ def get_branches():
yield line.strip() yield line.strip()
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
missing_file = re.findall( missing_file = re.findall(
r"error: pathspec '([^']*)' " r"error: pathspec '([^']*)' "

View File

@@ -1,13 +1,13 @@
from thefuck import utils
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('diff' in command.script and return ('diff' in command.script and
'--staged' not in command.script) '--staged' not in command.script)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return replace_argument(command.script, 'diff', 'diff --staged') return replace_argument(command.script, 'diff', 'diff --staged')

View File

@@ -1,8 +1,9 @@
from thefuck import utils from thefuck import utils
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return (command.script.split()[1] == 'stash' return (command.script.split()[1] == 'stash'
and 'usage:' in command.stderr) and 'usage:' in command.stderr)
@@ -19,7 +20,7 @@ stash_commands = (
'show') 'show')
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
stash_cmd = command.script.split()[2] stash_cmd = command.script.split()[2]
fixed = utils.get_closest(stash_cmd, stash_commands, fallback_to_first=False) fixed = utils.get_closest(stash_cmd, stash_commands, fallback_to_first=False)

View File

@@ -1,6 +1,6 @@
import re import re
from thefuck.utils import (git_support, from thefuck.utils import get_all_matched_commands, replace_command
get_all_matched_commands, replace_command) from thefuck.specific.git import git_support
@git_support @git_support

View File

@@ -1,13 +1,14 @@
from thefuck import shells, utils from thefuck import shells
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('pull' in command.script return ('pull' in command.script
and 'set-upstream' in command.stderr) and 'set-upstream' in command.stderr)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
line = command.stderr.split('\n')[-3].strip() line = command.stderr.split('\n')[-3].strip()
branch = line.split(' ')[-1] branch = line.split(' ')[-1]

View File

@@ -1,13 +1,13 @@
from thefuck import utils
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('fatal: Not a git repository' in command.stderr return ('fatal: Not a git repository' in command.stderr
and "Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)." in command.stderr) and "Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)." in command.stderr)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return replace_argument(command.script, 'pull', 'clone') return replace_argument(command.script, 'pull', 'clone')

View File

@@ -1,12 +1,12 @@
from thefuck import utils from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('push' in command.script return ('push' in command.script
and 'set-upstream' in command.stderr) and 'set-upstream' in command.stderr)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return command.stderr.split('\n')[-3].strip() return command.stderr.split('\n')[-3].strip()

View File

@@ -1,8 +1,8 @@
from thefuck import utils
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('push' in command.script return ('push' in command.script
and '! [rejected]' in command.stderr and '! [rejected]' in command.stderr
@@ -10,7 +10,7 @@ def match(command, settings):
and 'Updates were rejected because the tip of your current branch is behind' in command.stderr) and 'Updates were rejected because the tip of your current branch is behind' in command.stderr)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return replace_argument(command.script, 'push', 'push --force') return replace_argument(command.script, 'push', 'push --force')

View File

@@ -1,8 +1,9 @@
from thefuck import utils, shells from thefuck import shells
from thefuck.utils import replace_argument from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
return ('push' in command.script return ('push' in command.script
and '! [rejected]' in command.stderr and '! [rejected]' in command.stderr
@@ -10,7 +11,7 @@ def match(command, settings):
and 'Updates were rejected because the tip of your current branch is behind' in command.stderr) and 'Updates were rejected because the tip of your current branch is behind' in command.stderr)
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return shells.and_(replace_argument(command.script, 'push', 'pull'), return shells.and_(replace_argument(command.script, 'push', 'pull'),
command.script) command.script)

View File

@@ -1,14 +1,15 @@
from thefuck import shells, utils from thefuck import shells
from thefuck.specific.git import git_support
@utils.git_support @git_support
def match(command, settings): def match(command, settings):
# catches "Please commit or stash them" and "Please, commit your changes or # catches "Please commit or stash them" and "Please, commit your changes or
# stash them before you can switch branches." # stash them before you can switch branches."
return 'or stash them' in command.stderr return 'or stash them' in command.stderr
@utils.git_support @git_support
def get_new_command(command, settings): def get_new_command(command, settings):
formatme = shells.and_('git stash', '{}') formatme = shells.and_('git stash', '{}')
return formatme.format(command.script) return formatme.format(command.script)

View File

@@ -1,3 +1,4 @@
from thefuck.utils import for_app
# Appends .go when compiling go files # Appends .go when compiling go files
# #
# Example: # Example:
@@ -5,6 +6,7 @@
# error: go run: no go files listed # error: go run: no go files listed
@for_app('go')
def match(command, settings): def match(command, settings):
return (command.script.startswith('go run ') return (command.script.startswith('go run ')
and not command.script.endswith('.go')) and not command.script.endswith('.go'))

View File

@@ -1,6 +1,9 @@
from thefuck.utils import for_app
@for_app('grep')
def match(command, settings): def match(command, settings):
return (command.script.startswith('grep') return 'is a directory' in command.stderr.lower()
and 'is a directory' in command.stderr.lower())
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,11 +1,11 @@
import re import re
import subprocess import subprocess
from thefuck.utils import replace_command from thefuck.utils import replace_command, for_app
@for_app('gulp')
def match(command, script): def match(command, script):
return command.script.startswith('gulp')\ return 'is not in your gulpfile' in command.stdout
and 'is not in your gulpfile' in command.stdout
def get_gulp_tasks(): def get_gulp_tasks():

View File

@@ -1,5 +1,5 @@
import os import os
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support

View File

@@ -1,10 +1,10 @@
import re import re
from thefuck.utils import replace_command from thefuck.utils import replace_command, for_app
@for_app('heroku')
def match(command, settings): def match(command, settings):
return command.script.startswith('heroku') and \ return 'is not a heroku command' in command.stderr and \
'is not a heroku command' in command.stderr and \
'Perhaps you meant' in command.stderr 'Perhaps you meant' in command.stderr

View File

@@ -1,13 +1,16 @@
# Fixes common java command mistake """Fixes common java command mistake
#
# Example: Example:
# > java foo.java > java foo.java
# Error: Could not find or load main class foo.java Error: Could not find or load main class foo.java
"""
from thefuck.utils import for_app
@for_app('java')
def match(command, settings): def match(command, settings):
return (command.script.startswith('java ') return command.script.endswith('.java')
and command.script.endswith('.java'))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,14 +1,17 @@
# Appends .java when compiling java files """Appends .java when compiling java files
#
# Example: Example:
# > javac foo > javac foo
# error: Class names, 'foo', are only accepted if annotation error: Class names, 'foo', are only accepted if annotation
# processing is explicitly requested processing is explicitly requested
"""
from thefuck.utils import for_app
@for_app('javac')
def match(command, settings): def match(command, settings):
return (command.script.startswith('javac ') return not command.script.endswith('.java')
and not command.script.endswith('.java'))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,9 +1,10 @@
import re import re
from thefuck.utils import sudo_support,\ from thefuck.utils import replace_command, get_all_matched_commands, for_app
replace_command, get_all_matched_commands from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support
@for_app('lein')
def match(command, settings): def match(command, settings):
return (command.script.startswith('lein') return (command.script.startswith('lein')
and "is not a task. See 'lein help'" in command.stderr and "is not a task. See 'lein help'" in command.stderr

View File

@@ -1,7 +1,9 @@
from thefuck.utils import for_app
@for_app('ls')
def match(command, settings): def match(command, settings):
return (command.script == 'ls' return 'ls -' not in command.script
or command.script.startswith('ls ')
and 'ls -' not in command.script)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,5 +1,5 @@
import re import re
from thefuck.utils import get_closest from thefuck.utils import get_closest, for_app
def extract_possibilities(command): def extract_possibilities(command):
@@ -12,14 +12,12 @@ def extract_possibilities(command):
return possib return possib
@for_app('hg')
def match(command, settings): def match(command, settings):
return (command.script.startswith('hg ') return ('hg: unknown command' in command.stderr
and ('hg: unknown command' in command.stderr
and '(did you mean one of ' in command.stderr and '(did you mean one of ' in command.stderr
or "hg: command '" in command.stderr or "hg: command '" in command.stderr
and "' is ambiguous:" in command.stderr and "' is ambiguous:" in command.stderr)
)
)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,5 +1,5 @@
import re import re
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support

View File

@@ -0,0 +1,11 @@
from thefuck.utils import for_app
@for_app('mvn')
def match(command, settings):
return 'No goals have been specified for this build' in command.stdout
def get_new_command(command, settings):
return [command.script + ' clean package',
command.script + ' clean install']

View File

@@ -0,0 +1,32 @@
from thefuck.utils import replace_command, for_app
from difflib import get_close_matches
import re
def _get_failed_lifecycle(command):
return re.search(r'\[ERROR\] Unknown lifecycle phase "(.+)"',
command.stdout)
def _getavailable_lifecycles(command):
return re.search(
r'Available lifecycle phases are: (.+) -> \[Help 1\]', command.stdout)
@for_app('mvn')
def match(command, settings):
failed_lifecycle = _get_failed_lifecycle(command)
available_lifecycles = _getavailable_lifecycles(command)
return available_lifecycles and failed_lifecycle
def get_new_command(command, settings):
failed_lifecycle = _get_failed_lifecycle(command)
available_lifecycles = _getavailable_lifecycles(command)
if available_lifecycles and failed_lifecycle:
selected_lifecycle = get_close_matches(
failed_lifecycle.group(1), available_lifecycles.group(1).split(", "),
3, 0.6)
return replace_command(command, failed_lifecycle.group(1), selected_lifecycle)
else:
return []

View File

@@ -1,5 +1,6 @@
from difflib import get_close_matches from difflib import get_close_matches
from thefuck.utils import sudo_support, get_all_executables from thefuck.utils import get_all_executables
from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support

View File

@@ -5,12 +5,12 @@
# The file ~/github.com does not exist. # The file ~/github.com does not exist.
# Perhaps you meant 'http://github.com'? # Perhaps you meant 'http://github.com'?
# #
from thefuck.utils import for_app
@for_app('open', 'xdg-open', 'gnome-open', 'kde-open')
def match(command, settings): def match(command, settings):
return (command.script.startswith(('open', 'xdg-open', 'gnome-open', 'kde-open')) return ('.com' in command.script
and (
'.com' in command.script
or '.net' in command.script or '.net' in command.script
or '.org' in command.script or '.org' in command.script
or '.ly' in command.script or '.ly' in command.script
@@ -19,7 +19,7 @@ def match(command, settings):
or '.edu' in command.script or '.edu' in command.script
or '.info' in command.script or '.info' in command.script
or '.me' in command.script or '.me' in command.script
or 'www.' in command.script)) or 'www.' in command.script)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,4 +1,4 @@
from thefuck.archlinux import archlinux_env, get_pkgfile from thefuck.specific.archlinux import get_pkgfile, archlinux_env
from thefuck import shells from thefuck import shells

View File

@@ -7,7 +7,7 @@ should be:
""" """
from thefuck.utils import replace_command from thefuck.utils import replace_command
from thefuck.archlinux import archlinux_env, get_pkgfile from thefuck.specific.archlinux import get_pkgfile, archlinux_env
def match(command, settings): def match(command, settings):

View File

@@ -1,7 +1,10 @@
import re import re
from thefuck.utils import replace_argument from thefuck.utils import replace_argument, for_app
from thefuck.specific.sudo import sudo_support
@sudo_support
@for_app('pip')
def match(command, settings): def match(command, settings):
return ('pip' in command.script and return ('pip' in command.script and
'unknown command' in command.stderr and 'unknown command' in command.stderr and

View File

@@ -1,4 +1,4 @@
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
# add 'python' suffix to the command if # add 'python' suffix to the command if
# 1) The script does not have execute permission or # 1) The script does not have execute permission or
# 2) is interpreted as shell script # 2) is interpreted as shell script

View File

@@ -3,11 +3,12 @@
# Example: # Example:
# > python foo # > python foo
# error: python: can't open file 'foo': [Errno 2] No such file or directory # error: python: can't open file 'foo': [Errno 2] No such file or directory
from thefuck.utils import for_app
@for_app('python')
def match(command, settings): def match(command, settings):
return (command.script.startswith('python ') return not command.script.endswith('.py')
and not command.script.endswith('.py'))
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,5 +1,5 @@
import re import re
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
@sudo_support @sudo_support

View File

@@ -1,5 +1,4 @@
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
enabled_by_default = False enabled_by_default = False

View File

@@ -1,10 +1,10 @@
import shlex import shlex
from thefuck.utils import quote from thefuck.utils import quote, for_app
@for_app('sed')
def match(command, settings): def match(command, settings):
return ('sed' in command.script return "unterminated `s' command" in command.stderr
and "unterminated `s' command" in command.stderr)
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -1,25 +1,23 @@
import re import re
from thefuck.utils import for_app
patterns = [ commands = ('ssh', 'scp')
r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!',
r'WARNING: POSSIBLE DNS SPOOFING DETECTED!',
r"Warning: the \S+ host key for '([^']+)' differs from the key for the IP address '([^']+)'",
]
offending_pattern = re.compile(
r'(?:Offending (?:key for IP|\S+ key)|Matching host key) in ([^:]+):(\d+)',
re.MULTILINE)
commands = ['ssh', 'scp']
@for_app(*commands)
def match(command, settings): def match(command, settings):
if not command.script: if not command.script:
return False return False
if not command.script.split()[0] in commands: if not command.script.startswith(commands):
return False return False
if not any([re.findall(pattern, command.stderr) for pattern in patterns]):
return False patterns = (
return True r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!',
r'WARNING: POSSIBLE DNS SPOOFING DETECTED!',
r"Warning: the \S+ host key for '([^']+)' differs from the key for the IP address '([^']+)'",
)
return any(re.findall(pattern, command.stderr) for pattern in patterns)
def get_new_command(command, settings): def get_new_command(command, settings):
@@ -27,6 +25,9 @@ def get_new_command(command, settings):
def side_effect(old_cmd, command, settings): def side_effect(old_cmd, command, settings):
offending_pattern = re.compile(
r'(?:Offending (?:key for IP|\S+ key)|Matching host key) in ([^:]+):(\d+)',
re.MULTILINE)
offending = offending_pattern.findall(old_cmd.stderr) offending = offending_pattern.findall(old_cmd.stderr)
for filepath, lineno in offending: for filepath, lineno in offending:
with open(filepath, 'r') as fh: with open(filepath, 'r') as fh:

View File

@@ -1,16 +1,17 @@
""" """
The confusion in systemctl's param order is massive. The confusion in systemctl's param order is massive.
""" """
from thefuck.utils import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app
@sudo_support @sudo_support
@for_app('systemctl')
def match(command, settings): def match(command, settings):
# Catches 'Unknown operation 'service'.' when executing systemctl with # Catches 'Unknown operation 'service'.' when executing systemctl with
# misordered arguments # misordered arguments
cmd = command.script.split() cmd = command.script.split()
return ('systemctl' in command.script and return ('Unknown operation \'' in command.stderr and
'Unknown operation \'' in command.stderr and
len(cmd) - cmd.index('systemctl') == 3) len(cmd) - cmd.index('systemctl') == 3)

View File

@@ -1,10 +1,10 @@
from thefuck.utils import replace_command
import re import re
from thefuck.utils import replace_command, for_app
@for_app('tmux')
def match(command, settings): def match(command, settings):
return ('tmux' in command.script return ('ambiguous command:' in command.stderr
and 'ambiguous command:' in command.stderr
and 'could be:' in command.stderr) and 'could be:' in command.stderr)

View File

@@ -1,9 +1,10 @@
from thefuck import shells from thefuck import shells
from thefuck.utils import for_app
@for_app('tsuru')
def match(command, settings): def match(command, settings):
return (command.script.startswith('tsuru') return ('not authenticated' in command.stderr
and 'not authenticated' in command.stderr
and 'session has expired' in command.stderr) and 'session has expired' in command.stderr)

View File

@@ -1,10 +1,10 @@
import re import re
from thefuck.utils import get_all_matched_commands, replace_command from thefuck.utils import get_all_matched_commands, replace_command, for_app
@for_app('tsuru')
def match(command, settings): def match(command, settings):
return (command.script.startswith('tsuru ') return (' is not a tsuru command. See "tsuru help".' in command.stderr
and ' is not a tsuru command. See "tsuru help".' in command.stderr
and '\nDid you mean?\n\t' in command.stderr) and '\nDid you mean?\n\t' in command.stderr)

View File

@@ -1,5 +1,5 @@
import re import re
from thefuck.utils import (replace_command, get_all_matched_commands) from thefuck.utils import replace_command
def match(command, settings): def match(command, settings):
return (re.search(r"([^:]*): Unknown command.*", command.stderr) != None return (re.search(r"([^:]*): Unknown command.*", command.stderr) != None

View File

@@ -1,8 +1,10 @@
from thefuck import shells from thefuck import shells
from thefuck.utils import for_app
@for_app('vagrant')
def match(command, settings): def match(command, settings):
return command.script.startswith('vagrant ') and 'run `vagrant up`' in command.stderr.lower() return 'run `vagrant up`' in command.stderr.lower()
def get_new_command(command, settings): def get_new_command(command, settings):

View File

@@ -9,7 +9,7 @@ from subprocess import Popen, PIPE
from time import time from time import time
import io import io
import os import os
from .utils import DEVNULL, memoize from .utils import DEVNULL, memoize, cache
class Generic(object): class Generic(object):
@@ -85,9 +85,9 @@ class Bash(Generic):
return name, value return name, value
@memoize @memoize
@cache('.bashrc', '.bash_profile')
def get_aliases(self): def get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, proc = Popen(['bash', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
shell=True)
return dict( return dict(
self._parse_alias(alias) self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
@@ -131,8 +131,7 @@ class Fish(Generic):
@memoize @memoize
def get_aliases(self): def get_aliases(self):
overridden = self._get_overridden_aliases() overridden = self._get_overridden_aliases()
proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL, proc = Popen(['fish', '-ic', 'functions'], stdout=PIPE, stderr=DEVNULL)
shell=True)
functions = proc.stdout.read().decode('utf-8').strip().split('\n') functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {func: func for func in functions if func not in overridden} return {func: func for func in functions if func not in overridden}
@@ -171,9 +170,9 @@ class Zsh(Generic):
return name, value return name, value
@memoize @memoize
@cache('.zshrc')
def get_aliases(self): def get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, proc = Popen(['zsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
shell=True)
return dict( return dict(
self._parse_alias(alias) self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
@@ -205,8 +204,7 @@ class Tcsh(Generic):
@memoize @memoize
def get_aliases(self): def get_aliases(self):
proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, proc = Popen(['tcsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
shell=True)
return dict( return dict(
self._parse_alias(alias) self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')

View File

View File

@@ -1,9 +1,9 @@
""" This file provide some utility functions for Arch Linux specific rules.""" """ This file provide some utility functions for Arch Linux specific rules."""
import thefuck.utils
import subprocess import subprocess
from .. import utils
@thefuck.utils.memoize @utils.memoize
def get_pkgfile(command): def get_pkgfile(command):
""" Gets the packages that provide the given command using `pkgfile`. """ Gets the packages that provide the given command using `pkgfile`.
@@ -20,7 +20,7 @@ def get_pkgfile(command):
packages = subprocess.check_output( packages = subprocess.check_output(
['pkgfile', '-b', '-v', command], ['pkgfile', '-b', '-v', command],
universal_newlines=True, stderr=thefuck.utils.DEVNULL universal_newlines=True, stderr=utils.DEVNULL
).splitlines() ).splitlines()
return [package.split()[0] for package in packages] return [package.split()[0] for package in packages]
@@ -29,13 +29,13 @@ def get_pkgfile(command):
def archlinux_env(): def archlinux_env():
if thefuck.utils.which('yaourt'): if utils.which('yaourt'):
pacman = 'yaourt' pacman = 'yaourt'
elif thefuck.utils.which('pacman'): elif utils.which('pacman'):
pacman = 'sudo pacman' pacman = 'sudo pacman'
else: else:
return False, None return False, None
enabled_by_default = thefuck.utils.which('pkgfile') enabled_by_default = utils.which('pkgfile')
return enabled_by_default, pacman return enabled_by_default, pacman

15
thefuck/specific/brew.py Normal file
View File

@@ -0,0 +1,15 @@
import subprocess
from ..utils import memoize, which
enabled_by_default = bool(which('brew'))
@memoize
def get_brew_path_prefix():
"""To get brew path"""
try:
return subprocess.check_output(['brew', '--prefix'],
universal_newlines=True).strip()
except:
return None

32
thefuck/specific/git.py Normal file
View File

@@ -0,0 +1,32 @@
import re
from shlex import split
from decorator import decorator
from ..types import Command
from ..utils import quote, is_app
@decorator
def git_support(fn, command, settings):
"""Resolves git aliases and supports testing for both git and hub."""
# supports GitHub's `hub` command
# which is recommended to be used with `alias git=hub`
# but at this point, shell aliases have already been resolved
if not is_app(command, 'git', 'hub'):
return False
# perform git aliases expansion
if 'trace: alias expansion:' in command.stderr:
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)",
command.stderr)
alias = search.group(1)
# by default git quotes everything, for example:
# 'commit' '--amend'
# which is surprising and does not allow to easily test for
# eg. 'git commit'
expansion = ' '.join(map(quote, split(search.group(2))))
new_script = command.script.replace(alias, expansion)
command = Command._replace(command, script=new_script)
return fn(command, settings)

22
thefuck/specific/sudo.py Normal file
View File

@@ -0,0 +1,22 @@
import six
from decorator import decorator
from ..types import Command
@decorator
def sudo_support(fn, command, settings):
"""Removes sudo before calling fn and adds it after."""
if not command.script.startswith('sudo '):
return fn(command, settings)
result = fn(Command(command.script[5:],
command.stdout,
command.stderr),
settings)
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

View File

@@ -1,14 +1,34 @@
from collections import namedtuple from collections import namedtuple
from traceback import format_stack
from .logs import debug
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'))
class CorrectedCommand(object):
def __init__(self, script, side_effect, priority):
self.script = script
self.side_effect = side_effect
self.priority = priority
def __eq__(self, other):
"""Ignores `priority` field."""
if isinstance(other, CorrectedCommand):
return (other.script, other.side_effect) ==\
(self.script, self.side_effect)
else:
return False
def __hash__(self):
return (self.script, self.side_effect).__hash__()
def __repr__(self):
return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
class RulesNamesList(list): class RulesNamesList(list):
"""Wrapper a top of list for storing rules names.""" """Wrapper a top of list for storing rules names."""
@@ -18,7 +38,6 @@ class RulesNamesList(list):
class Settings(dict): class Settings(dict):
def __getattr__(self, item): def __getattr__(self, item):
return self.get(item) return self.get(item)
@@ -29,3 +48,61 @@ class Settings(dict):
conf = dict(kwargs) conf = dict(kwargs)
conf.update(self) conf.update(self)
return Settings(conf) return Settings(conf)
class SortedCorrectedCommandsSequence(object):
"""List-like collection/wrapper around generator, that:
- immediately gives access to the first commands through [];
- realises generator and sorts commands on first access to other
commands through [], or when len called.
"""
def __init__(self, commands, settings):
self._settings = settings
self._commands = commands
self._cached = self._realise_first()
self._realised = False
def _realise_first(self):
try:
return [next(self._commands)]
except StopIteration:
return []
def _remove_duplicates(self, corrected_commands):
"""Removes low-priority duplicates."""
commands = {command
for command in sorted(corrected_commands,
key=lambda command: -command.priority)
if command.script != self._cached[0]}
return commands
def _realise(self):
"""Realises generator, removes duplicates and sorts commands."""
if self._cached:
commands = self._remove_duplicates(self._commands)
self._cached = [self._cached[0]] + sorted(
commands, key=lambda corrected_command: corrected_command.priority)
self._realised = True
debug('SortedCommandsSequence was realised with: {}, after: {}'.format(
self._cached, '\n'.join(format_stack())), self._settings)
def __getitem__(self, item):
if item != 0 and not self._realised:
self._realise()
return self._cached[item]
def __bool__(self):
return bool(self._cached)
def __len__(self):
if not self._realised:
self._realise()
return len(self._cached)
def __iter__(self):
if not self._realised:
self._realise()
return iter(self._cached)

View File

@@ -88,9 +88,7 @@ def select_command(corrected_commands, settings):
logs.show_corrected_command(selector.value, settings) logs.show_corrected_command(selector.value, settings)
return selector.value return selector.value
multiple_cmds = len(corrected_commands) > 1 selector.on_change(lambda val: logs.confirm_text(val, settings))
selector.on_change(lambda val: logs.confirm_text(val, multiple_cmds, settings))
for action in read_actions(): for action in read_actions():
if action == SELECT: if action == SELECT:
sys.stderr.write('\n') sys.stderr.write('\n')

View File

@@ -1,11 +1,15 @@
from .types import Command
from difflib import get_close_matches from difflib import get_close_matches
from functools import wraps from functools import wraps
from pathlib import Path import shelve
from shlex import split from decorator import decorator
from contextlib import closing
import tempfile
import os import os
import pickle import pickle
import re import re
from pathlib import Path
import six import six
@@ -17,6 +21,23 @@ else:
from shlex import quote from shlex import quote
def memoize(fn):
"""Caches previous calls to the function."""
memo = {}
@wraps(fn)
def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs))
if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs)
return memo[key]
return wrapper
memoize.disabled = False
@memoize
def which(program): def which(program):
"""Returns `program` path or `None`.""" """Returns `program` path or `None`."""
@@ -47,81 +68,9 @@ def wrap_settings(params):
print(settings.apt) print(settings.apt)
""" """
def decorator(fn): def _wrap_settings(fn, command, settings):
@wraps(fn)
def wrapper(command, settings):
return fn(command, settings.update(**params)) return fn(command, settings.update(**params))
return wrapper return decorator(_wrap_settings)
return decorator
def sudo_support(fn):
"""Removes sudo before calling fn and adds it after."""
@wraps(fn)
def wrapper(command, settings):
if not command.script.startswith('sudo '):
return fn(command, settings)
result = fn(Command(command.script[5:],
command.stdout,
command.stderr),
settings)
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
def git_support(fn):
"""Resolves git aliases and supports testing for both git and hub."""
@wraps(fn)
def wrapper(command, settings):
# supports GitHub's `hub` command
# which is recommended to be used with `alias git=hub`
# but at this point, shell aliases have already been resolved
is_git_cmd = command.script.startswith(('git', 'hub'))
if not is_git_cmd:
return False
# perform git aliases expansion
if 'trace: alias expansion:' in command.stderr:
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)",
command.stderr)
alias = search.group(1)
# by default git quotes everything, for example:
# 'commit' '--amend'
# which is surprising and does not allow to easily test for
# eg. 'git commit'
expansion = ' '.join(map(quote, split(search.group(2))))
new_script = command.script.replace(alias, expansion)
command = Command._replace(command, script=new_script)
return fn(command, settings)
return wrapper
def memoize(fn):
"""Caches previous calls to the function."""
memo = {}
@wraps(fn)
def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs))
if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs)
return memo[key]
return wrapper
memoize.disabled = False
def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True): def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True):
@@ -163,11 +112,9 @@ def replace_argument(script, from_, to):
u' {} '.format(from_), u' {} '.format(to), 1) u' {} '.format(from_), u' {} '.format(to), 1)
def eager(fn): @decorator
@wraps(fn) def eager(fn, *args, **kwargs):
def wrapper(*args, **kwargs):
return list(fn(*args, **kwargs)) return list(fn(*args, **kwargs))
return wrapper
@eager @eager
@@ -185,3 +132,63 @@ def replace_command(command, broken, matched):
new_cmds = get_close_matches(broken, matched, cutoff=0.1) new_cmds = get_close_matches(broken, matched, cutoff=0.1)
return [replace_argument(command.script, broken, new_cmd.strip()) return [replace_argument(command.script, broken, new_cmd.strip())
for new_cmd in new_cmds] for new_cmd in new_cmds]
@memoize
def is_app(command, *app_names):
"""Returns `True` if command is call to one of passed app names."""
for name in app_names:
if command.script == name \
or command.script.startswith(u'{} '.format(name)):
return True
return False
def for_app(*app_names):
"""Specifies that matching script is for on of app names."""
def _for_app(fn, command, settings):
if is_app(command, *app_names):
return fn(command, settings)
else:
return False
return decorator(_for_app)
def cache(*depends_on):
"""Caches function result in temporary file.
Cache will be expired when modification date of files from `depends_on`
will be changed.
Function wrapped in `cache` should be arguments agnostic.
"""
def _get_mtime(name):
path = os.path.join(os.path.expanduser('~'), name)
try:
return str(os.path.getmtime(path))
except OSError:
return '0'
@decorator
def _cache(fn, *args, **kwargs):
if cache.disabled:
return fn(*args, **kwargs)
cache_path = os.path.join(tempfile.gettempdir(), '.thefuck-cache')
# A bit obscure, but simplest way to generate unique key for
# functions and methods in python 2 and 3:
key = '{}.{}'.format(fn.__module__, repr(fn).split('at')[0])
etag = '.'.join(_get_mtime(name) for name in depends_on)
with closing(shelve.open(cache_path)) as db:
if db.get(key, {}).get('etag') == etag:
return db[key]['value']
else:
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
return _cache
cache.disabled = False