mirror of
https://github.com/nvbn/thefuck.git
synced 2025-11-10 20:06:02 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbfb4b5e41 | ||
|
|
51c37bc5ab | ||
|
|
78769e4fbc | ||
|
|
3e4c043ccc | ||
|
|
934099fe9e | ||
|
|
464f86eccf | ||
|
|
891fbe7ed1 | ||
|
|
5abab8bd1e | ||
|
|
7ebc8a38af | ||
|
|
f40b63f44b | ||
|
|
4b4e7acc0f | ||
|
|
a8587d3871 | ||
|
|
370c58e679 | ||
|
|
328e65179e | ||
|
|
63bb4da8e1 | ||
|
|
0b5a7a8e2d | ||
|
|
5693bd49f7 | ||
|
|
12f8d017b9 |
@@ -4,6 +4,6 @@ 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
|
||||||
|
- python setup.py develop
|
||||||
script: py.test -v
|
script: py.test -v
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -169,12 +169,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 +186,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;
|
||||||
@@ -251,7 +253,8 @@ The Fuck has a few settings parameters which can be changed in `~/.thefuck/setti
|
|||||||
* `require_confirmation` – requires confirmation before running new command, by default `False`;
|
* `require_confirmation` – requires confirmation before running new command, by default `False`;
|
||||||
* `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 +264,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 +274,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:
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pytest
|
|||||||
mock
|
mock
|
||||||
pytest-mock
|
pytest-mock
|
||||||
wheel
|
wheel
|
||||||
|
setuptools>=17.1
|
||||||
|
|||||||
7
setup.py
7
setup.py
@@ -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.48'
|
VERSION = '1.49.1'
|
||||||
|
|
||||||
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
6
tests/conftest.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def no_memoize(monkeypatch):
|
||||||
|
monkeypatch.setattr('thefuck.utils.memoize.disabled', True)
|
||||||
@@ -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'
|
||||||
|
|||||||
35
tests/rules/test_history.py
Normal file
35
tests/rules/test_history.py
Normal 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
|
||||||
@@ -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):
|
||||||
|
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'])
|
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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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() == ('', '')
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ def test_memoize():
|
|||||||
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):
|
class TestGetClosest(object):
|
||||||
|
|
||||||
def test_when_can_match(self):
|
def test_when_can_match(self):
|
||||||
|
|||||||
@@ -29,13 +29,15 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
|
|||||||
'wait_command': 3,
|
'wait_command': 3,
|
||||||
'require_confirmation': False,
|
'require_confirmation': False,
|
||||||
'no_colors': False,
|
'no_colors': False,
|
||||||
|
'debug': False,
|
||||||
'priority': {}}
|
'priority': {}}
|
||||||
|
|
||||||
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 +89,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
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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,6 +80,7 @@ def get_command(settings, args):
|
|||||||
return
|
return
|
||||||
|
|
||||||
script = shells.from_shell(script)
|
script = shells.from_shell(script)
|
||||||
|
logs.debug('Call: {}'.format(script), settings)
|
||||||
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
|
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
|
||||||
env=dict(os.environ, LANG='C'))
|
env=dict(os.environ, LANG='C'))
|
||||||
if wait_output(settings, result):
|
if wait_output(settings, result):
|
||||||
@@ -90,6 +92,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 +128,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
24
thefuck/rules/history.py
Normal file
24
thefuck/rules/history.py
Normal 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
|
||||||
@@ -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:])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ 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
|
||||||
|
import six
|
||||||
from .utils import DEVNULL, memoize
|
from .utils import DEVNULL, memoize
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +50,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 +85,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 +100,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 +129,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 +170,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 +185,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 +200,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 +256,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())
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ 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):
|
def get_closest(word, possibilities, n=3, cutoff=0.6):
|
||||||
|
|||||||
Reference in New Issue
Block a user