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

Compare commits

...

29 Commits
1.31 ... 1.35

Author SHA1 Message Date
nvbn
a8ff2375c0 Bump to 1.35 2015-05-04 05:01:56 +02:00
Vladimir Iakovlev
80bfbec422 Update README.md 2015-05-04 05:00:11 +02:00
nvbn
3f2fe0d275 #89 #152 Use shell history 2015-05-04 04:44:16 +02:00
nvbn
72ac9650f9 Bump to 1.34 2015-05-03 13:25:01 +02:00
nvbn
93c90d5758 #157 Don't fail if can't get brew commands 2015-05-03 13:24:33 +02:00
nvbn
3ce8c1187c Make thefuck-alias depends on current shell 2015-05-03 13:04:33 +02:00
nvbn
bcd3154121 Bump to 1.33 2015-05-03 12:59:37 +02:00
nvbn
fcc2a1a40a #128 #69 add support of shell specific actions, add alias expansion for bash and zsh 2015-05-03 12:46:01 +02:00
nvbn
938f1df035 Remove not used fixture 2015-05-02 04:56:23 +02:00
nvbn
2acfea3350 #1 s/last_script/last_command/, s/last_fixed_script/last_fixed_command/ 2015-05-02 04:32:07 +02:00
nvbn
dd1861955c Refine tests 2015-05-02 04:29:55 +02:00
nvbn
ba601644d6 #1 Add history of last commands, allow fuck more than once 2015-05-01 08:38:38 +02:00
nvbn
fb7376f5a5 Bump to 1.32 2015-05-01 04:47:25 +02:00
nvbn
ee5c40d427 Update rules list in readme 2015-05-01 04:46:58 +02:00
nvbn
9a43ba6e24 #102 Update readme 2015-05-01 04:43:55 +02:00
nvbn
5eeb9d704c #102 Use side_effect in ssh_known_host rule 2015-05-01 04:41:33 +02:00
nvbn
b985dfbffc #102 Add support of rules with side effects 2015-05-01 04:39:37 +02:00
Vladimir Iakovlev
b928a59672 Merge pull request #150 from SanketDG/add-alias
Add thefuck-alias for outputting the alias command.
2015-04-30 20:53:22 +02:00
SanketDG
32fd929e48 add instructions to use thefuck-alias 2015-05-01 00:13:08 +05:30
SanketDG
8a49b40f6a add entry point 2015-05-01 00:12:43 +05:30
SanketDG
4276e1b991 add alias function 2015-05-01 00:12:30 +05:30
nvbn
6372674351 Merge branch 'SanketDG-sudo-shutdown' 2015-04-30 19:57:01 +02:00
nvbn
9f9c5369ec Merge branch 'sudo-shutdown' of https://github.com/SanketDG/thefuck into SanketDG-sudo-shutdown
Conflicts:
	thefuck/rules/sudo.py
2015-04-30 19:56:45 +02:00
Vladimir Iakovlev
50ab7429d9 Merge pull request #148 from danybony/patch-1
Add more patterns to sudo.py
2015-04-30 19:50:59 +02:00
SanketDG
55cfdda203 add rule for shutdown command 2015-04-30 19:50:37 +05:30
Daniele
be9446635b Add more patterns to sudo.py
These patterns cover commands like
`reboot`
or
`dpkg-reconfigure something`
2015-04-30 13:54:02 +01:00
Vladimir Iakovlev
b4cbcd7a99 Merge pull request #146 from kimtree/brew-improve
Improve a logic to get recommended command based on local environment
2015-04-29 08:48:20 +02:00
Namwoo Kim
9bf910a2dd Improve a logic to get recommended command based on local environment 2015-04-29 15:18:48 +09:00
Vladimir Iakovlev
7e76ab1dc6 Fix typo 2015-04-29 05:06:30 +02:00
17 changed files with 572 additions and 197 deletions

View File

@@ -1,5 +1,7 @@
# 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)
**Aliases changed in 1.34.**
Magnificent app which corrects your previous console command, 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).
@@ -102,15 +104,27 @@ sudo pip install thefuck
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation) [Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX): And add to `.bashrc` or `.bash_profile`(for OSX):
```bash ```bash
alias fuck='eval $(thefuck $(fc -ln -1))' alias fuck='eval $(thefuck $(fc -ln -1)); history -r'
# You can use whatever you want as an alias, like for Mondays: # You can use whatever you want as an alias, like for Mondays:
alias FUCK='fuck' alias FUCK='fuck'
``` ```
[On in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases) Or in your `.zshrc`:
```bash
alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'
```
Alternatively, you can redirect the output of `thefuck-alias`:
```bash
thefuck-alias >> ~/.bashrc
```
[Or in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases)
Changes will be available only in a new shell session. Changes will be available only in a new shell session.
@@ -143,7 +157,10 @@ using matched rule and run it. Rules enabled by default:
* `rm_dir` – adds `-rf` when you trying to remove directory; * `rm_dir` – adds `-rf` when you trying to remove directory;
* `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;
* `switch_layout` – switches command from your local layout to en. * `switch_layout` – switches command from your local layout to en;
* `apt_get` – installs app from apt if it not installed;
* `brew_install` – fixes formula name for `brew install`;
* `composer_not_command` – fixes composer command name.
Bundled, but not enabled by default: Bundled, but not enabled by default:
@@ -156,6 +173,9 @@ For adding your own rule you should create `your-rule-name.py`
in `~/.thefuck/rules`. Rule should contain two functions: in `~/.thefuck/rules`. Rule should contain two functions:
`match(command: Command, settings: Settings) -> bool` `match(command: Command, settings: Settings) -> bool`
and `get_new_command(command: Command, settings: Settings) -> str`. and `get_new_command(command: Command, settings: Settings) -> str`.
Also the rule can contain optional function
`side_effect(command: Command, settings: Settings) -> None` and
optional boolean `enabled_by_default`
`Command` has three attributes: `script`, `stdout` and `stderr`. `Command` has three attributes: `script`, `stdout` and `stderr`.
@@ -171,6 +191,12 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
return 'sudo {}'.format(command.script) return 'sudo {}'.format(command.script)
# Optional:
enabled_by_default = True
def side_effect(command, settings):
subprocess.call('chmod 777 .', shell=True)
``` ```
[More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules), [More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules),

View File

@@ -1,7 +1,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
VERSION = '1.31' VERSION = '1.35'
setup(name='thefuck', setup(name='thefuck',
@@ -17,4 +17,4 @@ setup(name='thefuck',
zip_safe=False, zip_safe=False,
install_requires=['pathlib', 'psutil', 'colorama', 'six'], install_requires=['pathlib', 'psutil', 'colorama', 'six'],
entry_points={'console_scripts': [ entry_points={'console_scripts': [
'thefuck = thefuck.main:main']}) 'thefuck = thefuck.main:main', 'thefuck-alias = thefuck.main:alias']})

View File

@@ -10,7 +10,7 @@ def brew_unknown_cmd():
@pytest.fixture @pytest.fixture
def brew_unknown_cmd_instaa(): def brew_unknown_cmd2():
return '''Error: Unknown command: instaa''' return '''Error: Unknown command: instaa'''
@@ -20,9 +20,9 @@ def test_match(brew_unknown_cmd):
assert not match(Command('brew ' + command), None) assert not match(Command('brew ' + command), None)
def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd_instaa): def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd2):
assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd), None)\ assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd),
== 'brew list' None) == 'brew list'
assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd_instaa), assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd2),
None) == 'brew install' None) == 'brew install'

View File

@@ -2,7 +2,7 @@ import os
import pytest import pytest
from mock import Mock from mock import Mock
from thefuck.rules.ssh_known_hosts import match, get_new_command,\ from thefuck.rules.ssh_known_hosts import match, get_new_command,\
remove_offending_keys side_effect
from tests.utils import Command from tests.utils import Command
@@ -53,18 +53,14 @@ def test_match(ssh_error):
assert not match(Command('ssh'), None) assert not match(Command('ssh'), None)
def test_remove_offending_keys(ssh_error): def test_side_effect(ssh_error):
errormsg, path, reset, known_hosts = ssh_error errormsg, path, reset, known_hosts = ssh_error
command = Command('ssh user@host', stderr=errormsg) command = Command('ssh user@host', stderr=errormsg)
remove_offending_keys(command, None) side_effect(command, None)
expected = ['123.234.567.890 asdjkasjdakjsd\n', '111.222.333.444 qwepoiwqepoiss\n'] expected = ['123.234.567.890 asdjkasjdakjsd\n', '111.222.333.444 qwepoiwqepoiss\n']
assert known_hosts(path) == expected assert known_hosts(path) == expected
def test_get_new_command(ssh_error, monkeypatch): def test_get_new_command(ssh_error, monkeypatch):
errormsg, _, _, _ = ssh_error errormsg, _, _, _ = ssh_error
method = Mock()
monkeypatch.setattr('thefuck.rules.ssh_known_hosts.remove_offending_keys', method)
assert get_new_command(Command('ssh user@host', stderr=errormsg), None) == 'ssh user@host' assert get_new_command(Command('ssh user@host', stderr=errormsg), None) == 'ssh user@host'
assert method.call_count

View File

@@ -1,83 +1,94 @@
import pytest
import six import six
from mock import patch, Mock from mock import Mock
from thefuck import conf from thefuck import conf
from tests.utils import Rule from tests.utils import Rule
def test_default(): @pytest.mark.parametrize('enabled, rules, result', [
assert Rule('test', enabled_by_default=True) in conf.DEFAULT_RULES (True, conf.DEFAULT_RULES, True),
assert Rule('test', enabled_by_default=False) not in conf.DEFAULT_RULES (False, conf.DEFAULT_RULES, False),
assert Rule('test', enabled_by_default=False) in (conf.DEFAULT_RULES + ['test']) (False, conf.DEFAULT_RULES + ['test'], True)])
def test_default(enabled, rules, result):
assert (Rule('test', enabled_by_default=enabled) in rules) == result
def test_settings_defaults(): @pytest.fixture
with patch('thefuck.conf.load_source', return_value=object()), \ def load_source(monkeypatch):
patch('thefuck.conf.os.environ', new_callable=lambda: {}): mock = Mock()
monkeypatch.setattr('thefuck.conf.load_source', mock)
return mock
@pytest.fixture
def environ(monkeypatch):
data = {}
monkeypatch.setattr('thefuck.conf.os.environ', data)
return data
@pytest.mark.usefixture('environ')
def test_settings_defaults(load_source):
load_source.return_value = object()
for key, val in conf.DEFAULT_SETTINGS.items(): for key, val in conf.DEFAULT_SETTINGS.items():
assert getattr(conf.get_settings(Mock()), key) == val assert getattr(conf.get_settings(Mock()), key) == val
def test_settings_from_file(): @pytest.mark.usefixture('environ')
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'], class TestSettingsFromFile(object):
def test_from_file(self, load_source):
load_source.return_value = Mock(rules=['test'],
wait_command=10, wait_command=10,
require_confirmation=True, require_confirmation=True,
no_colors=True)), \ no_colors=True)
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
settings = conf.get_settings(Mock()) settings = conf.get_settings(Mock())
assert settings.rules == ['test'] assert settings.rules == ['test']
assert settings.wait_command == 10 assert settings.wait_command == 10
assert settings.require_confirmation is True assert settings.require_confirmation is True
assert settings.no_colors is True assert settings.no_colors is True
def test_from_file_with_DEFAULT(self, load_source):
def test_settings_from_file_with_DEFAULT(): load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'],
with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT_RULES + ['test'],
wait_command=10, wait_command=10,
require_confirmation=True, require_confirmation=True,
no_colors=True)), \ no_colors=True)
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
settings = conf.get_settings(Mock()) settings = conf.get_settings(Mock())
assert settings.rules == conf.DEFAULT_RULES + ['test'] assert settings.rules == conf.DEFAULT_RULES + ['test']
def test_settings_from_env(): @pytest.mark.usefixture('load_source')
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'], class TestSettingsFromEnv(object):
wait_command=10)), \ def test_from_env(self, environ):
patch('thefuck.conf.os.environ', environ.update({'THEFUCK_RULES': 'bash:lisp',
new_callable=lambda: {'THEFUCK_RULES': 'bash:lisp',
'THEFUCK_WAIT_COMMAND': '55', 'THEFUCK_WAIT_COMMAND': '55',
'THEFUCK_REQUIRE_CONFIRMATION': 'true', 'THEFUCK_REQUIRE_CONFIRMATION': 'true',
'THEFUCK_NO_COLORS': 'false'}): 'THEFUCK_NO_COLORS': 'false'})
settings = conf.get_settings(Mock()) settings = conf.get_settings(Mock())
assert settings.rules == ['bash', 'lisp'] assert settings.rules == ['bash', 'lisp']
assert settings.wait_command == 55 assert settings.wait_command == 55
assert settings.require_confirmation is True assert settings.require_confirmation is True
assert settings.no_colors is False assert settings.no_colors is False
def test_from_env_with_DEFAULT(self, environ):
def test_settings_from_env_with_DEFAULT(): environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})
with patch('thefuck.conf.load_source', return_value=Mock()), \
patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}):
settings = conf.get_settings(Mock()) settings = conf.get_settings(Mock())
assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp'] assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp']
def test_initialize_settings_file_ignore_if_exists(): class TestInitializeSettingsFile(object):
def test_ignore_if_exists(self):
settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock()) settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock())
user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock))
conf.initialize_settings_file(user_dir_mock) conf.initialize_settings_file(user_dir_mock)
assert settings_path_mock.is_file.call_count == 1 assert settings_path_mock.is_file.call_count == 1
assert not settings_path_mock.open.called assert not settings_path_mock.open.called
def test_create_if_doesnt_exists(self):
def test_initialize_settings_file_create_if_exists_not():
settings_file = six.StringIO() settings_file = six.StringIO()
settings_path_mock = Mock( settings_path_mock = Mock(
is_file=Mock(return_value=False), is_file=Mock(return_value=False),
open=Mock(return_value=Mock( open=Mock(return_value=Mock(
__exit__=lambda *args: None, __enter__=lambda *args: settings_file __exit__=lambda *args: None, __enter__=lambda *args: settings_file)))
)),
)
user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock))
conf.initialize_settings_file(user_dir_mock) conf.initialize_settings_file(user_dir_mock)
settings_file_contents = settings_file.getvalue() settings_file_contents = settings_file.getvalue()

View File

@@ -1,96 +1,155 @@
import pytest
from subprocess import PIPE from subprocess import PIPE
from pathlib import PosixPath, Path from pathlib import PosixPath, Path
from mock import patch, Mock from mock import Mock
from thefuck import main, conf, types from thefuck import main, conf, types
from tests.utils import Rule, Command from tests.utils import Rule, Command
def test_load_rule(): def test_load_rule(monkeypatch):
match = object() match = object()
get_new_command = object() get_new_command = object()
with patch('thefuck.main.load_source', load_source = Mock()
return_value=Mock( load_source.return_value = Mock(match=match,
match=match,
get_new_command=get_new_command, get_new_command=get_new_command,
enabled_by_default=True)) as load_source: enabled_by_default=True)
monkeypatch.setattr('thefuck.main.load_source', load_source)
assert main.load_rule(Path('/rules/bash.py')) \ assert main.load_rule(Path('/rules/bash.py')) \
== Rule('bash', match, get_new_command) == Rule('bash', match, get_new_command)
load_source.assert_called_once_with('bash', '/rules/bash.py') load_source.assert_called_once_with('bash', '/rules/bash.py')
def test_get_rules(): @pytest.mark.parametrize('conf_rules, rules', [
with patch('thefuck.main.Path.glob') as glob, \ (conf.DEFAULT_RULES, [Rule('bash', 'bash', 'bash'),
patch('thefuck.main.load_source',
lambda x, _: Mock(match=x, get_new_command=x,
enabled_by_default=True)):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
assert list(main.get_rules(
Path('~'),
Mock(rules=conf.DEFAULT_RULES))) \
== [Rule('bash', 'bash', 'bash'),
Rule('lisp', 'lisp', 'lisp'), Rule('lisp', 'lisp', 'lisp'),
Rule('bash', 'bash', 'bash'), Rule('bash', 'bash', 'bash'),
Rule('lisp', 'lisp', 'lisp')] Rule('lisp', 'lisp', 'lisp')]),
assert list(main.get_rules( (types.RulesNamesList(['bash']), [Rule('bash', 'bash', 'bash'),
Path('~'), Rule('bash', 'bash', 'bash')])])
Mock(rules=types.RulesNamesList(['bash'])))) \ def test_get_rules(monkeypatch, conf_rules, rules):
== [Rule('bash', 'bash', 'bash'), monkeypatch.setattr(
Rule('bash', 'bash', 'bash')] 'thefuck.main.Path.glob',
lambda *_: [PosixPath('bash.py'), PosixPath('lisp.py')])
monkeypatch.setattr('thefuck.main.load_source',
lambda x, _: Mock(match=x, get_new_command=x,
enabled_by_default=True))
assert list(main.get_rules(Path('~'), Mock(rules=conf_rules))) == rules
def test_get_command(): class TestGetCommand(object):
with patch('thefuck.main.Popen') as Popen, \ @pytest.fixture(autouse=True)
patch('thefuck.main.os.environ', def Popen(self, monkeypatch):
new_callable=lambda: {}), \ Popen = Mock()
patch('thefuck.main.wait_output',
return_value=True):
Popen.return_value.stdout.read.return_value = b'stdout' Popen.return_value.stdout.read.return_value = b'stdout'
Popen.return_value.stderr.read.return_value = b'stderr' Popen.return_value.stderr.read.return_value = b'stderr'
assert main.get_command(Mock(), ['thefuck', 'apt-get', monkeypatch.setattr('thefuck.main.Popen', Popen)
'search', 'vim']) \ return Popen
@pytest.fixture(autouse=True)
def prepare(self, monkeypatch):
monkeypatch.setattr('thefuck.main.os.environ', {})
monkeypatch.setattr('thefuck.main.wait_output', lambda *_: True)
@pytest.fixture(autouse=True)
def generic_shell(self, monkeypatch):
monkeypatch.setattr('thefuck.shells.from_shell', lambda x: x)
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_get_command_calls(self, Popen):
assert main.get_command(Mock(),
['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={'LANG': 'C'})
assert main.get_command(Mock(), ['']) is None @pytest.mark.parametrize('args, result', [
(['thefuck', 'ls', '-la'], 'ls -la'),
(['thefuck', 'ls'], 'ls')])
def test_get_command_script(self, args, result):
if result:
assert main.get_command(Mock(), args).script == result
else:
assert main.get_command(Mock(), args) is None
def test_get_matched_rule(capsys): class TestGetMatchedRule(object):
rules = [Rule('', lambda x, _: x.script == 'cd ..'), def test_no_match(self):
Rule('', lambda *_: False), assert main.get_matched_rule(
Rule('rule', Mock(side_effect=OSError('Denied')))] Command('ls'), [Rule('', lambda *_: False)],
assert main.get_matched_rule(Command('ls'), Mock(no_colors=True)) is None
rules, Mock(no_colors=True)) is None
assert main.get_matched_rule(Command('cd ..'), def test_match(self):
rules, Mock(no_colors=True)) == rules[0] rule = Rule('', lambda x, _: x.script == 'cd ..')
assert capsys.readouterr()[1].split('\n')[0] \ assert main.get_matched_rule(
== '[WARN] Rule rule:' Command('cd ..'), [rule], Mock(no_colors=True)) == rule
def test_when_rule_failed(self, capsys):
main.get_matched_rule(
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))],
Mock(no_colors=True))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
def test_run_rule(capsys): class TestRunRule(object):
with patch('thefuck.main.confirm', return_value=True): @pytest.fixture(autouse=True)
def confirm(self, monkeypatch):
mock = Mock(return_value=True)
monkeypatch.setattr('thefuck.main.confirm', mock)
return mock
def test_run_rule(self, capsys):
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
None, None) Command(), None)
assert capsys.readouterr() == ('new-command\n', '') assert capsys.readouterr() == ('new-command\n', '')
with patch('thefuck.main.confirm', return_value=False):
def test_run_rule_with_side_effect(self, capsys):
side_effect = Mock()
settings = Mock()
command = Command()
main.run_rule(Rule(get_new_command=lambda *_: 'new-command',
side_effect=side_effect),
command, settings)
assert capsys.readouterr() == ('new-command\n', '')
side_effect.assert_called_once_with(command, settings)
def test_when_not_comfirmed(self, capsys, confirm):
confirm.return_value = False
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
None, None) Command(), None)
assert capsys.readouterr() == ('', '') assert capsys.readouterr() == ('', '')
def test_confirm(capsys): class TestConfirm(object):
# When confirmation not required: @pytest.fixture
assert main.confirm('command', Mock(require_confirmation=False)) def stdin(self, monkeypatch):
mock = Mock(return_value='\n')
monkeypatch.setattr('sys.stdin.read', mock)
return mock
def test_when_not_required(self, capsys):
assert main.confirm('command', None, Mock(require_confirmation=False))
assert capsys.readouterr() == ('', 'command\n') assert capsys.readouterr() == ('', 'command\n')
# When confirmation required and confirmed:
with patch('thefuck.main.sys.stdin.read', return_value='\n'): def test_with_side_effect_and_without_confirmation(self, capsys):
assert main.confirm('command', Mock(require_confirmation=True, assert main.confirm('command', Mock(), Mock(require_confirmation=False))
assert capsys.readouterr() == ('', 'command*\n')
# `stdin` fixture should be applied after `capsys`
def test_when_confirmation_required_and_confirmed(self, capsys, stdin):
assert main.confirm('command', None, Mock(require_confirmation=True,
no_colors=True)) no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]') assert capsys.readouterr() == ('', 'command [enter/ctrl+c]')
# When confirmation required and ctrl+c:
with patch('thefuck.main.sys.stdin.read', side_effect=KeyboardInterrupt): # `stdin` fixture should be applied after `capsys`
assert not main.confirm('command', Mock(require_confirmation=True, def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin):
assert main.confirm('command', Mock(), Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command* [enter/ctrl+c]')
def test_when_confirmation_required_and_aborted(self, capsys, stdin):
stdin.side_effect = KeyboardInterrupt
assert not main.confirm('command', None, Mock(require_confirmation=True,
no_colors=True)) no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n') assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n')

85
tests/test_shells.py Normal file
View File

@@ -0,0 +1,85 @@
import pytest
from mock import Mock, MagicMock
from thefuck import shells
@pytest.fixture
def builtins_open(monkeypatch):
mock = MagicMock()
monkeypatch.setattr('six.moves.builtins.open', mock)
return mock
@pytest.fixture
def isfile(monkeypatch):
mock = Mock(return_value=True)
monkeypatch.setattr('os.path.isfile', mock)
return mock
class TestGeneric(object):
def test_from_shell(self):
assert shells.Generic().from_shell('pwd') == 'pwd'
def test_to_shell(self):
assert shells.Generic().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open):
assert shells.Generic().put_to_history('ls') is None
assert builtins_open.call_count == 0
@pytest.mark.usefixtures('isfile')
class TestBash(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.shells.Popen', mock)
mock.return_value.stdout.read.return_value = (
b'alias l=\'ls -CF\'\n'
b'alias la=\'ls -A\'\n'
b'alias ll=\'ls -alF\'')
return mock
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after):
assert shells.Bash().from_shell(before) == after
def test_to_shell(self):
assert shells.Bash().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open):
shells.Bash().put_to_history('ls')
builtins_open.return_value.__enter__.return_value.\
write.assert_called_once_with('ls\n')
@pytest.mark.usefixtures('isfile')
class TestZsh(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.shells.Popen', mock)
mock.return_value.stdout.read.return_value = (
b'l=\'ls -CF\'\n'
b'la=\'ls -A\'\n'
b'll=\'ls -alF\'')
return mock
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after):
assert shells.Zsh().from_shell(before) == after
def test_to_shell(self):
assert shells.Zsh().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, monkeypatch):
monkeypatch.setattr('thefuck.shells.time',
lambda: 1430707243.3517463)
shells.Zsh().put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(': 1430707243:0;ls\n')

View File

@@ -1,11 +1,12 @@
from thefuck.types import Rule, RulesNamesList, Settings from thefuck.types import RulesNamesList, Settings
from tests.utils import Rule
def test_rules_names_list(): def test_rules_names_list():
assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp'] assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp']
assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp']) assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp'])
assert Rule('lisp', None, None, False) in RulesNamesList(['lisp']) assert Rule('lisp') in RulesNamesList(['lisp'])
assert Rule('bash', None, None, False) not in RulesNamesList(['lisp']) assert Rule('bash') not in RulesNamesList(['lisp'])
def test_update_settings(): def test_update_settings():

View File

@@ -1,26 +1,26 @@
import pytest
from mock import Mock from mock import Mock
from thefuck.utils import sudo_support, wrap_settings from thefuck.utils import sudo_support, wrap_settings
from thefuck.types import Settings from thefuck.types import Settings
from tests.utils import Command from tests.utils import Command
def test_wrap_settings(): @pytest.mark.parametrize('override, old, new', [
({'key': 'val'}, {}, {'key': 'val'}),
({'key': 'new-val'}, {'key': 'val'}, {'key': 'new-val'})])
def test_wrap_settings(override, old, new):
fn = lambda _, settings: settings fn = lambda _, settings: settings
assert wrap_settings({'key': 'val'})(fn)(None, Settings({})) \ assert wrap_settings(override)(fn)(None, Settings(old)) == new
== {'key': 'val'}
assert wrap_settings({'key': 'new-val'})(fn)(
None, Settings({'key': 'val'})) == {'key': 'new-val'}
def test_sudo_support(): @pytest.mark.parametrize('return_value, command, called, result', [
fn = Mock(return_value=True, __name__='') ('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'),
assert sudo_support(fn)(Command('sudo ls'), None) ('ls -lah', 'ls', 'ls', 'ls -lah'),
fn.assert_called_once_with(Command('ls'), None) (True, 'sudo ls', 'ls', True),
(True, 'ls', 'ls', True),
fn.return_value = False (False, 'sudo ls', 'ls', False),
assert not sudo_support(fn)(Command('sudo ls'), None) (False, 'ls', 'ls', False)])
def test_sudo_support(return_value, command, called, result):
fn.return_value = 'pwd' fn = Mock(return_value=return_value, __name__='')
assert sudo_support(fn)(Command('sudo ls'), None) == 'sudo pwd' assert sudo_support(fn)(Command(command), None) == result
fn.assert_called_once_with(Command(called), None)
assert sudo_support(fn)(Command('ls'), None) == 'pwd'

View File

@@ -7,5 +7,7 @@ def Command(script='', stdout='', stderr=''):
def Rule(name='', match=lambda *_: True, def Rule(name='', match=lambda *_: True,
get_new_command=lambda *_: '', get_new_command=lambda *_: '',
enabled_by_default=True): enabled_by_default=True,
return types.Rule(name, match, get_new_command, enabled_by_default) side_effect=None):
return types.Rule(name, match, get_new_command,
enabled_by_default, side_effect)

View File

@@ -26,17 +26,20 @@ def rule_failed(rule, exc_info, settings):
exception('Rule {}'.format(rule.name), exc_info, settings) exception('Rule {}'.format(rule.name), exc_info, settings)
def show_command(new_command, settings): def show_command(new_command, side_effect, settings):
sys.stderr.write('{bold}{command}{reset}\n'.format( sys.stderr.write('{bold}{command}{side_effect}{reset}\n'.format(
command=new_command, command=new_command,
side_effect='*' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings), bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings))) reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, settings): def confirm_command(new_command, side_effect, settings):
sys.stderr.write( sys.stderr.write(
'{bold}{command}{reset} [{green}enter{reset}/{red}ctrl+c{reset}]'.format( '{bold}{command}{side_effect}{reset} '
'[{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command, command=new_command,
side_effect='*' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings), bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings), green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings), red=color(colorama.Fore.RED, settings),

View File

@@ -6,7 +6,8 @@ import os
import sys import sys
from psutil import Process, TimeoutExpired from psutil import Process, TimeoutExpired
import colorama import colorama
from . import logs, conf, types import six
from . import logs, conf, types, shells
def setup_user_dir(): def setup_user_dir():
@@ -24,7 +25,8 @@ def load_rule(rule):
rule_module = load_source(rule.name[:-3], str(rule)) rule_module = load_source(rule.name[:-3], str(rule))
return types.Rule(rule.name[:-3], rule_module.match, return types.Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command, rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True)) getattr(rule_module, 'enabled_by_default', True),
getattr(rule_module, 'side_effect', None))
def get_rules(user_dir, settings): def get_rules(user_dir, settings):
@@ -60,7 +62,7 @@ def wait_output(settings, popen):
def get_command(settings, args): def get_command(settings, args):
"""Creates command from `args` and executes it.""" """Creates command from `args` and executes it."""
if sys.version_info[0] < 3: if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in args[1:]) script = ' '.join(arg.decode('utf-8') for arg in args[1:])
else: else:
script = ' '.join(args[1:]) script = ' '.join(args[1:])
@@ -68,6 +70,7 @@ def get_command(settings, args):
if not script: if not script:
return return
script = shells.from_shell(script)
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):
@@ -85,13 +88,13 @@ def get_matched_rule(command, rules, settings):
logs.rule_failed(rule, sys.exc_info(), settings) logs.rule_failed(rule, sys.exc_info(), settings)
def confirm(new_command, settings): def confirm(new_command, side_effect, settings):
"""Returns `True` when running of new command confirmed.""" """Returns `True` when running of new command confirmed."""
if not settings.require_confirmation: if not settings.require_confirmation:
logs.show_command(new_command, settings) logs.show_command(new_command, side_effect, settings)
return True return True
logs.confirm_command(new_command, settings) logs.confirm_command(new_command, side_effect, settings)
try: try:
sys.stdin.read(1) sys.stdin.read(1)
return True return True
@@ -102,14 +105,16 @@ def confirm(new_command, settings):
def run_rule(rule, command, settings): def run_rule(rule, command, settings):
"""Runs command from rule for passed command.""" """Runs command from rule for passed command."""
new_command = rule.get_new_command(command, settings) new_command = shells.to_shell(rule.get_new_command(command, settings))
if confirm(new_command, settings): if confirm(new_command, rule.side_effect, settings):
if rule.side_effect:
rule.side_effect(command, settings)
shells.put_to_history(new_command)
print(new_command) print(new_command)
def is_second_run(command): def alias():
"""Is it the second run of `fuck`?""" print(shells.app_alias())
return command.script.startswith('fuck')
def main(): def main():
@@ -119,10 +124,6 @@ def main():
command = get_command(settings, sys.argv) command = get_command(settings, sys.argv)
if command: if command:
if is_second_run(command):
logs.failed("Can't fuck twice", settings)
return
rules = get_rules(user_dir, settings) rules = get_rules(user_dir, settings)
matched_rule = get_matched_rule(command, rules, settings) matched_rule = get_matched_rule(command, rules, settings)
if matched_rule: if matched_rule:

View File

@@ -1,11 +1,80 @@
import difflib import difflib
import os
import re import re
import thefuck.logs import subprocess
# This commands are based on Homebrew 0.9.5
brew_commands = ['info', 'home', 'options', 'install', 'uninstall', 'search', BREW_CMD_PATH = '/Library/Homebrew/cmd'
'list', 'update', 'upgrade', 'pin', 'unpin', 'doctor', TAP_PATH = '/Library/Taps'
'create', 'edit'] TAP_CMD_PATH = '/%s/%s/cmd'
def _get_brew_path_prefix():
"""To get brew path"""
try:
return subprocess.check_output(['brew', '--prefix']).strip()
except:
return None
def _get_brew_commands(brew_path_prefix):
"""To get brew default commands on local environment"""
brew_cmd_path = brew_path_prefix + BREW_CMD_PATH
commands = [name.replace('.rb', '') for name in os.listdir(brew_cmd_path)
if name.endswith('.rb')]
return commands
def _get_brew_tap_specific_commands(brew_path_prefix):
"""To get tap's specific commands
https://github.com/Homebrew/homebrew/blob/master/Library/brew.rb#L115"""
commands = []
brew_taps_path = brew_path_prefix + TAP_PATH
for user in _get_directory_names_only(brew_taps_path):
taps = _get_directory_names_only(brew_taps_path + '/%s' % user)
# Brew Taps's naming rule
# https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/brew-tap.md#naming-conventions-and-limitations
taps = (tap for tap in taps if tap.startswith('homebrew-'))
for tap in taps:
tap_cmd_path = brew_taps_path + TAP_CMD_PATH % (user, tap)
if os.path.isdir(tap_cmd_path):
commands += (name.replace('brew-', '').replace('.rb', '')
for name in os.listdir(tap_cmd_path)
if _is_brew_tap_cmd_naming(name))
return commands
def _is_brew_tap_cmd_naming(name):
if name.startswith('brew-') and name.endswith('.rb'):
return True
return False
def _get_directory_names_only(path):
return [d for d in os.listdir(path)
if os.path.isdir(os.path.join(path, d))]
brew_path_prefix = _get_brew_path_prefix()
# Failback commands for testing (Based on Homebrew 0.9.5)
brew_commands = ['info', 'home', 'options', 'install', 'uninstall',
'search', 'list', 'update', 'upgrade', 'pin', 'unpin',
'doctor', 'create', 'edit']
if brew_path_prefix:
try:
brew_commands = _get_brew_commands(brew_path_prefix) \
+ _get_brew_tap_specific_commands(brew_path_prefix)
except OSError:
pass
def _get_similar_commands(command): def _get_similar_commands(command):

View File

@@ -22,7 +22,11 @@ def match(command, settings):
return True return True
def remove_offending_keys(command, settings): def get_new_command(command, settings):
return command.script
def side_effect(command, settings):
offending = offending_pattern.findall(command.stderr) offending = offending_pattern.findall(command.stderr)
for filepath, lineno in offending: for filepath, lineno in offending:
with open(filepath, 'r') as fh: with open(filepath, 'r') as fh:
@@ -30,8 +34,3 @@ def remove_offending_keys(command, settings):
del lines[int(lineno) - 1] del lines[int(lineno) - 1]
with open(filepath, 'w') as fh: with open(filepath, 'w') as fh:
fh.writelines(lines) fh.writelines(lines)
def get_new_command(command, settings):
remove_offending_keys(command, settings)
return command.script

View File

@@ -8,7 +8,10 @@ patterns = ['permission denied',
'This command has to be run under the root user.', 'This command has to be run under the root user.',
'This operation requires root.', 'This operation requires root.',
'You need to be root to perform this command.', 'You need to be root to perform this command.',
'requested operation requires superuser privilege'] 'requested operation requires superuser privilege',
'must be run as root',
'must be superuser',
'Need to be root']
def match(command, settings): def match(command, settings):

120
thefuck/shells.py Normal file
View File

@@ -0,0 +1,120 @@
"""Module with shell specific actions, each shell class should
implement `from_shell`, `to_shell`, `app_alias` and `put_to_history`
methods.
"""
from collections import defaultdict
from subprocess import Popen, PIPE
from time import time
import os
from psutil import Process
FNULL = open(os.devnull, 'w')
class Generic(object):
def _get_aliases(self):
return {}
def _expand_aliases(self, command_script):
aliases = self._get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return command_script.replace(binary, aliases[binary], 1)
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def to_shell(self, command_script):
"""Prepares command for running in shell."""
return command_script
def app_alias(self):
return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n"
def _get_history_file_name(self):
return ''
def _get_history_line(self, command_script):
return ''
def put_to_history(self, command_script):
"""Puts command script to shell history."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script))
class Bash(Generic):
def _parse_alias(self, alias):
name, value = alias.replace('alias ', '', 1).split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
def _get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=FNULL, shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.bash_history'))
def _get_history_line(self, command_script):
return u'{}\n'.format(command_script)
class Zsh(Generic):
def _parse_alias(self, alias):
name, value = alias.split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
def _get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=FNULL, shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.zsh_history'))
def _get_history_line(self, command_script):
return u': {}:0;{}\n'.format(int(time()), command_script)
shells = defaultdict(lambda: Generic(), {
'bash': Bash(),
'zsh': Zsh()})
def _get_shell():
shell = Process(os.getpid()).parent().cmdline()[0]
return shells[shell]
def from_shell(command):
return _get_shell().from_shell(command)
def to_shell(command):
return _get_shell().to_shell(command)
def app_alias():
return _get_shell().app_alias()
def put_to_history(command):
return _get_shell().put_to_history(command)

View File

@@ -4,11 +4,11 @@ from collections import namedtuple
Command = namedtuple('Command', ('script', 'stdout', 'stderr')) Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default')) 'enabled_by_default', 'side_effect'))
class RulesNamesList(list): class RulesNamesList(list):
"""Wrapper a top of list for string rules names.""" """Wrapper a top of list for storing rules names."""
def __contains__(self, item): def __contains__(self, item):
return super(RulesNamesList, self).__contains__(item.name) return super(RulesNamesList, self).__contains__(item.name)