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

Compare commits

..

34 Commits
1.47 ... 2.0

Author SHA1 Message Date
nvbn
4fb990742d Bump to 2.0 2015-07-19 22:33:56 +03:00
nvbn
cf3dca6f51 #284 Add coveralls support 2015-07-19 21:57:19 +03:00
nvbn
5187bada1b #N/A Update readme 2015-07-19 21:53:08 +03:00
nvbn
0238569b71 #N/A Require confirmation by default 2015-07-19 21:52:46 +03:00
nvbn
463b4fef2f Merge branch 'mcarton-git-aliases' 2015-07-19 21:29:39 +03:00
nvbn
f90bac10ed #290: Fix typo 2015-07-19 21:29:28 +03:00
nvbn
90014b2b05 Merge branch 'git-aliases' of https://github.com/mcarton/thefuck into mcarton-git-aliases 2015-07-19 21:27:04 +03:00
Vladimir Iakovlev
4276cacaf6 Merge pull request #292 from SimenB/delete-git-branch
Add git_branch_delete rule
2015-07-19 21:26:39 +03:00
Simen Bekkhus
b31aea3737 Add git_branch_delete rule 2015-07-19 13:45:46 +02:00
nvbn
fbfb4b5e41 Merge branch 'petr-tichy-master' 2015-07-18 17:19:57 +03:00
Petr Tichý
51c37bc5ab Fix wheel dependencies for Python 2 2015-07-17 18:51:35 +02:00
mcarton
5d0912fee8 Unquote over-quoted commands in @git_support
This allows writing rules more easily (eg. the git_branch_list rule
tests for `command.script.split() == 'git branch list'.split()`) and
looks nicer when `require_confirmation` is set.
2015-07-17 14:07:17 +02:00
mcarton
f6a4902074 Use @git_support in all git_* rules 2015-07-17 13:11:36 +02:00
mcarton
707d91200e Make the environment a setting
This would allow other rules to set the environment as needed for
`@git_support` and `GIT_TRACE`.
2015-07-17 11:37:13 +02:00
mcarton
b3e09d68df Start support for git aliases 2015-07-16 20:23:31 +02:00
nvbn
78769e4fbc Bump to 1.49 2015-07-15 07:49:18 +03:00
nvbn
3e4c043ccc #280: Add debug output 2015-07-15 07:47:54 +03:00
nvbn
934099fe9e #289: Add is a directory pattern to cp_omitting_directory rule 2015-07-15 07:12:07 +03:00
Vladimir Iakovlev
464f86eccf Merge pull request #288 from scorphus/overridden-aliases
fix(fish.get_aliases): do not include overridden aliases
2015-07-15 06:58:32 +03:00
Pablo Santiago Blum de Aguiar
891fbe7ed1 fix(fish.get_aliases): do not include overridden aliases
Fish Shell overrides some shell commands, such as `cd` and `ls` and
therefore some rules fail to match. The following aliases are excluded
by default:

 * cd
 * grep
 * ls
 * man
 * open

To change them, one can use the `TF_OVERRIDDEN_ALIASES` environment
variable such as:

```
set TF_OVERRIDDEN_ALIASES 'cd,grep,ls'
```

Fix #262
2015-07-13 22:53:15 -03:00
nvbn
5abab8bd1e Merge branch 'master' of github.com:nvbn/thefuck 2015-07-10 17:58:53 +03:00
nvbn
7ebc8a38af #N/A Add history rule 2015-07-10 17:58:41 +03:00
nvbn
f40b63f44b #N/A Add ability to disable memoization in tests 2015-07-10 17:06:05 +03:00
nvbn
4b4e7acc0f #N/A Add ability to get shell history 2015-07-10 16:42:21 +03:00
Vladimir Iakovlev
a8587d3871 Merge pull request #285 from mcarton/tmux
Use `get_closest` in the tmux rule
2015-07-10 15:54:14 +03:00
mcarton
370c58e679 Use get_closest in the tmux rule 2015-07-10 09:49:49 +02:00
Vladimir Iakovlev
328e65179e Merge pull request #283 from mcarton/mercurial
Some fixes in REAME.md
2015-07-09 20:13:38 +03:00
Vladimir Iakovlev
63bb4da8e1 Merge pull request #282 from mcarton/sudo
Add systemd's kind of error for the sudo rule
2015-07-09 20:13:17 +03:00
mcarton
0b5a7a8e2d Fix rule name in README 2015-07-09 18:35:33 +02:00
mcarton
5693bd49f7 #281 Add the mercurial rule to README.md 2015-07-09 18:01:44 +02:00
mcarton
12f8d017b9 Add systemd's kind of error for the sudo rule
A complete error would be:

```
% systemctl daemon-reload
==== AUTHENTICATING FOR org.freedesktop.systemd1.reload-daemon ===
Authentication is required to reload the systemd state.
Authenticating as: martin
Password:
```
2015-07-09 17:24:45 +02:00
nvbn
c7071763a3 Bump to 1.48 2015-07-08 21:34:39 +03:00
nvbn
27b5b9de6a #229 Use closest git command 2015-07-08 21:33:30 +03:00
nvbn
c0eae8b85c #N/A Add get_closest utility function 2015-07-08 21:30:24 +03:00
40 changed files with 473 additions and 126 deletions

View File

@@ -4,6 +4,10 @@ python:
- "3.3" - "3.3"
- "2.7" - "2.7"
install: install:
- python setup.py develop
- pip install -r requirements.txt - pip install -r requirements.txt
script: py.test -v - python setup.py develop
- pip install coveralls
script:
- export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1}
- coverage run --source=thefuck,tests -m py.test -v
after_success: coveralls

View File

@@ -1,4 +1,4 @@
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck) # The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck) [![Coverage Status](https://coveralls.io/repos/nvbn/thefuck/badge.svg?branch=master&service=github)](https://coveralls.io/github/nvbn/thefuck?branch=master)
**Aliases changed in 1.34.** **Aliases changed in 1.34.**
@@ -6,7 +6,9 @@ Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/) inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320). [tweet](https://twitter.com/liamosaur/status/506975850596536320).
Few examples: ![gif with examples](https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif)
Few more examples:
```bash ```bash
➜ apt-get install vim ➜ apt-get install vim
@@ -159,6 +161,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `dry` – fix repetitions like "git git push"; * `dry` – fix repetitions like "git git push";
* `fix_alt_space` – replaces Alt+Space with Space character; * `fix_alt_space` – replaces Alt+Space with Space character;
* `git_add` – fix *"Did you forget to 'git add'?"*; * `git_add` – fix *"Did you forget to 'git add'?"*;
* `git_branch_delete` – changes `git branch -d` to `git branch -D`;
* `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch; * `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch;
* `git_checkout` – creates the branch before checking-out; * `git_checkout` – creates the branch before checking-out;
* `git_diff_staged` – adds `--staged` to previous `git diff` with unexpected output; * `git_diff_staged` – adds `--staged` to previous `git diff` with unexpected output;
@@ -169,12 +172,14 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `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;
* `has_exists_script` – prepends `./` when script/binary exists; * `has_exists_script` – prepends `./` when script/binary exists;
* `history` – tries to replace command with most similar command from history;
* `java` – removes `.java` extension when running Java programs; * `java` – removes `.java` extension when running Java programs;
* `javac` – appends missing `.java` when compiling Java files; * `javac` – appends missing `.java` when compiling Java files;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `ls_lah` – adds -lah to ls; * `ls_lah` – adds -lah to ls;
* `man` – change manual section; * `man` – change manual section;
* `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;
* `mkdir_p` – adds `-p` when you trying to create directory without parent; * `mkdir_p` – adds `-p` when you trying to create directory without parent;
* `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;
@@ -184,7 +189,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `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` – 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`;
* `ssh_known_hosts` – removes host from `known_hosts` on warning; * `ssh_known_hosts` – removes host from `known_hosts` on warning;
* `sudo` – prepends `sudo` to previous command if it failed because of permissions; * `sudo` – prepends `sudo` to previous command if it failed because of permissions;
@@ -248,10 +253,11 @@ priority = 1000 # Lower first
The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`: The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`:
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`; * `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` – requires confirmation before running new command, by default `False`; * `require_confirmation` – requires confirmation before running new command, by default `True`;
* `wait_command` – max amount of time in seconds for getting previous command output; * `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output; * `no_colors` – disable colored output;
* `priority` – dict with rules priorities, rule with lower `priority` will be matched first. * `priority` – dict with rules priorities, rule with lower `priority` will be matched first;
* `debug` – enabled debug output, by default `False`;
Example of `settings.py`: Example of `settings.py`:
@@ -261,6 +267,7 @@ require_confirmation = True
wait_command = 10 wait_command = 10
no_colors = False no_colors = False
priority = {'sudo': 100, 'no_command': 9999} priority = {'sudo': 100, 'no_command': 9999}
debug = False
``` ```
Or via environment variables: Or via environment variables:
@@ -270,7 +277,8 @@ Or via environment variables:
* `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output; * `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`; * `THEFUCK_NO_COLORS` – disable colored output, `true/false`;
* `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`, * `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`,
rule with lower `priority` will be matched first. rule with lower `priority` will be matched first;
* `THEFUCK_DEBUG` – enables debug output, `true/false`.
For example: For example:

BIN
example.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

View File

@@ -2,3 +2,4 @@ pytest
mock mock
pytest-mock pytest-mock
wheel wheel
setuptools>=17.1

View File

@@ -11,12 +11,10 @@ elif (3, 0) < sys.version_info < (3, 3):
' ({}.{} detected).'.format(*sys.version_info[:2])) ' ({}.{} detected).'.format(*sys.version_info[:2]))
sys.exit(-1) sys.exit(-1)
VERSION = '1.47' VERSION = '2.0'
install_requires = ['psutil', 'colorama', 'six'] install_requires = ['psutil', 'colorama', 'six']
extras_require = {':python_version<"3.4"': ['pathlib']}
if sys.version_info < (3, 4):
install_requires.append('pathlib')
setup(name='thefuck', setup(name='thefuck',
version=VERSION, version=VERSION,
@@ -30,6 +28,7 @@ setup(name='thefuck',
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=install_requires, install_requires=install_requires,
extras_require=extras_require,
entry_points={'console_scripts': [ entry_points={'console_scripts': [
'thefuck = thefuck.main:main', 'thefuck = thefuck.main:main',
'thefuck-alias = thefuck.shells:app_alias']}) 'thefuck-alias = thefuck.shells:app_alias']})

6
tests/conftest.py Normal file
View File

@@ -0,0 +1,6 @@
import pytest
@pytest.fixture
def no_memoize(monkeypatch):
monkeypatch.setattr('thefuck.utils.memoize.disabled', True)

View File

@@ -1,14 +1,22 @@
from mock import Mock import pytest
from thefuck.rules.cp_omitting_directory import match, get_new_command from thefuck.rules.cp_omitting_directory import match, get_new_command
from tests.utils import Command
def test_match(): @pytest.mark.parametrize('script, stderr', [
assert match(Mock(script='cp dir', stderr="cp: omitting directory 'dir'"), ('cp dir', 'cp: dor: is a directory'),
None) ('cp dir', "cp: omitting directory 'dir'")])
assert not match(Mock(script='some dir', def test_match(script, stderr):
stderr="cp: omitting directory 'dir'"), None) assert match(Command(script, stderr=stderr), None)
assert not match(Mock(script='cp dir', stderr=""), None)
@pytest.mark.parametrize('script, stderr', [
('some dir', 'cp: dor: is a directory'),
('some dir', "cp: omitting directory 'dir'"),
('cp dir', '')])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr), None)
def test_get_new_command(): def test_get_new_command():
assert get_new_command(Mock(script='cp dir'), None) == 'cp -a dir' assert get_new_command(Command(script='cp dir'), None) == 'cp -a dir'

View File

@@ -0,0 +1,22 @@
import pytest
from thefuck.rules.git_branch_delete import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr():
return '''error: The branch 'branch' is not fully merged.
If you are sure you want to delete it, run 'git branch -D branch'.
'''
def test_match(stderr):
assert match(Command('git branch -d branch', stderr=stderr), None)
assert not match(Command('git branch -d branch'), None)
assert not match(Command('ls', stderr=stderr), None)
def test_get_new_command(stderr):
assert get_new_command(Command('git branch -d branch', stderr=stderr), None)\
== "git branch -D branch"

View File

@@ -3,15 +3,13 @@ from thefuck.rules.git_diff_staged import match, get_new_command
from tests.utils import Command from tests.utils import Command
@pytest.mark.parametrize('command', [ @pytest.mark.parametrize('command', [Command(script='git diff')])
Command(script='git diff'),
Command(script='git df'),
Command(script='git ds')])
def test_match(command): def test_match(command):
assert match(command, None) assert match(command, None)
@pytest.mark.parametrize('command', [ @pytest.mark.parametrize('command', [
Command(script='git diff --staged'),
Command(script='git tag'), Command(script='git tag'),
Command(script='git branch'), Command(script='git branch'),
Command(script='git log')]) Command(script='git log')])
@@ -20,8 +18,6 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [ @pytest.mark.parametrize('command, new_command', [
(Command('git diff'), 'git diff --staged'), (Command('git diff'), 'git diff --staged')])
(Command('git df'), 'git df --staged'),
(Command('git ds'), 'git ds --staged')])
def test_get_new_command(command, new_command): def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command assert get_new_command(command, None) == new_command

View File

@@ -25,6 +25,16 @@ stats
""" """
@pytest.fixture
def git_not_command_closest():
return '''git: 'tags' is not a git command. See 'git --help'.
Did you mean one of these?
stage
tag
'''
@pytest.fixture @pytest.fixture
def git_command(): def git_command():
return "* master" return "* master"
@@ -37,8 +47,11 @@ def test_match(git_not_command, git_command, git_not_command_one_of_this):
assert not match(Command('git branch', stderr=git_command), None) assert not match(Command('git branch', stderr=git_command), None)
def test_get_new_command(git_not_command, git_not_command_one_of_this): def test_get_new_command(git_not_command, git_not_command_one_of_this,
assert get_new_command(Command('git brnch', stderr=git_not_command), None)\ git_not_command_closest):
== 'git branch' assert get_new_command(Command('git brnch', stderr=git_not_command), None) \
== 'git branch'
assert get_new_command(Command('git st', stderr=git_not_command_one_of_this), assert get_new_command(Command('git st', stderr=git_not_command_one_of_this),
None) == 'git status' None) == 'git status'
assert get_new_command(Command('git tags', stderr=git_not_command_closest),
None) == 'git tag'

View File

@@ -3,22 +3,20 @@ from thefuck.rules.git_stash import match, get_new_command
from tests.utils import Command from tests.utils import Command
@pytest.fixture cherry_pick_error = (
def cherry_pick_error(): 'error: Your local changes would be overwritten by cherry-pick.\n'
return ('error: Your local changes would be overwritten by cherry-pick.\n' 'hint: Commit your changes or stash them to proceed.\n'
'hint: Commit your changes or stash them to proceed.\n' 'fatal: cherry-pick failed')
'fatal: cherry-pick failed')
@pytest.fixture rebase_error = (
def rebase_error(): 'Cannot rebase: Your index contains uncommitted changes.\n'
return ('Cannot rebase: Your index contains uncommitted changes.\n' 'Please commit or stash them.')
'Please commit or stash them.')
@pytest.mark.parametrize('command', [ @pytest.mark.parametrize('command', [
Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error()), Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error),
Command(script='git rebase -i HEAD~7', stderr=rebase_error())]) Command(script='git rebase -i HEAD~7', stderr=rebase_error)])
def test_match(command): def test_match(command):
assert match(command, None) assert match(command, None)

View File

@@ -0,0 +1,35 @@
import pytest
from thefuck.rules.history import match, get_new_command
from tests.utils import Command
@pytest.fixture
def history(mocker):
return mocker.patch('thefuck.rules.history.get_history',
return_value=['ls cat', 'diff x', 'nocommand x'])
@pytest.fixture
def callables(mocker):
return mocker.patch('thefuck.rules.history.get_all_callables',
return_value=['diff', 'ls'])
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.parametrize('script', ['ls cet', 'daff x'])
def test_match(script):
assert match(Command(script=script), None)
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.parametrize('script', ['apt-get', 'nocommand y'])
def test_not_match(script):
assert not match(Command(script=script), None)
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.parametrize('script, result', [
('ls cet', 'ls cat'),
('daff x', 'diff x')])
def test_get_new_command(script, result):
assert get_new_command(Command(script), None) == result

View File

@@ -1,36 +1,42 @@
from mock import patch, Mock import pytest
from thefuck.rules.no_command import match, get_new_command, _get_all_callables from thefuck.rules.no_command import match, get_new_command, get_all_callables
from tests.utils import Command
@patch('thefuck.rules.no_command._safe', return_value=[]) @pytest.fixture(autouse=True)
@patch('thefuck.rules.no_command.get_aliases', def _safe(mocker):
return_value=['vim', 'apt-get', 'fsck', 'fuck']) mocker.patch('thefuck.rules.no_command._safe', return_value=[])
@pytest.fixture(autouse=True)
def get_aliases(mocker):
mocker.patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
@pytest.mark.usefixtures('no_memoize')
def test_get_all_callables(*args): def test_get_all_callables(*args):
all_callables = _get_all_callables() all_callables = get_all_callables()
assert 'vim' in all_callables assert 'vim' in all_callables
assert 'fsck' in all_callables assert 'fsck' in all_callables
assert 'fuck' not in all_callables assert 'fuck' not in all_callables
@patch('thefuck.rules.no_command._safe', return_value=[]) @pytest.mark.usefixtures('no_memoize')
@patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
def test_match(*args): def test_match(*args):
assert match(Mock(stderr='vom: not found', script='vom file.py'), None) assert match(Command(stderr='vom: not found', script='vom file.py'), None)
assert match(Mock(stderr='fucck: not found', script='fucck'), None) assert match(Command(stderr='fucck: not found', script='fucck'), None)
assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None) assert not match(Command(stderr='qweqwe: not found', script='qweqwe'), None)
assert not match(Mock(stderr='some text', script='vom file.py'), None) assert not match(Command(stderr='some text', script='vom file.py'), None)
@patch('thefuck.rules.no_command._safe', return_value=[]) @pytest.mark.usefixtures('no_memoize')
@patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
def test_get_new_command(*args): def test_get_new_command(*args):
assert get_new_command( assert get_new_command(
Mock(stderr='vom: not found', Command(stderr='vom: not found',
script='vom file.py'), script='vom file.py'),
None) == 'vim file.py' None) == 'vim file.py'
assert get_new_command( assert get_new_command(
Mock(stderr='fucck: not found', Command(stderr='fucck: not found',
script='fucck'), script='fucck'),
None) == 'fsck' Command) == 'fsck'

View File

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

View File

@@ -5,3 +5,10 @@ from thefuck import logs
def test_color(): def test_color():
assert logs.color('red', Mock(no_colors=False)) == 'red' assert logs.color('red', Mock(no_colors=False)) == 'red'
assert logs.color('red', Mock(no_colors=True)) == '' assert logs.color('red', Mock(no_colors=True)) == ''
def test_debug(capsys):
logs.debug('test', Mock(no_colors=True, debug=True))
assert capsys.readouterr() == ('', 'DEBUG: test\n')
logs.debug('test', Mock(no_colors=True, debug=False))
assert capsys.readouterr() == ('', '')

View File

@@ -77,23 +77,23 @@ class TestGetCommand(object):
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x) monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_get_command_calls(self, Popen): def test_get_command_calls(self, Popen):
assert main.get_command(Mock(), assert main.get_command(Mock(env={}),
['thefuck', 'apt-get', 'search', 'vim']) \ ['thefuck', 'apt-get', 'search', 'vim']) \
== Command('apt-get search vim', 'stdout', 'stderr') == Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim', Popen.assert_called_once_with('apt-get search vim',
shell=True, shell=True,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
env={'LANG': 'C'}) env={})
@pytest.mark.parametrize('args, result', [ @pytest.mark.parametrize('args, result', [
(['thefuck', 'ls', '-la'], 'ls -la'), (['thefuck', 'ls', '-la'], 'ls -la'),
(['thefuck', 'ls'], 'ls')]) (['thefuck', 'ls'], 'ls')])
def test_get_command_script(self, args, result): def test_get_command_script(self, args, result):
if result: if result:
assert main.get_command(Mock(), args).script == result assert main.get_command(Mock(env={}), args).script == result
else: else:
assert main.get_command(Mock(), args) is None assert main.get_command(Mock(env={}), args) is None
class TestGetMatchedRule(object): class TestGetMatchedRule(object):
@@ -110,7 +110,7 @@ class TestGetMatchedRule(object):
def test_when_rule_failed(self, capsys): def test_when_rule_failed(self, capsys):
main.get_matched_rule( main.get_matched_rule(
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))], Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))],
Mock(no_colors=True)) Mock(no_colors=True, debug=False))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:' assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
@@ -126,7 +126,7 @@ class TestRunRule(object):
def test_run_rule_with_side_effect(self, capsys): def test_run_rule_with_side_effect(self, capsys):
side_effect = Mock() side_effect = Mock()
settings = Mock() settings = Mock(debug=False)
command = Command() command = Command()
main.run_rule(Rule(get_new_command=lambda *_: 'new-command', main.run_rule(Rule(get_new_command=lambda *_: 'new-command',
side_effect=side_effect), side_effect=side_effect),

View File

@@ -12,6 +12,16 @@ def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True) return mocker.patch('os.path.isfile', return_value=True)
@pytest.fixture
@pytest.mark.usefixtures('isfile')
def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__\
.return_value.__iter__.return_value = lines
return aux
class TestGeneric(object): class TestGeneric(object):
@pytest.fixture @pytest.fixture
def shell(self): def shell(self):
@@ -38,6 +48,12 @@ class TestGeneric(object):
assert 'thefuck' in shell.app_alias() assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias() assert 'TF_ALIAS' in shell.app_alias()
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
# We don't know what to do in generic shell with history lines,
# so just ignore them:
assert list(shell.get_history()) == []
@pytest.mark.usefixtures('isfile') @pytest.mark.usefixtures('isfile')
class TestBash(object): class TestBash(object):
@@ -85,6 +101,10 @@ class TestBash(object):
assert 'thefuck' in shell.app_alias() assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias() assert 'TF_ALIAS' in shell.app_alias()
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.usefixtures('isfile') @pytest.mark.usefixtures('isfile')
class TestFish(object): class TestFish(object):
@@ -96,18 +116,34 @@ class TestFish(object):
def Popen(self, mocker): def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen') mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = ( mock.return_value.stdout.read.return_value = (
b'fish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nmath') b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n'
b'man\nmath\npopd\npushd\nruby')
return mock return mock
@pytest.fixture
def environ(self, monkeypatch):
data = {'TF_OVERRIDDEN_ALIASES': 'cd, ls, man, open'}
monkeypatch.setattr('thefuck.shells.os.environ', data)
return data
@pytest.mark.usefixture('environ')
def test_get_overridden_aliases(self, shell, environ):
assert shell._get_overridden_aliases() == ['cd', 'ls', 'man', 'open']
@pytest.mark.parametrize('before, after', [ @pytest.mark.parametrize('before, after', [
('cd', 'cd'),
('pwd', 'pwd'), ('pwd', 'pwd'),
('fuck', 'fish -ic "fuck"'), ('fuck', 'fish -ic "fuck"'),
('find', 'find'), ('find', 'find'),
('funced', 'fish -ic "funced"'), ('funced', 'fish -ic "funced"'),
('grep', 'grep'),
('awk', 'awk'), ('awk', 'awk'),
('math "2 + 2"', r'fish -ic "math \"2 + 2\""'), ('math "2 + 2"', r'fish -ic "math \"2 + 2\""'),
('man', 'man'),
('open', 'open'),
('vim', 'vim'), ('vim', 'vim'),
('ll', 'fish -ic "ll"')]) # Fish has no aliases but functions ('ll', 'fish -ic "ll"'),
('ls', 'ls')]) # Fish has no aliases but functions
def test_from_shell(self, before, after, shell): def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after assert shell.from_shell(before) == after
@@ -129,10 +165,12 @@ class TestFish(object):
'fuck': 'fuck', 'fuck': 'fuck',
'funced': 'funced', 'funced': 'funced',
'funcsave': 'funcsave', 'funcsave': 'funcsave',
'grep': 'grep',
'history': 'history', 'history': 'history',
'll': 'll', 'll': 'll',
'math': 'math'} 'math': 'math',
'popd': 'popd',
'pushd': 'pushd',
'ruby': 'ruby'}
def test_app_alias(self, shell): def test_app_alias(self, shell):
assert 'function fuck' in shell.app_alias() assert 'function fuck' in shell.app_alias()
@@ -187,3 +225,7 @@ class TestZsh(object):
assert 'alias fuck' in shell.app_alias() assert 'alias fuck' in shell.app_alias()
assert 'thefuck' in shell.app_alias() assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias() assert 'TF_ALIAS' in shell.app_alias()
def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])
assert list(shell.get_history()) == ['ls', 'rm']

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from mock import Mock from mock import Mock
from thefuck.utils import sudo_support, wrap_settings, memoize from thefuck.utils import git_support, sudo_support, wrap_settings, memoize, get_closest
from thefuck.types import Settings from thefuck.types import Settings
from tests.utils import Command from tests.utils import Command
@@ -26,9 +26,36 @@ def test_sudo_support(return_value, command, called, result):
fn.assert_called_once_with(Command(called), None) 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
def test_memoize(): def test_memoize():
fn = Mock(__name__='fn') fn = Mock(__name__='fn')
memoized = memoize(fn) memoized = memoize(fn)
memoized() memoized()
memoized() memoized()
fn.assert_called_once_with() fn.assert_called_once_with()
@pytest.mark.usefixtures('no_memoize')
def test_no_memoize():
fn = Mock(__name__='fn')
memoized = memoize(fn)
memoized()
memoized()
assert fn.call_count == 2
class TestGetClosest(object):
def test_when_can_match(self):
assert 'branch' == get_closest('brnch', ['branch', 'status'])
def test_when_cant_match(self):
assert 'status' == get_closest('st', ['status', 'reset'])

View File

@@ -27,15 +27,18 @@ DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3, 'wait_command': 3,
'require_confirmation': False, 'require_confirmation': True,
'no_colors': False, 'no_colors': False,
'priority': {}} 'debug': False,
'priority': {},
'env': {'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command', 'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation', 'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors', 'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_PRIORITY': 'priority'} 'THEFUCK_PRIORITY': 'priority',
'THEFUCK_DEBUG': 'debug'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
@@ -87,7 +90,7 @@ def _val_from_env(env, attr):
return dict(_priority_from_env(val)) return dict(_priority_from_env(val))
elif attr == 'wait_command': elif attr == 'wait_command':
return int(val) return int(val)
elif attr in ('require_confirmation', 'no_colors'): elif attr in ('require_confirmation', 'no_colors', 'debug'):
return val.lower() == 'true' return val.lower() == 'true'
else: else:
return val return val

View File

@@ -1,3 +1,4 @@
from pprint import pformat
import sys import sys
from traceback import format_exception from traceback import format_exception
import colorama import colorama
@@ -52,3 +53,12 @@ def failed(msg, settings):
msg=msg, msg=msg,
red=color(colorama.Fore.RED, settings), red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings))) reset=color(colorama.Style.RESET_ALL, settings)))
def debug(msg, settings):
if settings.debug:
sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format(
msg=msg,
reset=color(colorama.Style.RESET_ALL, settings),
blue=color(colorama.Fore.BLUE, settings),
bold=color(colorama.Style.BRIGHT, settings)))

View File

@@ -1,6 +1,7 @@
from imp import load_source from imp import load_source
from pathlib import Path from pathlib import Path
from os.path import expanduser from os.path import expanduser
from pprint import pformat
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import os import os
import sys import sys
@@ -79,8 +80,13 @@ def get_command(settings, args):
return return
script = shells.from_shell(script) script = shells.from_shell(script)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, logs.debug('Call: {}'.format(script), settings)
env=dict(os.environ, LANG='C'))
env = dict(os.environ)
env.update(settings.env)
logs.debug('Executing with env: {}'.format(env), settings)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env)
if wait_output(settings, result): if wait_output(settings, result):
return types.Command(script, result.stdout.read().decode('utf-8'), return types.Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8')) result.stderr.read().decode('utf-8'))
@@ -90,6 +96,7 @@ def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command.""" """Returns first matched rule for command."""
for rule in rules: for rule in rules:
try: try:
logs.debug(u'Trying rule: {}'.format(rule.name), settings)
if rule.match(command, settings): if rule.match(command, settings):
return rule return rule
except Exception: except Exception:
@@ -125,12 +132,21 @@ def main():
colorama.init() colorama.init()
user_dir = setup_user_dir() user_dir = setup_user_dir()
settings = conf.get_settings(user_dir) settings = conf.get_settings(user_dir)
logs.debug('Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv) command = get_command(settings, sys.argv)
if command: if command:
logs.debug('Received stdout: {}'.format(command.stdout), settings)
logs.debug('Received stderr: {}'.format(command.stderr), settings)
rules = get_rules(user_dir, settings) rules = get_rules(user_dir, settings)
logs.debug(
'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
settings)
matched_rule = get_matched_rule(command, rules, settings) matched_rule = get_matched_rule(command, rules, settings)
if matched_rule: if matched_rule:
logs.debug('Matched rule: {}'.format(matched_rule.name), settings)
run_rule(matched_rule, command, settings) run_rule(matched_rule, command, settings)
return return

View File

@@ -1,9 +1,10 @@
import difflib
import os import os
import re import re
from subprocess import check_output from subprocess import check_output
from thefuck.utils import get_closest
# Formulars are base on each local system's status # Formulars are base on each local system's status
brew_formulas = [] brew_formulas = []
try: try:
brew_path_prefix = check_output(['brew', '--prefix'], brew_path_prefix = check_output(['brew', '--prefix'],
@@ -17,8 +18,8 @@ except:
pass pass
def _get_similar_formulars(formula_name): def _get_similar_formula(formula_name):
return difflib.get_close_matches(formula_name, brew_formulas, 1, 0.85) return get_closest(formula_name, brew_formulas, 1, 0.85)
def match(command, settings): def match(command, settings):
@@ -29,7 +30,7 @@ def match(command, settings):
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 = len(_get_similar_formulars(formula)) > 0 has_possible_formulas = bool(_get_similar_formula(formula))
return has_possible_formulas return has_possible_formulas
@@ -37,6 +38,6 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
not_exist_formula = re.findall(r'Error: No available formula for ([a-z]+)', not_exist_formula = re.findall(r'Error: No available formula for ([a-z]+)',
command.stderr)[0] command.stderr)[0]
exist_formula = _get_similar_formulars(not_exist_formula)[0] exist_formula = _get_similar_formula(not_exist_formula)
return command.script.replace(not_exist_formula, exist_formula, 1) return command.script.replace(not_exist_formula, exist_formula, 1)

View File

@@ -1,8 +1,7 @@
import difflib
import os import os
import re import re
import subprocess import subprocess
from thefuck.utils import get_closest
BREW_CMD_PATH = '/Library/Homebrew/cmd' BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps' TAP_PATH = '/Library/Taps'
@@ -78,8 +77,8 @@ if brew_path_prefix:
pass pass
def _get_similar_commands(command): def _get_similar_command(command):
return difflib.get_close_matches(command, brew_commands) return get_closest(command, brew_commands)
def match(command, settings): def match(command, settings):
@@ -90,7 +89,7 @@ def match(command, settings):
if is_proper_command: if is_proper_command:
broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)', broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)',
command.stderr)[0] command.stderr)[0]
has_possible_commands = len(_get_similar_commands(broken_cmd)) > 0 has_possible_commands = bool(_get_similar_command(broken_cmd))
return has_possible_commands return has_possible_commands
@@ -98,6 +97,6 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)', broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)',
command.stderr)[0] command.stderr)[0]
new_cmd = _get_similar_commands(broken_cmd)[0] new_cmd = _get_similar_command(broken_cmd)
return command.script.replace(broken_cmd, new_cmd, 1) return command.script.replace(broken_cmd, new_cmd, 1)

View File

@@ -4,8 +4,9 @@ from thefuck.utils import sudo_support
@sudo_support @sudo_support
def match(command, settings): def match(command, settings):
stderr = command.stderr.lower()
return command.script.startswith('cp ') \ return command.script.startswith('cp ') \
and 'cp: omitting directory' in command.stderr.lower() and ('omitting directory' in stderr or 'is a directory' in stderr)
@sudo_support @sudo_support

View File

@@ -1,13 +1,15 @@
import re import re
from thefuck import shells from thefuck import utils, shells
@utils.git_support
def match(command, settings): def match(command, settings):
return ('git' in command.script return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr and '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
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

@@ -0,0 +1,7 @@
def match(command, settings):
return ('git branch -d' in command.script
and 'If you are sure you want to delete it' in command.stderr)
def get_new_command(command, settings):
return command.script.replace('-d', '-D')

View File

@@ -1,10 +1,12 @@
from thefuck import shells from thefuck import utils, shells
@utils.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() == 'git branch list'.split() return command.script.split() == 'git branch list'.split()
@utils.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

@@ -1,13 +1,15 @@
import re import re
from thefuck import shells from thefuck import shells, utils
@utils.git_support
def match(command, settings): def match(command, settings):
return ('git' in command.script return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr and '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)
@utils.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,6 +1,13 @@
from thefuck import utils
@utils.git_support
def match(command, settings): def match(command, settings):
return command.script.startswith('git d') return ('git' in command.script and
'diff' in command.script and
'--staged' not in command.script)
@utils.git_support
def get_new_command(command, settings): def get_new_command(command, settings):
return '{} --staged'.format(command.script) return '{} --staged'.format(command.script)

View File

@@ -1,16 +1,28 @@
import re import re
from thefuck.utils import get_closest, git_support
@git_support
def match(command, settings): def match(command, settings):
return ('git' in command.script return ('git' in command.script
and " is not a git command. See 'git --help'." in command.stderr and " is not a git command. See 'git --help'." in command.stderr
and 'Did you mean' in command.stderr) and 'Did you mean' in command.stderr)
def _get_all_git_matched_commands(stderr):
should_yield = False
for line in stderr.split('\n'):
if 'Did you mean' in line:
should_yield = True
elif should_yield and line:
yield line.strip()
@git_support
def get_new_command(command, settings): def get_new_command(command, settings):
broken_cmd = re.findall(r"git: '([^']*)' is not a git command", broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
command.stderr)[0] command.stderr)[0]
new_cmd = re.findall(r'Did you mean[^\n]*\n\s*([^\n]*)', new_cmd = get_closest(broken_cmd,
command.stderr)[0] _get_all_git_matched_commands(command.stderr))
return command.script.replace(broken_cmd, new_cmd, 1) return command.script.replace(broken_cmd, new_cmd, 1)

View File

@@ -1,12 +1,14 @@
from thefuck import shells from thefuck import shells, utils
@utils.git_support
def match(command, settings): def match(command, settings):
return ('git' in command.script return ('git' in command.script
and 'pull' in command.script and 'pull' in command.script
and 'set-upstream' in command.stderr) and 'set-upstream' in command.stderr)
@utils.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,8 +1,13 @@
from thefuck import utils
@utils.git_support
def match(command, settings): def match(command, settings):
return ('git' in command.script return ('git' in command.script
and 'push' in command.script and 'push' in command.script
and 'set-upstream' in command.stderr) and 'set-upstream' in command.stderr)
@utils.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,12 +1,14 @@
from thefuck import shells from thefuck import shells, utils
@utils.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 'git' in command.script and 'or stash them' in command.stderr return 'git' in command.script and 'or stash them' in command.stderr
@utils.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)

24
thefuck/rules/history.py Normal file
View File

@@ -0,0 +1,24 @@
from difflib import get_close_matches
from thefuck.shells import get_history
from thefuck.utils import get_closest, memoize
from thefuck.rules.no_command import get_all_callables
@memoize
def _history_of_exists_without_current(command):
callables = get_all_callables()
return [line for line in get_history()
if line != command.script
and line.split(' ')[0] in callables]
def match(command, settings):
return len(get_close_matches(command.script,
_history_of_exists_without_current(command)))
def get_new_command(command, settings):
return get_closest(command.script,
_history_of_exists_without_current(command))
priority = 9999

View File

@@ -1,6 +1,5 @@
import re import re
from thefuck.utils import get_closest
from difflib import get_close_matches
def extract_possisiblities(command): def extract_possisiblities(command):
@@ -26,9 +25,5 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
script = command.script.split(' ') script = command.script.split(' ')
possisiblities = extract_possisiblities(command) possisiblities = extract_possisiblities(command)
matches = get_close_matches(script[1], possisiblities) script[1] = get_closest(script[1], possisiblities)
if matches:
script[1] = matches[0]
else:
script[1] = possisiblities[0]
return ' '.join(script) return ' '.join(script)

View File

@@ -1,7 +1,7 @@
from difflib import get_close_matches from difflib import get_close_matches
import os import os
from pathlib import Path from pathlib import Path
from thefuck.utils import sudo_support from thefuck.utils import sudo_support, memoize
from thefuck.shells import thefuck_alias, get_aliases from thefuck.shells import thefuck_alias, get_aliases
@@ -12,7 +12,8 @@ def _safe(fn, fallback):
return fallback return fallback
def _get_all_callables(): @memoize
def get_all_callables():
tf_alias = thefuck_alias() tf_alias = thefuck_alias()
return [exe.name return [exe.name
for path in os.environ.get('PATH', '').split(':') for path in os.environ.get('PATH', '').split(':')
@@ -25,14 +26,14 @@ def _get_all_callables():
def match(command, settings): def match(command, settings):
return 'not found' in command.stderr and \ return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0], bool(get_close_matches(command.script.split(' ')[0],
_get_all_callables())) get_all_callables()))
@sudo_support @sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
old_command = command.script.split(' ')[0] old_command = command.script.split(' ')[0]
new_command = get_close_matches(old_command, new_command = get_close_matches(old_command,
_get_all_callables())[0] get_all_callables())[0]
return ' '.join([new_command] + command.script.split(' ')[1:]) return ' '.join([new_command] + command.script.split(' ')[1:])

View File

@@ -13,7 +13,8 @@ patterns = ['permission denied',
'must be root', 'must be root',
'need to be root', 'need to be root',
'need root', 'need root',
'only root can do that'] 'only root can do that',
'authentication is required']
def match(command, settings): def match(command, settings):

View File

@@ -1,3 +1,4 @@
from thefuck.utils import get_closest
import re import re
@@ -8,7 +9,12 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
cmd = re.match(r"ambiguous command: (.*), could be: ([^, \n]*)", cmd = re.match(r"ambiguous command: (.*), could be: (.*)",
command.stderr) command.stderr)
return command.script.replace(cmd.group(1), cmd.group(2)) old_cmd = cmd.group(1)
suggestions = [cmd.strip() for cmd in cmd.group(2).split(',')]
new_cmd = get_closest(old_cmd, suggestions)
return command.script.replace(old_cmd, new_cmd)

View File

@@ -7,6 +7,7 @@ from collections import defaultdict
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from time import time from time import time
import os import os
import io
from psutil import Process from psutil import Process
from .utils import DEVNULL, memoize from .utils import DEVNULL, memoize
@@ -48,6 +49,26 @@ class Generic(object):
with open(history_file_name, 'a') as history: with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script)) history.write(self._get_history_line(command_script))
def _script_from_history(self, line):
"""Returns prepared history line.
Should return a blank line if history line is corrupted or empty.
"""
return ''
def get_history(self):
"""Returns list of history entries."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with io.open(history_file_name, 'r',
encoding='utf-8', errors='ignore') as history:
for line in history:
prepared = self._script_from_history(line)\
.strip()
if prepared:
yield prepared
def and_(self, *commands): def and_(self, *commands):
return u' && '.join(commands) return u' && '.join(commands)
@@ -63,7 +84,6 @@ class Bash(Generic):
value = value[1:-1] value = value[1:-1]
return name, value return name, value
@memoize
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) shell=True)
@@ -79,12 +99,24 @@ class Bash(Generic):
def _get_history_line(self, command_script): def _get_history_line(self, command_script):
return u'{}\n'.format(command_script) return u'{}\n'.format(command_script)
def _script_from_history(self, line):
print(line)
return line
class Fish(Generic): class Fish(Generic):
def _get_overridden_aliases(self):
overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip()
if overridden_aliases:
return [alias.strip() for alias in overridden_aliases.split(',')]
else:
return ['cd', 'grep', 'ls', 'man', 'open']
def app_alias(self): def app_alias(self):
return ("function fuck -d 'Correct your previous console command'\n" return ("set TF_ALIAS fuck\n"
"function fuck -d 'Correct your previous console command'\n"
" set -l exit_code $status\n" " set -l exit_code $status\n"
" set -l TF_ALIAS fuck\n"
" set -l eval_script" " set -l eval_script"
" (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n" " (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n"
" set -l fucked_up_commandd $history[1]\n" " set -l fucked_up_commandd $history[1]\n"
@@ -96,12 +128,12 @@ class Fish(Generic):
" end\n" " end\n"
"end") "end")
@memoize
def get_aliases(self): def get_aliases(self):
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) shell=True)
functions = proc.stdout.read().decode('utf-8').strip().split('\n') functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {function: function for function in functions} return {func: func for func in functions if func not in overridden}
def _expand_aliases(self, command_script): def _expand_aliases(self, command_script):
aliases = self.get_aliases() aliases = self.get_aliases()
@@ -137,7 +169,6 @@ class Zsh(Generic):
value = value[1:-1] value = value[1:-1]
return name, value return name, value
@memoize
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) shell=True)
@@ -153,6 +184,12 @@ class Zsh(Generic):
def _get_history_line(self, command_script): def _get_history_line(self, command_script):
return u': {}:0;{}\n'.format(int(time()), command_script) return u': {}:0;{}\n'.format(int(time()), command_script)
def _script_from_history(self, line):
if ';' in line:
return line.split(';', 1)[1]
else:
return ''
class Tcsh(Generic): class Tcsh(Generic):
def app_alias(self): def app_alias(self):
@@ -162,7 +199,6 @@ class Tcsh(Generic):
name, value = alias.split("\t", 1) name, value = alias.split("\t", 1)
return name, value return name, value
@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) shell=True)
@@ -219,5 +255,11 @@ def and_(*commands):
return _get_shell().and_(*commands) return _get_shell().and_(*commands)
@memoize
def get_aliases(): def get_aliases():
return list(_get_shell().get_aliases().keys()) return list(_get_shell().get_aliases().keys())
@memoize
def get_history():
return list(_get_shell().get_history())

View File

@@ -1,6 +1,9 @@
from difflib import get_close_matches
from functools import wraps from functools import wraps
from shlex import split
import os import os
import pickle import pickle
import re
import six import six
from .types import Command from .types import Command
@@ -8,11 +11,9 @@ from .types import Command
DEVNULL = open(os.devnull, 'w') DEVNULL = open(os.devnull, 'w')
if six.PY2: if six.PY2:
import pipes from pipes import quote
quote = pipes.quote
else: else:
import shlex from shlex import quote
quote = shlex.quote
def which(program): def which(program):
@@ -72,6 +73,30 @@ def sudo_support(fn):
return wrapper return wrapper
def git_support(fn):
"""Resolve git aliases."""
@wraps(fn)
def wrapper(command, settings):
if (command.script.startswith('git') and
'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): def memoize(fn):
"""Caches previous calls to the function.""" """Caches previous calls to the function."""
memo = {} memo = {}
@@ -79,9 +104,19 @@ def memoize(fn):
@wraps(fn) @wraps(fn)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs)) key = pickle.dumps((args, kwargs))
if key not in memo: if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs) memo[key] = fn(*args, **kwargs)
return memo[key] return memo[key]
return wrapper return wrapper
memoize.disabled = False
def get_closest(word, possibilities, n=3, cutoff=0.6):
"""Returns closest match or just first from possibilities."""
possibilities = list(possibilities)
try:
return get_close_matches(word, possibilities, n, cutoff)[0]
except IndexError:
return possibilities[0]