1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-04 09:02:08 +00:00

Compare commits

..

42 Commits
1.33 ... 1.40

Author SHA1 Message Date
nvbn
0fc7c00e8d Bump to 1.40 2015-05-11 14:16:59 +02:00
nvbn
64318c09b7 #161 support different psutils versions 2015-05-11 14:16:23 +02:00
nvbn
5b6e17b5f1 Merge branch 'master' of github.com:nvbn/thefuck 2015-05-10 09:35:44 +02:00
nvbn
6cdc2c27fb #179 /c++1/cpp11/s 2015-05-10 09:35:02 +02:00
nvbn
62c605d0ac Merge branch 'C++11' of git://github.com/mcarton/thefuck into mcarton-C++11 2015-05-10 09:33:49 +02:00
mcarton
8930d01601 Update README.md to add the C++11 rule 2015-05-09 20:42:18 +02:00
mcarton
c749615ad6 Add a C++11 rule 2015-05-09 20:37:13 +02:00
Vladimir Iakovlev
f03d8c54b1 Merge pull request #177 from ja5h/master
Fixed grammar in README.txt
2015-05-09 19:30:20 +02:00
archilius777
20f1c76d27 Fixed grammar in README.txt 2015-05-09 22:56:35 +05:30
nvbn
f477cd69c2 Bump to 1.39 2015-05-09 18:53:49 +02:00
nvbn
690729d5a1 #176 Fix fails with wrong aliases 2015-05-09 18:53:36 +02:00
nvbn
f082ba829f Bump to 1.38 2015-05-08 15:27:33 +02:00
Vladimir Iakovlev
112e20d7c5 Merge pull request #171 from mcarton/dry
Add a don't repeat yourself rule
2015-05-08 12:11:41 +02:00
mcarton
95007220fb Add a test for the DRY rule 2015-05-08 11:42:00 +02:00
mcarton
56f636f3d8 Remove unnecessary space in the DRY rule 2015-05-08 11:41:26 +02:00
mcarton
932a7c5db5 Add a don't repeat yourself rule 2015-05-08 01:49:47 +02:00
Vladimir Iakovlev
1bed4d4e8d Merge pull request #170 from SanketDG/manfix
add rule for having no spaces in man commands.
2015-05-08 01:11:48 +02:00
Vladimir Iakovlev
e0bba379ff Merge pull request #169 from mcarton/git-checkout
Add the git_checkout rule
2015-05-08 01:11:09 +02:00
SanketDG
045959ec47 add man_no_space 2015-05-08 00:16:50 +05:30
SanketDG
65aeea857e add tests for man_no_space 2015-05-08 00:15:57 +05:30
SanketDG
793e883073 add man_no_space command 2015-05-08 00:15:32 +05:30
mcarton
a395ac568c Add the git_checkout rule
It creates a branch before checking-out to it if the branch does not
exist.
2015-05-07 20:32:04 +02:00
nvbn
29e70e14a0 Bump to 1.37 2015-05-07 14:16:17 +02:00
nvbn
0cdd23edcf Use wheel 2015-05-07 14:16:07 +02:00
nvbn
36d80859a4 Add tox config 2015-05-07 13:51:27 +02:00
nvbn
2b12b4bfce Improve tests with mocker 2015-05-07 13:42:52 +02:00
nvbn
91c1fe414a Update thefuck-alias entry point 2015-05-07 13:32:23 +02:00
nvbn
f3d377114e Bump to 1.36 2015-05-07 13:12:25 +02:00
nvbn
05f594b918 #154 Add ability to override priority in settings 2015-05-07 13:11:45 +02:00
nvbn
5bf1424613 #164 Decrease priority of no_command 2015-05-07 12:57:43 +02:00
nvbn
fc3fcf028a #154 Add priority to rules 2015-05-06 13:57:09 +02:00
nvbn
5864faadef #165 fix python 2 support 2015-05-06 13:17:14 +02:00
Vladimir Iakovlev
608d48e408 Merge pull request #166 from mcarton/git-add
Add a git_add rule
2015-05-06 13:10:23 +02:00
mcarton
9380eb1f56 Add a git_add rule 2015-05-06 11:31:31 +02:00
Vladimir Iakovlev
fb069b74d7 Merge pull request #159 from mcarton/master
Add a rule for pacman
2015-05-05 13:30:03 +02:00
mcarton
6624ecb3b8 Add a rule for pacman 2015-05-05 11:13:29 +02:00
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
27 changed files with 400 additions and 208 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).
@@ -71,7 +73,7 @@ REPL-y 0.3.1
... ...
``` ```
If you are scared to blindly run changed command, there's `require_confirmation` If you are scared to blindly run the changed command, there is a `require_confirmation`
[settings](#settings) option: [settings](#settings) option:
```bash ```bash
@@ -102,14 +104,20 @@ 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 the `.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'
``` ```
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`: Alternatively, you can redirect the output of `thefuck-alias`:
```bash ```bash
@@ -129,20 +137,26 @@ sudo pip install thefuck --upgrade
## How it works ## How it works
The Fuck tries to match rule for the previous command, create new command The Fuck tries to match a rule for the previous command, creates a new command
using matched rule and run it. Rules enabled by default: using the matched rule and runs it. Rules enabled by default are as follows:
* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`; * `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`;
* `cpp11` – add missing `-std=c++11` to `g++` or `clang++`;
* `cd_parent` – changes `cd..` to `cd ..`; * `cd_parent` – changes `cd..` to `cd ..`;
* `cd_mkdir` – creates directories before cd'ing into them; * `cd_mkdir` – creates directories before cd'ing into them;
* `cp_omitting_directory` – adds `-a` when you `cp` directory; * `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `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_checkout` – creates the branch before checking-out;
* `git_no_command` – fixes wrong git commands like `git brnch`; * `git_no_command` – fixes wrong git commands like `git brnch`;
* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`; * `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`;
* `has_exists_script` – prepends `./` when script/binary exists; * `has_exists_script` – prepends `./` when script/binary exists;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `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`;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`;
* `pacman` – installs app with `pacman` or `yaourt` if it is not installed;
* `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`; * `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`;
* `python_command` – prepends `python` when you trying to run not executable/without `./` python script; * `python_command` – prepends `python` when you trying to run not executable/without `./` python script;
* `sl_ls` – changes `sl` to `ls`; * `sl_ls` – changes `sl` to `ls`;
@@ -183,12 +197,14 @@ 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: # Optional:
enabled_by_default = True enabled_by_default = True
def side_effect(command, settings): def side_effect(command, settings):
subprocess.call('chmod 777 .', shell=True) subprocess.call('chmod 777 .', shell=True)
priority = 1000 # Lower first
``` ```
[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),
@@ -196,12 +212,13 @@ def side_effect(command, settings):
## Settings ## Settings
The Fuck has a few settings parameters, they 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` – require 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.
Example of `settings.py`: Example of `settings.py`:
@@ -210,6 +227,7 @@ rules = ['sudo', 'no_command']
require_confirmation = True require_confirmation = True
wait_command = 10 wait_command = 10
no_colors = False no_colors = False
priority = {'sudo': 100, 'no_command': 9999}
``` ```
Or via environment variables: Or via environment variables:
@@ -217,7 +235,9 @@ Or via environment variables:
* `THEFUCK_RULES` – list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`; * `THEFUCK_RULES` – list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`;
* `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`; * `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`;
* `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`,
rule with lower `priority` will be matched first.
For example: For example:
@@ -226,6 +246,7 @@ export THEFUCK_RULES='sudo:no_command'
export THEFUCK_REQUIRE_CONFIRMATION='true' export THEFUCK_REQUIRE_CONFIRMATION='true'
export THEFUCK_WAIT_COMMAND=10 export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false' export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
``` ```
## Developing ## Developing

View File

@@ -28,4 +28,4 @@ call('git commit -am "Bump to {}"'.format(version), shell=True)
call('git tag {}'.format(version), shell=True) call('git tag {}'.format(version), shell=True)
call('git push', shell=True) call('git push', shell=True)
call('git push --tags', shell=True) call('git push --tags', shell=True)
call('python setup.py sdist upload', shell=True) call('python setup.py sdist bdist_wheel upload', shell=True)

View File

@@ -1,2 +1,4 @@
pytest pytest
mock mock
pytest-mock
wheel

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View File

@@ -1,7 +1,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
VERSION = '1.33' VERSION = '1.40'
setup(name='thefuck', setup(name='thefuck',
@@ -17,4 +17,5 @@ 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-alias = thefuck.main:alias']}) 'thefuck = thefuck.main:main',
'thefuck-alias = thefuck.shells:app_alias']})

17
tests/rules/test_dry.py Normal file
View File

@@ -0,0 +1,17 @@
import pytest
from thefuck.rules.dry import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='cd cd foo'),
Command(script='git git push origin/master')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('cd cd foo'), 'cd foo'),
(Command('git git push origin/master'), 'git push origin/master')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,12 @@
from thefuck.rules.man_no_space import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('mandiff', stderr='mandiff: command not found'), None)
assert not match(Command(), None)
def test_get_new_command():
assert get_new_command(
Command('mandiff'), None) == 'man diff'

View File

@@ -14,10 +14,8 @@ def test_default(enabled, rules, result):
@pytest.fixture @pytest.fixture
def load_source(monkeypatch): def load_source(mocker):
mock = Mock() return mocker.patch('thefuck.conf.load_source')
monkeypatch.setattr('thefuck.conf.load_source', mock)
return mock
@pytest.fixture @pytest.fixture
@@ -40,12 +38,14 @@ class TestSettingsFromFile(object):
load_source.return_value = Mock(rules=['test'], 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,
priority={'vim': 100})
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
assert settings.priority == {'vim': 100}
def test_from_file_with_DEFAULT(self, load_source): def test_from_file_with_DEFAULT(self, load_source):
load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'], load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'],
@@ -62,12 +62,14 @@ class TestSettingsFromEnv(object):
environ.update({'THEFUCK_RULES': 'bash:lisp', environ.update({'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',
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15'})
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
assert settings.priority == {'bash': 10, 'vim': 15}
def test_from_env_with_DEFAULT(self, environ): def test_from_env_with_DEFAULT(self, environ):
environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})

View File

@@ -1,42 +0,0 @@
import pytest
from mock import Mock
from thefuck.history import History
class TestHistory(object):
@pytest.fixture(autouse=True)
def process(self, monkeypatch):
Process = Mock()
Process.return_value.parent.return_value.pid = 1
monkeypatch.setattr('thefuck.history.Process', Process)
return Process
@pytest.fixture(autouse=True)
def db(self, monkeypatch):
class DBMock(dict):
def __init__(self):
super(DBMock, self).__init__()
self.sync = Mock()
def __call__(self, *args, **kwargs):
return self
db = DBMock()
monkeypatch.setattr('thefuck.history.shelve.open', db)
return db
def test_set(self, db):
history = History()
history.update(last_command='ls',
last_fixed_command=None)
assert db == {'1-last_command': 'ls',
'1-last_fixed_command': None}
def test_get(self, db):
history = History()
db['1-last_command'] = 'cd ..'
assert history.last_command == 'cd ..'
def test_get_without_value(self):
history = History()
assert history.last_command is None

View File

@@ -6,34 +6,55 @@ from thefuck import main, conf, types
from tests.utils import Rule, Command from tests.utils import Rule, Command
def test_load_rule(monkeypatch): def test_load_rule(mocker):
match = object() match = object()
get_new_command = object() get_new_command = object()
load_source = Mock() load_source = mocker.patch(
load_source.return_value = Mock(match=match, 'thefuck.main.load_source',
get_new_command=get_new_command, return_value=Mock(match=match,
enabled_by_default=True) get_new_command=get_new_command,
monkeypatch.setattr('thefuck.main.load_source', load_source) enabled_by_default=True,
priority=900))
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, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py') load_source.assert_called_once_with('bash', '/rules/bash.py')
@pytest.mark.parametrize('conf_rules, rules', [ class TestGetRules(object):
(conf.DEFAULT_RULES, [Rule('bash', 'bash', 'bash'), @pytest.fixture(autouse=True)
Rule('lisp', 'lisp', 'lisp'), def glob(self, mocker):
Rule('bash', 'bash', 'bash'), return mocker.patch('thefuck.main.Path.glob', return_value=[])
Rule('lisp', 'lisp', 'lisp')]),
(types.RulesNamesList(['bash']), [Rule('bash', 'bash', 'bash'), def _compare_names(self, rules, names):
Rule('bash', 'bash', 'bash')])]) return [r.name for r in rules] == names
def test_get_rules(monkeypatch, conf_rules, rules):
monkeypatch.setattr( @pytest.mark.parametrize('conf_rules, rules', [
'thefuck.main.Path.glob', (conf.DEFAULT_RULES, ['bash', 'lisp', 'bash', 'lisp']),
lambda *_: [PosixPath('bash.py'), PosixPath('lisp.py')]) (types.RulesNamesList(['bash']), ['bash', 'bash'])])
monkeypatch.setattr('thefuck.main.load_source', def test_get(self, monkeypatch, glob, conf_rules, rules):
lambda x, _: Mock(match=x, get_new_command=x, glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
enabled_by_default=True)) monkeypatch.setattr('thefuck.main.load_source',
assert list(main.get_rules(Path('~'), Mock(rules=conf_rules))) == rules lambda x, _: Rule(x))
assert self._compare_names(
main.get_rules(Path('~'), Mock(rules=conf_rules, priority={})),
rules)
@pytest.mark.parametrize('priority, unordered, ordered', [
({},
[Rule('bash', priority=100), Rule('python', priority=5)],
['python', 'bash']),
({},
[Rule('lisp', priority=9999), Rule('c', priority=conf.DEFAULT_PRIORITY)],
['c', 'lisp']),
({'python': 9999},
[Rule('bash', priority=100), Rule('python', priority=5)],
['bash', 'python'])])
def test_ordered_by_priority(self, monkeypatch, priority, unordered, ordered):
monkeypatch.setattr('thefuck.main._get_loaded_rules',
lambda *_: unordered)
assert self._compare_names(
main.get_rules(Path('~'), Mock(priority=priority)),
ordered)
class TestGetCommand(object): class TestGetCommand(object):
@@ -56,7 +77,7 @@ 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(), Mock(), assert main.get_command(Mock(),
['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',
@@ -65,21 +86,14 @@ class TestGetCommand(object):
stderr=PIPE, stderr=PIPE,
env={'LANG': 'C'}) env={'LANG': 'C'})
@pytest.mark.parametrize('history, args, result', [ @pytest.mark.parametrize('args, result', [
(Mock(), [''], None), (['thefuck', 'ls', '-la'], 'ls -la'),
(Mock(last_command='ls', last_fixed_command='ls -la'), (['thefuck', 'ls'], 'ls')])
['thefuck', 'fuck'], 'ls -la'), def test_get_command_script(self, args, result):
(Mock(last_command='ls', last_fixed_command='ls -la'),
['thefuck', 'ls'], 'ls -la'),
(Mock(last_command='ls', last_fixed_command=''),
['thefuck', 'ls'], 'ls'),
(Mock(last_command='ls', last_fixed_command=''),
['thefuck', 'fuck'], 'ls')])
def test_get_command_script(self, history, args, result):
if result: if result:
assert main.get_command(Mock(), history, args).script == result assert main.get_command(Mock(), args).script == result
else: else:
assert main.get_command(Mock(), history, args) is None assert main.get_command(Mock(), args) is None
class TestGetMatchedRule(object): class TestGetMatchedRule(object):
@@ -102,14 +116,12 @@ class TestGetMatchedRule(object):
class TestRunRule(object): class TestRunRule(object):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def confirm(self, monkeypatch): def confirm(self, mocker):
mock = Mock(return_value=True) return mocker.patch('thefuck.main.confirm', return_value=True)
monkeypatch.setattr('thefuck.main.confirm', mock)
return mock
def test_run_rule(self, capsys): 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'),
Command(), Mock(), None) Command(), None)
assert capsys.readouterr() == ('new-command\n', '') assert capsys.readouterr() == ('new-command\n', '')
def test_run_rule_with_side_effect(self, capsys): def test_run_rule_with_side_effect(self, capsys):
@@ -118,30 +130,21 @@ class TestRunRule(object):
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),
command, Mock(), settings) command, settings)
assert capsys.readouterr() == ('new-command\n', '') assert capsys.readouterr() == ('new-command\n', '')
side_effect.assert_called_once_with(command, settings) side_effect.assert_called_once_with(command, settings)
def test_hisotry_updated(self):
history = Mock()
main.run_rule(Rule(get_new_command=lambda *_: 'ls -lah'),
Command('ls'), history, None)
history.update.assert_called_once_with(last_command='ls',
last_fixed_command='ls -lah')
def test_when_not_comfirmed(self, capsys, confirm): def test_when_not_comfirmed(self, capsys, confirm):
confirm.return_value = False confirm.return_value = False
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
Command(), Mock(), None) Command(), None)
assert capsys.readouterr() == ('', '') assert capsys.readouterr() == ('', '')
class TestConfirm(object): class TestConfirm(object):
@pytest.fixture @pytest.fixture
def stdin(self, monkeypatch): def stdin(self, mocker):
mock = Mock(return_value='\n') return mocker.patch('sys.stdin.read', return_value='\n')
monkeypatch.setattr('sys.stdin.read', mock)
return mock
def test_when_not_required(self, capsys): def test_when_not_required(self, capsys):
assert main.confirm('command', None, Mock(require_confirmation=False)) assert main.confirm('command', None, Mock(require_confirmation=False))

View File

@@ -1,21 +1,34 @@
import pytest import pytest
from mock import Mock
from thefuck import shells from thefuck import shells
@pytest.fixture
def builtins_open(mocker):
return mocker.patch('six.moves.builtins.open')
@pytest.fixture
def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True)
class TestGeneric(object): class TestGeneric(object):
def test_from_shell(self): def test_from_shell(self):
assert shells.Generic().from_shell('pwd') == 'pwd' assert shells.Generic().from_shell('pwd') == 'pwd'
def test_to_shell(self): def test_to_shell(self):
assert shells.Bash().to_shell('pwd') == 'pwd' 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): class TestBash(object):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def Popen(self, monkeypatch): def Popen(self, mocker):
mock = Mock() mock = mocker.patch('thefuck.shells.Popen')
monkeypatch.setattr('thefuck.shells.Popen', mock)
mock.return_value.stdout.read.return_value = ( mock.return_value.stdout.read.return_value = (
b'alias l=\'ls -CF\'\n' b'alias l=\'ls -CF\'\n'
b'alias la=\'ls -A\'\n' b'alias la=\'ls -A\'\n'
@@ -31,12 +44,17 @@ class TestBash(object):
def test_to_shell(self): def test_to_shell(self):
assert shells.Bash().to_shell('pwd') == 'pwd' 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): class TestZsh(object):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def Popen(self, monkeypatch): def Popen(self, mocker):
mock = Mock() mock = mocker.patch('thefuck.shells.Popen')
monkeypatch.setattr('thefuck.shells.Popen', mock)
mock.return_value.stdout.read.return_value = ( mock.return_value.stdout.read.return_value = (
b'l=\'ls -CF\'\n' b'l=\'ls -CF\'\n'
b'la=\'ls -A\'\n' b'la=\'ls -A\'\n'
@@ -50,4 +68,11 @@ class TestZsh(object):
assert shells.Zsh().from_shell(before) == after assert shells.Zsh().from_shell(before) == after
def test_to_shell(self): def test_to_shell(self):
assert shells.Zsh().to_shell('pwd') == 'pwd' assert shells.Zsh().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker):
mocker.patch('thefuck.shells.time',
return_value=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,4 +1,5 @@
from thefuck import types from thefuck import types
from thefuck.conf import DEFAULT_PRIORITY
def Command(script='', stdout='', stderr=''): def Command(script='', stdout='', stderr=''):
@@ -8,6 +9,8 @@ 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,
side_effect=None): side_effect=None,
priority=DEFAULT_PRIORITY):
return types.Rule(name, match, get_new_command, return types.Rule(name, match, get_new_command,
enabled_by_default, side_effect) enabled_by_default, side_effect,
priority)

View File

@@ -22,17 +22,20 @@ class _DefaultRulesNames(types.RulesNamesList):
DEFAULT_RULES = _DefaultRulesNames([]) DEFAULT_RULES = _DefaultRulesNames([])
DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3, 'wait_command': 3,
'require_confirmation': False, 'require_confirmation': False,
'no_colors': False} 'no_colors': False,
'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'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
@@ -65,16 +68,29 @@ def _rules_from_env(val):
return val return val
def _priority_from_env(val):
"""Gets priority pairs from env."""
for part in val.split(':'):
try:
rule, priority = part.split('=')
yield rule, int(priority)
except ValueError:
continue
def _val_from_env(env, attr): def _val_from_env(env, attr):
"""Transforms env-strings to python.""" """Transforms env-strings to python."""
val = os.environ[env] val = os.environ[env]
if attr == 'rules': if attr == 'rules':
val = _rules_from_env(val) return _rules_from_env(val)
elif attr == 'priority':
return dict(_priority_from_env(val))
elif attr == 'wait_command': elif attr == 'wait_command':
val = int(val) return int(val)
elif attr in ('require_confirmation', 'no_colors'): elif attr in ('require_confirmation', 'no_colors'):
val = val.lower() == 'true' return val.lower() == 'true'
return val else:
return val
def _settings_from_env(): def _settings_from_env():

View File

@@ -1,27 +0,0 @@
import os
import shelve
from tempfile import gettempdir
from psutil import Process
class History(object):
"""Temporary history of commands/fixed-commands dependent on
current shell instance.
"""
def __init__(self):
self._path = os.path.join(gettempdir(), '.thefuck_history')
self._pid = Process(os.getpid()).parent().pid
self._db = shelve.open(self._path)
def _prepare_key(self, key):
return '{}-{}'.format(self._pid, key)
def update(self, **kwargs):
self._db.update({self._prepare_key(k): v for k,v in kwargs.items()})
self._db.sync()
return self
def __getattr__(self, item):
return self._db.get(self._prepare_key(item))

View File

@@ -6,7 +6,7 @@ import os
import sys import sys
from psutil import Process, TimeoutExpired from psutil import Process, TimeoutExpired
import colorama import colorama
from .history import History import six
from . import logs, conf, types, shells from . import logs, conf, types, shells
@@ -26,7 +26,17 @@ def load_rule(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)) getattr(rule_module, 'side_effect', None),
getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY))
def _get_loaded_rules(rules, settings):
"""Yields all available rules."""
for rule in rules:
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
def get_rules(user_dir, settings): def get_rules(user_dir, settings):
@@ -35,11 +45,9 @@ def get_rules(user_dir, settings):
.joinpath('rules') \ .joinpath('rules') \
.glob('*.py') .glob('*.py')
user = user_dir.joinpath('rules').glob('*.py') user = user_dir.joinpath('rules').glob('*.py')
for rule in sorted(list(bundled)) + list(user): rules = _get_loaded_rules(sorted(bundled) + sorted(user), settings)
if rule.name != '__init__.py': return sorted(rules, key=lambda rule: settings.priority.get(
loaded_rule = load_rule(rule) rule.name, rule.priority))
if loaded_rule in settings.rules:
yield loaded_rule
def wait_output(settings, popen): def wait_output(settings, popen):
@@ -60,22 +68,17 @@ def wait_output(settings, popen):
return False return False
def get_command(settings, history, 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:])
if script == 'fuck' or script == history.last_command:
script = history.last_fixed_command or history.last_command
if not script: if not script:
return return
script = shells.from_shell(script) script = shells.from_shell(script)
history.update(last_command=script,
last_fixed_command=None)
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):
@@ -108,33 +111,27 @@ def confirm(new_command, side_effect, settings):
return False return False
def run_rule(rule, command, history, settings): def run_rule(rule, command, settings):
"""Runs command from rule for passed command.""" """Runs command from rule for passed command."""
new_command = shells.to_shell(rule.get_new_command(command, settings)) new_command = shells.to_shell(rule.get_new_command(command, settings))
if confirm(new_command, rule.side_effect, settings): if confirm(new_command, rule.side_effect, settings):
if rule.side_effect: if rule.side_effect:
rule.side_effect(command, settings) rule.side_effect(command, settings)
history.update(last_command=command.script, shells.put_to_history(new_command)
last_fixed_command=new_command)
print(new_command) print(new_command)
def alias():
print("\nalias fuck='eval $(thefuck $(fc -ln -1))'\n")
def main(): 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)
history = History()
command = get_command(settings, history, sys.argv) command = get_command(settings, sys.argv)
if command: if command:
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:
run_rule(matched_rule, command, history, settings) run_rule(matched_rule, command, settings)
return return
logs.failed('No fuck given', settings) logs.failed('No fuck given', settings)

View File

@@ -2,7 +2,7 @@ import difflib
import os import os
import re import re
import subprocess import subprocess
import thefuck.logs
BREW_CMD_PATH = '/Library/Homebrew/cmd' BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps' TAP_PATH = '/Library/Taps'
@@ -10,7 +10,7 @@ TAP_CMD_PATH = '/%s/%s/cmd'
def _get_brew_path_prefix(): def _get_brew_path_prefix():
'''To get brew path''' """To get brew path"""
try: try:
return subprocess.check_output(['brew', '--prefix']).strip() return subprocess.check_output(['brew', '--prefix']).strip()
except: except:
@@ -18,18 +18,18 @@ def _get_brew_path_prefix():
def _get_brew_commands(brew_path_prefix): def _get_brew_commands(brew_path_prefix):
'''To get brew default commands on local environment''' """To get brew default commands on local environment"""
brew_cmd_path = brew_path_prefix + BREW_CMD_PATH brew_cmd_path = brew_path_prefix + BREW_CMD_PATH
commands = (name.replace('.rb', '') for name in os.listdir(brew_cmd_path) commands = [name.replace('.rb', '') for name in os.listdir(brew_cmd_path)
if name.endswith('.rb')) if name.endswith('.rb')]
return commands return commands
def _get_brew_tap_specific_commands(brew_path_prefix): def _get_brew_tap_specific_commands(brew_path_prefix):
'''To get tap's specific commands """To get tap's specific commands
https://github.com/Homebrew/homebrew/blob/master/Library/brew.rb#L115''' https://github.com/Homebrew/homebrew/blob/master/Library/brew.rb#L115"""
commands = [] commands = []
brew_taps_path = brew_path_prefix + TAP_PATH brew_taps_path = brew_path_prefix + TAP_PATH
@@ -61,17 +61,20 @@ def _get_directory_names_only(path):
return [d for d in os.listdir(path) return [d for d in os.listdir(path)
if os.path.isdir(os.path.join(path, d))] if os.path.isdir(os.path.join(path, d))]
brew_commands = []
brew_path_prefix = _get_brew_path_prefix() brew_path_prefix = _get_brew_path_prefix()
# Failback commands for testing (Based on Homebrew 0.9.5)
brew_commands = ['info', 'home', 'options', 'install', 'uninstall',
'search', 'list', 'update', 'upgrade', 'pin', 'unpin',
'doctor', 'create', 'edit']
if brew_path_prefix: if brew_path_prefix:
brew_commands += _get_brew_commands(brew_path_prefix) try:
brew_commands += _get_brew_tap_specific_commands(brew_path_prefix) brew_commands = _get_brew_commands(brew_path_prefix) \
else: + _get_brew_tap_specific_commands(brew_path_prefix)
# Failback commands for testing (Based on Homebrew 0.9.5) except OSError:
brew_commands = ['info', 'home', 'options', 'install', 'uninstall', pass
'search', 'list', 'update', 'upgrade', 'pin', 'unpin',
'doctor', 'create', 'edit']
def _get_similar_commands(command): def _get_similar_commands(command):

9
thefuck/rules/cpp11.py Normal file
View File

@@ -0,0 +1,9 @@
def match(command, settings):
return (('g++' in command.script or 'clang++' in command.script) and
('This file requires compiler and library support for the '
'ISO C++ 2011 standard.' in command.stderr or
'-Wc++11-extensions' in command.stderr))
def get_new_command(command, settings):
return command.script + ' -std=c++11'

12
thefuck/rules/dry.py Normal file
View File

@@ -0,0 +1,12 @@
def match(command, settings):
split_command = command.script.split()
return len(split_command) >= 2 and split_command[0] == split_command[1]
def get_new_command(command, settings):
return command.script[command.script.find(' ')+1:]
# it should be rare enough to actually have to type twice the same word, so
# this rule can have a higher priority to come before things like "cd cd foo"
priority = 900

15
thefuck/rules/git_add.py Normal file
View File

@@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" in command.stderr)
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
return 'git add -- {} && {}'.format(missing_file, command.script)

View File

@@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" not in command.stderr)
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
return 'git branch {} && {}'.format(missing_file, command.script)

View File

@@ -0,0 +1,9 @@
def match(command, settings):
return (command.script.startswith(u'man')
and u'command not found' in command.stderr.lower())
def get_new_command(command, settings):
return u'man {}'.format(command.script[3:])
priority = 2000

View File

@@ -31,3 +31,6 @@ def get_new_command(command, settings):
new_command = get_close_matches(old_command, new_command = get_close_matches(old_command,
_get_all_bins())[0] _get_all_bins())[0]
return ' '.join([new_command] + command.script.split(' ')[1:]) return ' '.join([new_command] + command.script.split(' ')[1:])
priority = 3000

43
thefuck/rules/pacman.py Normal file
View File

@@ -0,0 +1,43 @@
import subprocess
from thefuck.utils import DEVNULL
def __command_available(command):
try:
subprocess.check_output([command], stderr=DEVNULL)
return True
except subprocess.CalledProcessError:
# command exists but is not happy to be called without any argument
return True
except OSError:
return False
def __get_pkgfile(command):
try:
return subprocess.check_output(
['pkgfile', '-b', '-v', command.script.split(" ")[0]],
universal_newlines=True, stderr=subprocess.DEVNULL
).split()
except subprocess.CalledProcessError:
return None
def match(command, settings):
return 'not found' in command.stderr and __get_pkgfile(command)
def get_new_command(command, settings):
package = __get_pkgfile(command)[0]
return '{} -S {} && {}'.format(pacman, package, command.script)
if not __command_available('pkgfile'):
enabled_by_default = False
elif __command_available('yaourt'):
pacman = 'yaourt'
elif __command_available('pacman'):
pacman = 'sudo pacman'
else:
enabled_by_default = False

View File

@@ -1,14 +1,14 @@
"""Module with shell specific actions, each shell class should """Module with shell specific actions, each shell class should
implement `from_shell` and `to_shell` methods. implement `from_shell`, `to_shell`, `app_alias` and `put_to_history`
methods.
""" """
from collections import defaultdict from collections import defaultdict
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from time import time
import os import os
from psutil import Process from psutil import Process
from .utils import DEVNULL
FNULL = open(os.devnull, 'w')
class Generic(object): class Generic(object):
@@ -31,6 +31,22 @@ class Generic(object):
"""Prepares command for running in shell.""" """Prepares command for running in shell."""
return command_script 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): class Bash(Generic):
def _parse_alias(self, alias): def _parse_alias(self, alias):
@@ -40,11 +56,18 @@ class Bash(Generic):
return name, value return name, value
def _get_aliases(self): def _get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=FNULL, shell=True) proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
return dict( return dict(
self._parse_alias(alias) self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias) if alias and '=' in 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): class Zsh(Generic):
@@ -55,11 +78,18 @@ class Zsh(Generic):
return name, value return name, value
def _get_aliases(self): def _get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=FNULL, shell=True) proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
return dict( return dict(
self._parse_alias(alias) self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n') for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias) if alias and '=' in 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(), { shells = defaultdict(lambda: Generic(), {
@@ -68,7 +98,10 @@ shells = defaultdict(lambda: Generic(), {
def _get_shell(): def _get_shell():
shell = Process(os.getpid()).parent().cmdline()[0] try:
shell = Process(os.getpid()).parent().cmdline()[0]
except TypeError:
shell = Process(os.getpid()).parent.cmdline[0]
return shells[shell] return shells[shell]
@@ -78,3 +111,11 @@ def from_shell(command):
def to_shell(command): def to_shell(command):
return _get_shell().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,7 +4,8 @@ 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', 'side_effect')) 'enabled_by_default', 'side_effect',
'priority'))
class RulesNamesList(list): class RulesNamesList(list):

View File

@@ -4,6 +4,9 @@ import six
from .types import Command from .types import Command
DEVNULL = open(os.devnull, 'w')
def which(program): def which(program):
"""Returns `program` path or `None`.""" """Returns `program` path or `None`."""

6
tox.ini Normal file
View File

@@ -0,0 +1,6 @@
[tox]
envlist = py27,py33,py34
[testenv]
deps = -rrequirements.txt
commands = py.test