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

Compare commits

...

39 Commits
1.15 ... 1.26

Author SHA1 Message Date
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
nvbn
54d82f9528 Bump version 2015-04-21 14:41:28 +02:00
nvbn
888756d519 #74 Don't fail when rule throws exception 2015-04-21 14:40:52 +02:00
nvbn
d5b4bddc4c #74 Don't fail when runned without args 2015-04-21 14:26:45 +02:00
nvbn
d09238a6e8 Merge branch 'master' of github.com:nvbn/thefuck
Conflicts:
	thefuck/rules/sudo.py
2015-04-21 14:23:31 +02:00
nvbn
c6c3756caf Merge branch 'soheilpro-sudo-rule-root-privilege' 2015-04-21 14:22:34 +02:00
Vladimir Iakovlev
275574beae Merge pull request #73 from Dugucloud/master
Added a string which could be thrown by Fedora's new dnf package manager.
2015-04-21 14:21:53 +02:00
Dugucloud
de4b774134 Added a string which could be thrown by Fedora's new dnf package manager. 2015-04-21 19:43:10 +08:00
Soheil Rashidi
3af5c80d29 Add 'root privilege' pattern to sudo rule. 2015-04-21 12:57:35 +04:30
nvbn
bd5f5045aa #71 Handle iterdir iterator fails 2015-04-21 08:57:35 +02:00
nvbn
798928b5ad #71 Don't fail on non-exists dir in $PATH 2015-04-21 08:45:45 +02:00
nvbn
82e2c89472 Fix version number 2015-04-21 08:40:17 +02:00
nvbn
f2392349f7 #71 Handle OSError more gratefully 2015-04-21 08:38:52 +02:00
nvbn
478fa4cd09 #71 Not fail on os error 2015-04-21 08:30:48 +02:00
Vladimir Iakovlev
273fc097bd Update switch_lang.py 2015-04-21 07:16:36 +02:00
Vladimir Iakovlev
00d0987cf5 Merge pull request #70 from fzerorubigd/master
add persian language to switch lang rule
2015-04-21 07:15:53 +02:00
fzerorubigd
3798c341d5 add persian language to switch lang rule
refs #28
2015-04-21 09:42:13 +04:30
nvbn
e1fe7ff7d0 Bump version 2015-04-21 06:56:26 +02:00
nvbn
e3edea05ed #24 Make no_command crossplatform 2015-04-21 06:55:47 +02:00
nvbn
3606131502 Fix tests 2015-04-21 06:36:51 +02:00
nvbn
8ed01fedbf Bump version 2015-04-21 06:34:03 +02:00
nvbn
ab8ac23749 Fix python 3 support 2015-04-21 06:33:51 +02:00
nvbn
e7d5d93056 #68 Add rule for switching layout 2015-04-21 06:26:15 +02:00
nvbn
5ccf163594 command.script now unicode 2015-04-21 06:24:40 +02:00
15 changed files with 295 additions and 110 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
@@ -158,7 +158,8 @@ 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;
* `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.
## Creating your own rules
@@ -192,9 +193,7 @@ The Fuck has a few settings parameters, they can be changed in `~/.thefuck/setti
* `rules` – list of enabled rules, by default all;
* `require_confirmation` – require confirmation before running new command, by default `False`;
* `wait_command` – max amount of time in seconds for getting previous command output;
* `command_not_found` – path to `command_not_found` binary,
by default `/usr/lib/command-not-found`.
* `wait_command` – max amount of time in seconds for getting previous command output.
## 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,14 +1,18 @@
from setuptools import setup, find_packages
VERSION = '1.26'
setup(name='thefuck',
version=1.15,
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'],

View File

@@ -0,0 +1,12 @@
from thefuck.main 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,62 +1,19 @@
from subprocess import PIPE
from mock import patch, Mock
import pytest
from thefuck.rules.no_command import match, get_new_command
from thefuck.main import Command
@pytest.fixture
def command_found():
return b'''No command 'aptget' found, did you mean:
Command 'apt-get' from package 'apt' (main)
aptget: command not found
'''
@pytest.fixture
def command_not_found():
return b'''No command 'vom' found, but there are 19 similar ones
vom: command not found
'''
def test_match():
with patch('thefuck.rules.no_command._get_all_bins',
return_value=['vim', 'apt-get']):
assert match(Mock(stderr='vom: not found', script='vom file.py'), None)
assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None)
assert not match(Mock(stderr='some text', script='vom file.py'), None)
@pytest.fixture
def bins_exists(request):
p = patch('thefuck.rules.no_command.which',
return_value=True)
p.start()
request.addfinalizer(p.stop)
@pytest.fixture
def settings():
class _Settings(object):
pass
return _Settings
@pytest.mark.usefixtures('bins_exists')
def test_match(command_found, command_not_found, settings):
with patch('thefuck.rules.no_command.Popen') as Popen:
Popen.return_value.stderr.read.return_value = command_found
assert match(Command('aptget install vim', '', ''), settings)
Popen.assert_called_once_with('/usr/lib/command-not-found aptget',
shell=True, stderr=PIPE)
Popen.return_value.stderr.read.return_value = command_not_found
assert not match(Command('ls', '', ''), settings)
with patch('thefuck.rules.no_command.Popen') as Popen:
Popen.return_value.stderr.read.return_value = command_found
assert match(Command('sudo aptget install vim', '', ''),
Mock(command_not_found='test'))
Popen.assert_called_once_with('test aptget',
shell=True, stderr=PIPE)
@pytest.mark.usefixtures('bins_exists')
def test_get_new_command(command_found):
with patch('thefuck.rules.no_command._get_output',
return_value=command_found.decode()):
assert get_new_command(Command('aptget install vim', '', ''), settings)\
== 'apt-get install vim'
assert get_new_command(Command('sudo aptget install vim', '', ''), settings) \
== 'sudo apt-get install vim'
def test_get_new_command():
with patch('thefuck.rules.no_command._get_all_bins',
return_value=['vim', 'apt-get']):
assert get_new_command(
Mock(stderr='vom: not found',
script='vom file.py'),
None) == 'vim file.py'

View File

@@ -0,0 +1,69 @@
import os
import pytest
from mock import Mock
from thefuck.main 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

@@ -0,0 +1,25 @@
# -*- encoding: utf-8 -*-
from mock import Mock
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)
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'

View File

@@ -24,7 +24,7 @@ def test_load_rule():
return_value=Mock(
match=match,
get_new_command=get_new_command)) as load_source:
assert main.load_rule(Path('/rules/bash.py')) == main.Rule(match, get_new_command)
assert main.load_rule(Path('/rules/bash.py')) == main.Rule('bash', match, get_new_command)
load_source.assert_called_once_with('bash', '/rules/bash.py')
@@ -35,14 +35,14 @@ def test_get_rules():
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
assert main.get_rules(
Path('~'),
Mock(rules=None)) == [main.Rule('bash', 'bash'),
main.Rule('lisp', 'lisp'),
main.Rule('bash', 'bash'),
main.Rule('lisp', 'lisp')]
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(
Path('~'),
Mock(rules=['bash'])) == [main.Rule('bash', 'bash'),
main.Rule('bash', 'bash')]
Mock(rules=['bash'])) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('bash', 'bash', 'bash')]
def test_get_command():
@@ -61,24 +61,28 @@ def test_get_command():
stdout=PIPE,
stderr=PIPE,
env={'LANG': 'C'})
assert main.get_command(Mock(), ['']) is None
def test_get_matched_rule():
rules = [main.Rule(lambda x, _: x.script == 'cd ..', None),
main.Rule(lambda *_: False, None)]
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):'
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(main.Rule('', None, lambda *_: 'new-command'),
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(main.Rule('', None, lambda *_: 'new-command'),
None, None)
assert capsys.readouterr() == ('', '')

View File

@@ -5,11 +5,12 @@ 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', ('match', 'get_new_command'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command'))
def setup_user_dir():
@@ -43,7 +44,8 @@ def is_rule_enabled(settings, rule):
def load_rule(rule):
"""Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule))
return Rule(rule_module.match, rule_module.get_new_command)
return Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command)
def get_rules(user_dir, settings):
@@ -76,7 +78,14 @@ def wait_output(settings, popen):
def get_command(settings, args):
"""Creates command from `args` and executes it."""
script = ' '.join(args[1:])
if sys.version_info[0] < 3:
script = ' '.join(arg.decode('utf-8') for arg in args[1:])
else:
script = ' '.join(args[1:])
if not script:
return
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C'))
if wait_output(settings, result):
@@ -87,8 +96,12 @@ def get_command(settings, args):
def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command."""
for rule in rules:
if rule.match(command, settings):
return rule
try:
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()))))
def confirm(new_command, settings):

View File

@@ -7,5 +7,5 @@ def match(command, settings):
def get_new_command(command, settings):
return './{}'.format(command.script)
return u'./{}'.format(command.script)

View File

@@ -1,30 +1,30 @@
from subprocess import Popen, PIPE
import re
from thefuck.utils import which, wrap_settings
from difflib import get_close_matches
import os
from pathlib import Path
local_settings = {'command_not_found': '/usr/lib/command-not-found'}
def _safe(fn, fallback):
try:
return fn()
except OSError:
return fallback
def _get_output(command, settings):
name = command.script.split(' ')[command.script.startswith('sudo')]
check_script = '{} {}'.format(settings.command_not_found, name)
result = Popen(check_script, shell=True, stderr=PIPE)
return result.stderr.read().decode('utf-8')
def _get_all_bins():
return [exe.name
for path in os.environ.get('PATH', '').split(':')
for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)]
@wrap_settings(local_settings)
def match(command, settings):
if which(settings.command_not_found):
output = _get_output(command, settings)
return "No command" in output and "from package" in output
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
_get_all_bins()))
@wrap_settings(local_settings)
def get_new_command(command, settings):
output = _get_output(command, settings)
broken_name = re.findall(r"No command '([^']*)' found",
output)[0]
fixed_name = re.findall(r"Command '([^']*)' from package",
output)[0]
return command.script.replace(broken_name, fixed_name, 1)
old_command = command.script.split(' ')[0]
new_command = get_close_matches(old_command,
_get_all_bins())[0]
return ' '.join([new_command] + command.script.split(' ')[1:])

View File

@@ -3,11 +3,12 @@
# 2) is interpreted as shell script
def match(command, settings):
toks = command.script.split()
return (len(toks) > 0
and toks[0].endswith('.py')
and ('Permission denied' in command.stderr or
'command not found' in command.stderr))
toks = command.script.split()
return (len(toks) > 0
and toks[0].endswith('.py')
and ('Permission denied' in command.stderr or
'command not found' in command.stderr))
def get_new_command(command, settings):
return 'python ' + command.script
return 'python ' + 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

@@ -3,7 +3,9 @@ patterns = ['permission denied',
'pkg: Insufficient privileges',
'you cannot perform this operation unless you are root',
'non-root users cannot',
'Operation not permitted']
'Operation not permitted',
'root privilege',
'This command has to be run under the root user.']
def match(command, settings):
@@ -14,4 +16,4 @@ def match(command, settings):
def get_new_command(command, settings):
return 'sudo {}'.format(command.script)
return u'sudo {}'.format(command.script)

View File

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