1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-01 15:42:06 +00:00

Compare commits

...

45 Commits
1.23 ... 1.28

Author SHA1 Message Date
nvbn
0272e8a801 Bump to 1.28 2015-04-22 23:37:02 +02:00
nvbn
2e652112ff Merge branch 'master' of github.com:nvbn/thefuck 2015-04-22 23:36:43 +02:00
Vladimir Iakovlev
12eab10028 Update README.md 2015-04-22 23:08:10 +02:00
Vladimir Iakovlev
61eab83789 Merge pull request #101 from scott-abernethy/brew-install
Brew installation note in README
2015-04-22 23:05:25 +02:00
nvbn
d3d1f99232 Move special data types to types 2015-04-22 23:04:22 +02:00
Scott Abernethy
ca67080bd9 Brew installation note in README 2015-04-23 09:00:18 +12:00
nvbn
54c408a6b5 Rename DEFAULT to DEFAULT_RULES 2015-04-22 22:37:11 +02:00
nvbn
20b6c4c160 Inherit RulesNamesList from list 2015-04-22 22:36:18 +02:00
nvbn
0553d57ec1 Don't mess with inheritance for filling settings 2015-04-22 22:29:23 +02:00
Vladimir Iakovlev
e046d55de8 Merge pull request #99 from timofurrer/master
fix rm dir rule to make it case insensitive
2015-04-22 20:20:00 +02:00
nvbn
69a9516477 Add ability to change settings via environment variables 2015-04-22 20:18:53 +02:00
Timo Furrer
c788dfbc14 fix rm dir rule to make it case insensitive
In bash the output for the command `rm -f foo/` is:

    rm: cannot remove ‘foo/’: Is a directory

And not:

    rm: cannot remove ‘foo/’: is a directory
2015-04-22 19:04:52 +02:00
nvbn
b4b599df80 Update readme 2015-04-22 16:52:09 +02:00
nvbn
69ddd82bae Bump to 1.27 2015-04-22 16:46:06 +02:00
nvbn
e7b78205f4 Add transparent sudo support for rules where it required 2015-04-22 16:45:38 +02:00
nvbn
7010b3a7f6 #43 Add test for rm_root 2015-04-22 16:22:10 +02:00
nvbn
3a9c2cc204 Merge branch 'SpyCheese-patch-1' 2015-04-22 16:09:09 +02:00
nvbn
fa4e4522b7 #43 Add rm_root as disabled by default rule 2015-04-22 16:08:54 +02:00
nvbn
14ef5c7d1c Merge branch 'patch-1' of git://github.com/SpyCheese/thefuck into SpyCheese-patch-1 2015-04-22 16:03:20 +02:00
nvbn
957209bdb6 Add ability to bundle disabled by default rules 2015-04-22 15:59:44 +02:00
nvbn
8376fed459 Merge branch 'master' of github.com:nvbn/thefuck 2015-04-22 06:03:34 +02:00
nvbn
5d424dad88 Use colorama for colored output 2015-04-22 06:03:06 +02:00
nvbn
126194ec2e Put errors in stderr instead of "echo ..." in stdout 2015-04-22 05:29:44 +02:00
Vladimir Iakovlev
6b54a3a072 Merge pull request #88 from Dugucloud/master
Added sudo rule for Fedora yum's output.
2015-04-22 05:15:24 +02:00
Dugucloud
79fb7c987c Added sudo rule for Fedora yum's output. 2015-04-22 09:26:45 +08:00
秋纫
d2356c570e Merge pull request #1 from nvbn/master
Synchronize with nvbn's repo.
2015-04-22 09:23:20 +08:00
nvbn
d1b1465f4e Bump to 1.26 2015-04-21 22:31:01 +02:00
nvbn
564eb55262 Merge branch 'master' of github.com:nvbn/thefuck 2015-04-21 22:30:38 +02:00
nvbn
20f8a4ad17 Bump to 1.24 2015-04-21 22:30:15 +02:00
Vladimir Iakovlev
a794b58729 Merge pull request #86 from dionyziz/switch_lang_greek
Add Greek to switch lang
2015-04-21 22:19:23 +02:00
nvbn
fdd6144f88 Merge branch 'nicwest-ssh-known-hosts' 2015-04-21 22:11:10 +02:00
nvbn
d1416a6c2a #82 Remove unned print, fix python 3 support 2015-04-21 22:10:53 +02:00
Dionysis Zindros
4f10fe647d Add tests for greek langage 2015-04-21 22:09:48 +02:00
nvbn
3df77b5bad Merge branch 'ssh-known-hosts' of git://github.com/nicwest/thefuck into nicwest-ssh-known-hosts 2015-04-21 22:06:21 +02:00
Vladimir Iakovlev
da013c5c99 Merge pull request #84 from SanketDG/test_cd_parent
Add tests for cd_parent command.
2015-04-21 22:01:57 +02:00
Dionysis Zindros
4b8d4926aa Add Greek to switch lang 2015-04-21 22:00:05 +02:00
SanketDG
2a7cbef3b5 add tests for cd_parent 2015-04-21 23:41:49 +05:30
Nic West
943613a194 add thing for when known hosts have changed 2015-04-21 17:05:52 +01:00
Vladimir Iakovlev
5b97992d50 Merge pull request #77 from madmatt112/master
Fix spelling mistake
2015-04-21 16:55:39 +02:00
Matthew Field
3f21d5fc3f Fix spelling mistake 2015-04-21 08:47:14 -06:00
Vladimir Iakovlev
d90e093fb7 Merge pull request #75 from installgen2/patch-1
Fix broken settings link in README
2015-04-21 14:53:08 +02:00
Gen2
8e18ff6eab Fix broken settings link in README 2015-04-21 13:46:38 +01:00
SpyCheese
ceeccf1cd7 Update rm_root.py
Okay, there was an incorrect match function.
2015-04-19 10:21:46 +05:00
SpyCheese
f113bae59d Update rm_root.py 2015-04-19 09:12:19 +05:00
SpyCheese
2a79a5e413 Create rm_root.py 2015-04-19 09:03:34 +05:00
35 changed files with 619 additions and 105 deletions

View File

@@ -71,8 +71,8 @@ REPL-y 0.3.1
...
```
If you are scary to blindly run changed command, there's `require_confirmation`
[settings](#Settings) option:
If you are scared to blindly run changed command, there's `require_confirmation`
[settings](#settings) option:
```bash
➜ apt-get install vim
@@ -106,6 +106,12 @@ If it fails try to use `easy_install`:
sudo easy_install thefuck
```
Or using an OS package manager (currently supported in OSX via [brew](http://brew.sh)):
```bash
brew install thefuck
```
And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX):
```bash
@@ -158,9 +164,14 @@ using matched rule and run it. Rules enabled by default:
* `no_command` – fixes wrong console commands, for example `vom/vim`;
* `python_command` – prepends `python` when you trying to run not executable/without `./` python script;
* `rm_dir` – adds `-rf` when you trying to remove directory;
* `ssh_known_hosts` – removes host from `known_hosts` on warning;
* `sudo` – prepends `sudo` to previous command if it failed because of permissions;
* `switch_layout` – switches command from your local layout to en.
Bundled, but not enabled by default:
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules
For adding your own rule you should create `your-rule-name.py`
@@ -170,7 +181,7 @@ and `get_new_command(command: Command, settings: Settings) -> str`.
`Command` has three attributes: `script`, `stdout` and `stderr`.
`Settings` is `~/.thefuck/settings.py`.
`Settings` is a special object filled with `~/.thefuck/settings.py` and values from env, [more](#settings).
Simple example of the rule for running script with `sudo`:
@@ -191,9 +202,17 @@ def get_new_command(command, settings):
The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`:
* `rules` – list of enabled rules, by default all;
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` – require 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.
Or via environment variables:
* `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_WAIT_COMMAND` – max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`.
## Developing

31
release.py Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python
from subprocess import call
import re
version = None
def get_new_setup_py_lines():
global version
with open('setup.py', 'r') as sf:
current_setup = sf.readlines()
for line in current_setup:
if line.startswith('VERSION = '):
major, minor = re.findall(r"VERSION = '(\d+)\.(\d+)'", line)[0]
version = "{}.{}".format(major, int(minor) + 1)
yield "VERSION = '{}'\n".format(version)
else:
yield line
lines = list(get_new_setup_py_lines())
with open('setup.py', 'w') as sf:
sf.writelines(lines)
call('git pull', shell=True)
call('git commit -am "Bump to {}"'.format(version), shell=True)
call('git tag {}'.format(version), shell=True)
call('git push', shell=True)
call('git push --tags', shell=True)
call('python setup.py sdist upload', shell=True)

View File

@@ -1,16 +1,20 @@
from setuptools import setup, find_packages
VERSION = '1.28'
setup(name='thefuck',
version="1.23",
version=VERSION,
description="Magnificent app which corrects your previous console command",
author='Vladimir Iakovlev',
author_email='nvbn.rm@gmail.com',
url='https://github.com/nvbn/thefuck',
license='MIT',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
packages=find_packages(exclude=['ez_setup', 'examples',
'tests', 'release']),
include_package_data=True,
zip_safe=False,
install_requires=['pathlib', 'psutil'],
install_requires=['pathlib', 'psutil', 'colorama', 'six'],
entry_points={'console_scripts': [
'thefuck = thefuck.main:main']})

View File

@@ -0,0 +1,12 @@
from thefuck.types import Command
from thefuck.rules.cd_parent import match, get_new_command
def test_match():
assert match(Command('cd..', '', 'cd..: command not found'), None)
assert not match(Command('', '', ''), None)
def test_get_new_command():
assert get_new_command(
Command('cd..', '', ''), None) == 'cd ..'

View File

@@ -1,5 +1,5 @@
import pytest
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.git_not_command import match, get_new_command

View File

@@ -1,5 +1,5 @@
import pytest
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.git_push import match, get_new_command

View File

@@ -1,5 +1,5 @@
from mock import Mock, patch
from thefuck.rules. has_exists_script import match, get_new_command
from thefuck.rules.has_exists_script import match, get_new_command
def test_match():

View File

@@ -1,4 +1,4 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.mkdir_p import match, get_new_command

View File

@@ -1,9 +1,11 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.python_command import match, get_new_command
def test_match():
assert match(Command('temp.py', '', 'Permission denied'), None)
assert not match(Command('', '', ''), None)
def test_get_new_command():
assert get_new_command(Command('./test_sudo.py', '', ''), None) == 'python ./test_sudo.py'

View File

@@ -1,9 +1,10 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.rm_dir import match, get_new_command
def test_match():
assert match(Command('rm foo', '', 'rm: foo: is a directory'), None)
assert match(Command('rm foo', '', 'rm: foo: Is a directory'), None)
assert not match(Command('rm foo', '', ''), None)
assert not match(Command('rm foo', '', 'foo bar baz'), None)
assert not match(Command('', '', ''), None)

View File

@@ -0,0 +1,18 @@
from mock import Mock
from thefuck.rules.rm_root import match, get_new_command
def test_match():
assert match(Mock(script='rm -rf /',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='ls',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='rm --no-preserve-root /',
stderr='add --no-preserve-root'), None)
assert not match(Mock(script='rm -rf /',
stderr=''), None)
def test_get_new_command():
assert get_new_command(Mock(script='rm -rf /'), None) \
== 'rm -rf / --no-preserve-root'

View File

@@ -0,0 +1,69 @@
import os
import pytest
from mock import Mock
from thefuck.types import Command
from thefuck.rules.ssh_known_hosts import match, get_new_command, remove_offending_keys
@pytest.fixture
def ssh_error(tmpdir):
path = os.path.join(str(tmpdir), 'known_hosts')
def reset(path):
with open(path, 'w') as fh:
lines = [
'123.234.567.890 asdjkasjdakjsd\n'
'98.765.432.321 ejioweojwejrosj\n'
'111.222.333.444 qwepoiwqepoiss\n'
]
fh.writelines(lines)
def known_hosts(path):
with open(path, 'r') as fh:
return fh.readlines()
reset(path)
errormsg = u"""@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
b6:cb:07:34:c0:a0:94:d3:0d:69:83:31:f4:c5:20:9b.
Please contact your system administrator.
Add correct host key in {0} to get rid of this message.
Offending RSA key in {0}:2
RSA host key for {1} has changed and you have requested strict checking.
Host key verification failed.""".format(path, '98.765.432.321')
return errormsg, path, reset, known_hosts
def test_match(ssh_error):
errormsg, _, _, _ = ssh_error
assert match(Command('ssh', '', errormsg), None)
assert match(Command('ssh', '', errormsg), None)
assert match(Command('scp something something', '', errormsg), None)
assert match(Command('scp something something', '', errormsg), None)
assert not match(Command('', '', errormsg), None)
assert not match(Command('notssh', '', errormsg), None)
assert not match(Command('ssh', '', ''), None)
def test_remove_offending_keys(ssh_error):
errormsg, path, reset, known_hosts = ssh_error
command = Command('ssh user@host', '', errormsg)
remove_offending_keys(command, None)
expected = ['123.234.567.890 asdjkasjdakjsd\n', '111.222.333.444 qwepoiwqepoiss\n']
assert known_hosts(path) == expected
def test_get_new_command(ssh_error, monkeypatch):
errormsg, _, _, _ = ssh_error
method = Mock()
monkeypatch.setattr('thefuck.rules.ssh_known_hosts.remove_offending_keys', method)
assert get_new_command(Command('ssh user@host', '', errormsg), None) == 'ssh user@host'
assert method.call_count

View File

@@ -1,4 +1,4 @@
from thefuck.main import Command
from thefuck.types import Command
from thefuck.rules.sudo import match, get_new_command

View File

@@ -7,8 +7,13 @@ from thefuck.rules import switch_lang
def test_match():
assert switch_lang.match(Mock(stderr='command not found: фзе-пуе',
script=u'фзе-пуе'), None)
assert switch_lang.match(Mock(stderr='command not found: λσ',
script=u'λσ'), None)
assert not switch_lang.match(Mock(stderr='command not found: pat-get',
script=u'pat-get'), None)
assert not switch_lang.match(Mock(stderr='command not found: ls',
script=u'ls'), None)
assert not switch_lang.match(Mock(stderr='some info',
script=u'фзе-пуе'), None)
@@ -16,3 +21,5 @@ def test_match():
def test_get_new_command():
assert switch_lang.get_new_command(
Mock(script=u'фзе-пуе штыефдд мшь'), None) == 'apt-get install vim'
assert switch_lang.get_new_command(
Mock(script=u'λσ -λα'), None) == 'ls -la'

61
tests/test_conf.py Normal file
View File

@@ -0,0 +1,61 @@
from mock import patch, Mock
from thefuck.types import Rule
from thefuck import conf
def test_default():
assert Rule('test', None, None, True) in conf.DEFAULT_RULES
assert Rule('test', None, None, False) not in conf.DEFAULT_RULES
assert Rule('test', None, None, False) in (conf.DEFAULT_RULES + ['test'])
def test_settings_defaults():
with patch('thefuck.conf.load_source', return_value=object()), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
for key, val in conf.DEFAULT_SETTINGS.items():
assert getattr(conf.get_settings(Mock()), key) == val
def test_settings_from_file():
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
settings = conf.get_settings(Mock())
assert settings.rules == ['test']
assert settings.wait_command == 10
assert settings.require_confirmation is True
assert settings.no_colors is True
def test_settings_from_file_with_DEFAULT():
with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT_RULES + ['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
settings = conf.get_settings(Mock())
assert settings.rules == conf.DEFAULT_RULES + ['test']
def test_settings_from_env():
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'],
wait_command=10)), \
patch('thefuck.conf.os.environ',
new_callable=lambda: {'THEFUCK_RULES': 'bash:lisp',
'THEFUCK_WAIT_COMMAND': '55',
'THEFUCK_REQUIRE_CONFIRMATION': 'true',
'THEFUCK_NO_COLORS': 'false'}):
settings = conf.get_settings(Mock())
assert settings.rules == ['bash', 'lisp']
assert settings.wait_command == 55
assert settings.require_confirmation is True
assert settings.no_colors is False
def test_settings_from_env_with_DEFAULT():
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())
assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp']

7
tests/test_logs.py Normal file
View File

@@ -0,0 +1,7 @@
from mock import Mock
from thefuck import logs
def test_color():
assert logs.color('red', Mock(no_colors=False)) == 'red'
assert logs.color('red', Mock(no_colors=True)) == ''

View File

@@ -1,20 +1,7 @@
from subprocess import PIPE
from pathlib import PosixPath, Path
from mock import patch, Mock
from thefuck import main
def test_get_settings():
with patch('thefuck.main.load_source', return_value=Mock(rules=['bash'])):
assert main.get_settings(Path('/')).rules == ['bash']
with patch('thefuck.main.load_source', return_value=Mock(spec=[])):
assert main.get_settings(Path('/')).rules is None
def test_is_rule_enabled():
assert main.is_rule_enabled(Mock(rules=None), Path('bash.py'))
assert main.is_rule_enabled(Mock(rules=['bash']), Path('bash.py'))
assert not main.is_rule_enabled(Mock(rules=['bash']), Path('lisp.py'))
from thefuck import main, conf, types
def test_load_rule():
@@ -23,26 +10,31 @@ def test_load_rule():
with patch('thefuck.main.load_source',
return_value=Mock(
match=match,
get_new_command=get_new_command)) as load_source:
assert main.load_rule(Path('/rules/bash.py')) == main.Rule('bash', match, get_new_command)
get_new_command=get_new_command,
enabled_by_default=True)) as load_source:
assert main.load_rule(Path('/rules/bash.py')) \
== types.Rule('bash', match, get_new_command, True)
load_source.assert_called_once_with('bash', '/rules/bash.py')
def test_get_rules():
with patch('thefuck.main.Path.glob') as glob, \
patch('thefuck.main.load_source',
lambda x, _: Mock(match=x, get_new_command=x)):
lambda x, _: Mock(match=x, get_new_command=x,
enabled_by_default=True)):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
assert main.get_rules(
assert list(main.get_rules(
Path('~'),
Mock(rules=None)) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('lisp', 'lisp', 'lisp'),
main.Rule('bash', 'bash', 'bash'),
main.Rule('lisp', 'lisp', 'lisp')]
assert main.get_rules(
Mock(rules=conf.DEFAULT_RULES))) \
== [types.Rule('bash', 'bash', 'bash', True),
types.Rule('lisp', 'lisp', 'lisp', True),
types.Rule('bash', 'bash', 'bash', True),
types.Rule('lisp', 'lisp', 'lisp', True)]
assert list(main.get_rules(
Path('~'),
Mock(rules=['bash'])) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('bash', 'bash', 'bash')]
Mock(rules=types.RulesNamesList(['bash'])))) \
== [types.Rule('bash', 'bash', 'bash', True),
types.Rule('bash', 'bash', 'bash', True)]
def test_get_command():
@@ -55,7 +47,7 @@ def test_get_command():
Popen.return_value.stderr.read.return_value = b'stderr'
assert main.get_command(Mock(), ['thefuck', 'apt-get',
'search', 'vim']) \
== main.Command('apt-get search vim', 'stdout', 'stderr')
== types.Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdout=PIPE,
@@ -65,24 +57,24 @@ def test_get_command():
def test_get_matched_rule(capsys):
rules = [main.Rule('', lambda x, _: x.script == 'cd ..', None),
main.Rule('', lambda *_: False, None),
main.Rule('rule', Mock(side_effect=OSError('Denied')), None)]
assert main.get_matched_rule(main.Command('ls', '', ''),
rules, None) is None
assert main.get_matched_rule(main.Command('cd ..', '', ''),
rules, None) == rules[0]
assert capsys.readouterr()[1].split('\n')[0]\
== '[WARN] rule: Traceback (most recent call last):'
rules = [types.Rule('', lambda x, _: x.script == 'cd ..', None, True),
types.Rule('', lambda *_: False, None, True),
types.Rule('rule', Mock(side_effect=OSError('Denied')), None, True)]
assert main.get_matched_rule(types.Command('ls', '', ''),
rules, Mock(no_colors=True)) is None
assert main.get_matched_rule(types.Command('cd ..', '', ''),
rules, Mock(no_colors=True)) == rules[0]
assert capsys.readouterr()[1].split('\n')[0] \
== '[WARN] Rule rule:'
def test_run_rule(capsys):
with patch('thefuck.main.confirm', return_value=True):
main.run_rule(main.Rule('', None, lambda *_: 'new-command'),
main.run_rule(types.Rule('', None, lambda *_: 'new-command', True),
None, None)
assert capsys.readouterr() == ('new-command\n', '')
with patch('thefuck.main.confirm', return_value=False):
main.run_rule(main.Rule('', None, lambda *_: 'new-command'),
main.run_rule(types.Rule('', None, lambda *_: 'new-command', True),
None, None)
assert capsys.readouterr() == ('', '')
@@ -93,9 +85,11 @@ def test_confirm(capsys):
assert capsys.readouterr() == ('', 'command\n')
# When confirmation required and confirmed:
with patch('thefuck.main.sys.stdin.read', return_value='\n'):
assert main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]')
assert main.confirm('command', Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]')
# When confirmation required and ctrl+c:
with patch('thefuck.main.sys.stdin.read', side_effect=KeyboardInterrupt):
assert not main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]Aborted\n')
assert not main.confirm('command', Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n')

15
tests/test_types.py Normal file
View File

@@ -0,0 +1,15 @@
from thefuck.types import Rule, RulesNamesList, Settings
def test_rules_names_list():
assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp']
assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp'])
assert Rule('lisp', None, None, False) in RulesNamesList(['lisp'])
assert Rule('bash', None, None, False) not in RulesNamesList(['lisp'])
def test_update_settings():
settings = Settings({'key': 'val'})
new_settings = settings.update(key='new-val')
assert new_settings.key == 'new-val'
assert settings.key == 'val'

25
tests/test_utils.py Normal file
View File

@@ -0,0 +1,25 @@
from mock import Mock
from thefuck.utils import sudo_support, wrap_settings
from thefuck.types import Command, Settings
def test_wrap_settings():
fn = lambda _, settings: settings
assert wrap_settings({'key': 'val'})(fn)(None, Settings({})) \
== {'key': 'val'}
assert wrap_settings({'key': 'new-val'})(fn)(
None, Settings({'key': 'val'})) == {'key': 'new-val'}
def test_sudo_support():
fn = Mock(return_value=True, __name__='')
assert sudo_support(fn)(Command('sudo ls', 'out', 'err'), None)
fn.assert_called_once_with(Command('ls', 'out', 'err'), None)
fn.return_value = False
assert not sudo_support(fn)(Command('sudo ls', 'out', 'err'), None)
fn.return_value = 'pwd'
assert sudo_support(fn)(Command('sudo ls', 'out', 'err'), None) == 'sudo pwd'
assert sudo_support(fn)(Command('ls', 'out', 'err'), None) == 'pwd'

94
thefuck/conf.py Normal file
View File

@@ -0,0 +1,94 @@
from copy import copy
from imp import load_source
import os
import sys
from six import text_type
from . import logs, types
class _DefaultRulesNames(types.RulesNamesList):
def __add__(self, items):
return _DefaultRulesNames(list(self) + items)
def __contains__(self, item):
return item.enabled_by_default or \
super(_DefaultRulesNames, self).__contains__(item)
def __eq__(self, other):
if isinstance(other, _DefaultRulesNames):
return super(_DefaultRulesNames, self).__eq__(other)
else:
return False
DEFAULT_RULES = _DefaultRulesNames([])
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3,
'require_confirmation': False,
'no_colors': False}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors'}
def _settings_from_file(user_dir):
"""Loads settings from file."""
settings = load_source('settings',
text_type(user_dir.joinpath('settings.py')))
return {key: getattr(settings, key)
for key in DEFAULT_SETTINGS.keys()
if hasattr(settings, key)}
def _rules_from_env(val):
"""Transforms rules list from env-string to python."""
val = val.split(':')
if 'DEFAULT_RULES' in val:
val = DEFAULT_RULES + [rule for rule in val if rule != 'DEFAULT_RULES']
return val
def _val_from_env(env, attr):
"""Transforms env-strings to python."""
val = os.environ[env]
if attr == 'rules':
val = _rules_from_env(val)
elif attr == 'wait_command':
val = int(val)
elif attr in ('require_confirmation', 'no_colors'):
val = val.lower() == 'true'
return val
def _settings_from_env():
"""Loads settings from env."""
return {attr: _val_from_env(env, attr)
for env, attr in ENV_TO_ATTR.items()
if env in os.environ}
def get_settings(user_dir):
"""Returns settings filled with values from `settings.py` and env."""
conf = copy(DEFAULT_SETTINGS)
try:
conf.update(_settings_from_file(user_dir))
except Exception:
logs.exception("Can't load settings from file",
sys.exc_info(),
types.Settings(conf))
try:
conf.update(_settings_from_env())
except Exception:
logs.exception("Can't load settings from env",
sys.exc_info(),
types.Settings(conf))
if not isinstance(conf['rules'], types.RulesNamesList):
conf['rules'] = types.RulesNamesList(conf['rules'])
return types.Settings(conf)

51
thefuck/logs.py Normal file
View File

@@ -0,0 +1,51 @@
import sys
from traceback import format_exception
import colorama
def color(color_, settings):
"""Utility for ability to disabling colored output."""
if settings.no_colors:
return ''
else:
return color_
def exception(title, exc_info, settings):
sys.stderr.write(
u'{warn}[WARN] {title}:{reset}\n{trace}'
u'{warn}----------------------------{reset}\n\n'.format(
warn=color(colorama.Back.RED + colorama.Fore.WHITE
+ colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings),
title=title,
trace=''.join(format_exception(*exc_info))))
def rule_failed(rule, exc_info, settings):
exception('Rule {}'.format(rule.name), exc_info, settings)
def show_command(new_command, settings):
sys.stderr.write('{bold}{command}{reset}\n'.format(
command=new_command,
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, settings):
sys.stderr.write(
'{bold}{command}{reset} [{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command,
bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
sys.stderr.flush()
def failed(msg, settings):
sys.stderr.write('{red}{msg}{reset}\n'.format(
msg=msg,
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))

View File

@@ -1,16 +1,12 @@
from collections import namedtuple
from imp import load_source
from pathlib import Path
from os.path import expanduser
from subprocess import Popen, PIPE
import os
import sys
from traceback import format_exception
from psutil import Process, TimeoutExpired
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command'))
import colorama
from . import logs, conf, types
def setup_user_dir():
@@ -23,39 +19,25 @@ def setup_user_dir():
return user_dir
def get_settings(user_dir):
"""Returns prepared settings module."""
settings = load_source('settings',
str(user_dir.joinpath('settings.py')))
settings.__dict__.setdefault('rules', None)
settings.__dict__.setdefault('wait_command', 3)
settings.__dict__.setdefault('require_confirmation', False)
return settings
def is_rule_enabled(settings, rule):
"""Returns `True` when rule mentioned in `rules` or `rules`
isn't defined.
"""
return settings.rules is None or rule.name[:-3] in settings.rules
def load_rule(rule):
"""Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule))
return Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command)
return types.Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True))
def get_rules(user_dir, settings):
"""Returns all enabled rules."""
bundled = Path(__file__).parent\
.joinpath('rules')\
.glob('*.py')
bundled = Path(__file__).parent \
.joinpath('rules') \
.glob('*.py')
user = user_dir.joinpath('rules').glob('*.py')
return [load_rule(rule) for rule in sorted(list(bundled)) + list(user)
if rule.name != '__init__.py' and is_rule_enabled(settings, rule)]
for rule in sorted(list(bundled)) + list(user):
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
def wait_output(settings, popen):
@@ -89,8 +71,8 @@ def get_command(settings, args):
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C'))
if wait_output(settings, result):
return Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8'))
return types.Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8'))
def get_matched_rule(command, rules, settings):
@@ -100,23 +82,21 @@ def get_matched_rule(command, rules, settings):
if rule.match(command, settings):
return rule
except Exception:
sys.stderr.write(u'[WARN] {}: {}---------------------\n\n'.format(
rule.name, ''.join(format_exception(*sys.exc_info()))))
logs.rule_failed(rule, sys.exc_info(), settings)
def confirm(new_command, settings):
"""Returns `True` when running of new command confirmed."""
if not settings.require_confirmation:
sys.stderr.write(new_command + '\n')
logs.show_command(new_command, settings)
return True
sys.stderr.write(new_command + ' [Enter/Ctrl+C]')
sys.stderr.flush()
logs.confirm_command(new_command, settings)
try:
sys.stdin.read(1)
return True
except KeyboardInterrupt:
sys.stderr.write('Aborted\n')
logs.failed('Aborted', settings)
return False
@@ -133,13 +113,14 @@ def is_second_run(command):
def main():
colorama.init()
user_dir = setup_user_dir()
settings = get_settings(user_dir)
settings = conf.get_settings(user_dir)
command = get_command(settings, sys.argv)
if command:
if is_second_run(command):
print("echo Can't fuck twice")
logs.failed("Can't fuck twice", settings)
return
rules = get_rules(user_dir, settings)
@@ -148,4 +129,4 @@ def main():
run_rule(matched_rule, command, settings)
return
print('echo No fuck given')
logs.failed('No fuck given', settings)

View File

@@ -1,10 +1,13 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return command.script.startswith('cp ') \
and 'cp: omitting directory' in command.stderr.lower()
@sudo_support
def get_new_command(command, settings):
return re.sub(r'^cp', 'cp -a', command.script)

View File

@@ -1,11 +1,14 @@
import os
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return os.path.exists(command.script.split()[0]) \
and 'command not found' in command.stderr
@sudo_support
def get_new_command(command, settings):
return u'./{}'.format(command.script)

View File

@@ -1,12 +1,15 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return (command.script.startswith('lein')
and "is not a task. See 'lein help'" in command.stderr
and 'Did you mean this?' in command.stderr)
@sudo_support
def get_new_command(command, settings):
broken_cmd = re.findall(r"'([^']*)' is not a task",
command.stderr)[0]

View File

@@ -1,9 +1,13 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return ('mkdir' in command.script
and 'No such file or directory' in command.stderr)
@sudo_support
def get_new_command(command, settings):
return re.sub('^mkdir (.*)', 'mkdir -p \\1', command.script)

View File

@@ -1,6 +1,7 @@
from difflib import get_close_matches
import os
from pathlib import Path
from thefuck.utils import sudo_support
def _safe(fn, fallback):
@@ -17,12 +18,14 @@ def _get_all_bins():
if not _safe(exe.is_dir, True)]
@sudo_support
def match(command, settings):
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
_get_all_bins()))
@sudo_support
def get_new_command(command, settings):
old_command = command.script.split(' ')[0]
new_command = get_close_matches(old_command,

View File

@@ -1,7 +1,10 @@
from thefuck.utils import sudo_support
# add 'python' suffix to the command if
# 1) The script does not have execute permission or
# 2) is interpreted as shell script
@sudo_support
def match(command, settings):
toks = command.script.split()
return (len(toks) > 0
@@ -10,5 +13,6 @@ def match(command, settings):
'command not found' in command.stderr))
@sudo_support
def get_new_command(command, settings):
return 'python ' + command.script

View File

@@ -1,9 +1,13 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return ('rm' in command.script
and 'is a directory' in command.stderr)
and 'is a directory' in command.stderr.lower())
@sudo_support
def get_new_command(command, settings):
return re.sub('^rm (.*)', 'rm -rf \\1', command.script)

16
thefuck/rules/rm_root.py Normal file
View File

@@ -0,0 +1,16 @@
from thefuck.utils import sudo_support
enabled_by_default = False
@sudo_support
def match(command, settings):
return ({'rm', '/'}.issubset(command.script.split())
and '--no-preserve-root' not in command.script
and '--no-preserve-root' in command.stderr)
@sudo_support
def get_new_command(command, settings):
return u'{} --no-preserve-root'.format(command.script)

View File

@@ -0,0 +1,37 @@
import re
patterns = [
r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!',
r'WARNING: POSSIBLE DNS SPOOFING DETECTED!',
r"Warning: the \S+ host key for '([^']+)' differs from the key for the IP address '([^']+)'",
]
offending_pattern = re.compile(
r'(?:Offending (?:key for IP|\S+ key)|Matching host key) in ([^:]+):(\d+)',
re.MULTILINE)
commands = ['ssh', 'scp']
def match(command, settings):
if not command.script:
return False
if not command.script.split()[0] in commands:
return False
if not any([re.findall(pattern, command.stderr) for pattern in patterns]):
return False
return True
def remove_offending_keys(command, settings):
offending = offending_pattern.findall(command.stderr)
for filepath, lineno in offending:
with open(filepath, 'r') as fh:
lines = fh.readlines()
del lines[int(lineno) - 1]
with open(filepath, 'w') as fh:
fh.writelines(lines)
def get_new_command(command, settings):
remove_offending_keys(command, settings)
return command.script

View File

@@ -5,7 +5,8 @@ patterns = ['permission denied',
'non-root users cannot',
'Operation not permitted',
'root privilege',
'This command has to be run under the root user.']
'This command has to be run under the root user.',
'You need to be root to perform this command.']
def match(command, settings):

View File

@@ -3,7 +3,8 @@
target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?'''
source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''',
u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''']
u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''',
u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?''']
def _get_matched_layout(command):

26
thefuck/types.py Normal file
View File

@@ -0,0 +1,26 @@
from collections import namedtuple
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default'))
class RulesNamesList(list):
"""Wrapper a top of list for string rules names."""
def __contains__(self, item):
return super(RulesNamesList, self).__contains__(item.name)
class Settings(dict):
def __getattr__(self, item):
return self.get(item)
def update(self, **kwargs):
"""Returns new settings with new values from `kwargs`."""
conf = dict(self)
conf.update(kwargs)
return Settings(conf)

View File

@@ -1,5 +1,7 @@
from functools import wraps
import os
import six
from .types import Command
def which(program):
@@ -35,9 +37,25 @@ def wrap_settings(params):
def decorator(fn):
@wraps(fn)
def wrapper(command, settings):
for key, val in params.items():
if not hasattr(settings, key):
setattr(settings, key, val)
return fn(command, settings)
return fn(command, settings.update(**params))
return wrapper
return decorator
def sudo_support(fn):
"""Removes sudo before calling fn and adds it after."""
@wraps(fn)
def wrapper(command, settings):
if not command.script.startswith('sudo '):
return fn(command, settings)
result = fn(Command(command.script[5:],
command.stdout,
command.stderr),
settings)
if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result)
else:
return result
return wrapper