1
0
mirror of https://github.com/nvbn/thefuck.git synced 2024-10-06 10:51:11 +01:00

Merge branch 'master' of github.com:nvbn/thefuck

This commit is contained in:
nvbn 2015-11-26 03:42:16 +08:00
commit 9192b555b5
16 changed files with 160 additions and 65 deletions

View File

@ -281,7 +281,7 @@ The Fuck has a few settings parameters which can be changed in `$XDG_CONFIG_HOME
* `no_colors` – disable colored output; * `no_colors` – disable colored output;
* `priority` – dict with rules priorities, rule with lower `priority` will be matched first; * `priority` – dict with rules priorities, rule with lower `priority` will be matched first;
* `debug` – enables debug output, by default `False`; * `debug` – enables debug output, by default `False`;
* `history_limit` &ndash numeric value of how many history commands will be scanned, like `2000`; * `history_limit` – numeric value of how many history commands will be scanned, like `2000`;
Example of `settings.py`: Example of `settings.py`:

View File

@ -3,4 +3,4 @@ import pytest
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def generic_shell(monkeypatch): def generic_shell(monkeypatch):
monkeypatch.setattr('thefuck.shells.and_', lambda *x: ' && '.join(x)) monkeypatch.setattr('thefuck.shells.and_', lambda *x: u' && '.join(x))

View File

@ -1,13 +1,17 @@
# -*- coding: utf-8 -*-
import os import os
import pytest import pytest
import zipfile import zipfile
from thefuck.rules.dirty_unzip import match, get_new_command, side_effect from thefuck.rules.dirty_unzip import match, get_new_command, side_effect
from tests.utils import Command from tests.utils import Command
from unicodedata import normalize
@pytest.fixture @pytest.fixture
def zip_error(tmpdir): def zip_error(tmpdir):
path = os.path.join(str(tmpdir), 'foo.zip') def zip_error_inner(filename):
path = os.path.join(str(tmpdir), filename)
def reset(path): def reset(path):
with zipfile.ZipFile(path, 'w') as archive: with zipfile.ZipFile(path, 'w') as archive:
@ -22,29 +26,47 @@ def zip_error(tmpdir):
os.chdir(str(tmpdir)) os.chdir(str(tmpdir))
reset(path) reset(path)
assert set(os.listdir('.')) == {'foo.zip', 'a', 'b', 'c', 'd'} dir_list = os.listdir(u'.')
if filename not in dir_list:
filename = normalize('NFD', filename)
assert set(dir_list) == {filename, 'a', 'b', 'c', 'd'}
assert set(os.listdir('./d')) == {'e'} assert set(os.listdir('./d')) == {'e'}
return zip_error_inner
@pytest.mark.parametrize('script', [ @pytest.mark.parametrize('script,filename', [
'unzip foo', (u'unzip café', u'café.zip'),
'unzip foo.zip']) (u'unzip café.zip', u'café.zip'),
def test_match(zip_error, script): (u'unzip foo', u'foo.zip'),
(u'unzip foo.zip', u'foo.zip')])
def test_match(zip_error, script, filename):
zip_error(filename)
assert match(Command(script=script)) assert match(Command(script=script))
@pytest.mark.parametrize('script', [ @pytest.mark.parametrize('script,filename', [
'unzip foo', (u'unzip café', u'café.zip'),
'unzip foo.zip']) (u'unzip café.zip', u'café.zip'),
def test_side_effect(zip_error, script): (u'unzip foo', u'foo.zip'),
(u'unzip foo.zip', u'foo.zip')])
def test_side_effect(zip_error, script, filename):
zip_error(filename)
side_effect(Command(script=script), None) side_effect(Command(script=script), None)
assert set(os.listdir('.')) == {'foo.zip', 'd'}
dir_list = os.listdir(u'.')
if filename not in set(dir_list):
filename = normalize('NFD', filename)
assert set(dir_list) == {filename, 'd'}
@pytest.mark.parametrize('script,fixed', [ @pytest.mark.parametrize('script,fixed,filename', [
('unzip foo', 'unzip foo -d foo'), (u'unzip café', u"unzip café -d 'café'", u'café.zip'),
(R"unzip foo\ bar.zip", R"unzip foo\ bar.zip -d 'foo bar'"), (u'unzip foo', u'unzip foo -d foo', u'foo.zip'),
(R"unzip 'foo bar.zip'", R"unzip 'foo bar.zip' -d 'foo bar'"), (u"unzip foo\\ bar.zip", u"unzip foo\\ bar.zip -d 'foo bar'", u'foo.zip'),
('unzip foo.zip', 'unzip foo.zip -d foo')]) (u"unzip 'foo bar.zip'", u"unzip 'foo bar.zip' -d 'foo bar'", u'foo.zip'),
def test_get_new_command(zip_error, script, fixed): (u'unzip foo.zip', u'unzip foo.zip -d foo', u'foo.zip')])
def test_get_new_command(zip_error, script, fixed, filename):
zip_error(filename)
assert get_new_command(Command(script=script)) == fixed assert get_new_command(Command(script=script)) == fixed

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest import pytest
import os import os
from thefuck.rules.fix_file import match, get_new_command from thefuck.rules.fix_file import match, get_new_command
@ -87,6 +89,20 @@ Traceback (most recent call last):
TypeError: first argument must be string or compiled pattern TypeError: first argument must be string or compiled pattern
"""), """),
(u'python café.py', u'café.py', 8, None, '',
u"""
Traceback (most recent call last):
File "café.py", line 8, in <module>
match("foo")
File "café.py", line 5, in match
m = re.search(None, command)
File "/usr/lib/python3.4/re.py", line 170, in search
return _compile(pattern, flags).search(string)
File "/usr/lib/python3.4/re.py", line 293, in _compile
raise TypeError("first argument must be string or compiled pattern")
TypeError: first argument must be string or compiled pattern
"""),
('ruby a.rb', 'a.rb', 3, None, '', ('ruby a.rb', 'a.rb', 3, None, '',
""" """
a.rb:3: syntax error, unexpected keyword_end a.rb:3: syntax error, unexpected keyword_end
@ -227,7 +243,7 @@ def test_get_new_command_with_settings(mocker, monkeypatch, test, settings):
if test[3]: if test[3]:
assert (get_new_command(cmd) == assert (get_new_command(cmd) ==
'dummy_editor {} +{}:{} && {}'.format(test[1], test[2], test[3], test[0])) u'dummy_editor {} +{}:{} && {}'.format(test[1], test[2], test[3], test[0]))
else: else:
assert (get_new_command(cmd) == assert (get_new_command(cmd) ==
'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0])) u'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0]))

View File

@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-
from thefuck.rules.grep_recursive import match, get_new_command from thefuck.rules.grep_recursive import match, get_new_command
from tests.utils import Command from tests.utils import Command
def test_match(): def test_match():
assert match(Command('grep blah .', stderr='grep: .: Is a directory')) assert match(Command('grep blah .', stderr='grep: .: Is a directory'))
assert match(Command(u'grep café .', stderr='grep: .: Is a directory'))
assert not match(Command()) assert not match(Command())
def test_get_new_command(): def test_get_new_command():
assert get_new_command( assert get_new_command(Command('grep blah .')) == 'grep -r blah .'
Command('grep blah .')) == 'grep -r blah .' assert get_new_command(Command(u'grep café .')) == u'grep -r café .'

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from pathlib import PosixPath from pathlib import PosixPath
from thefuck import corrector, conf from thefuck import corrector, conf
@ -53,7 +55,9 @@ def test_organize_commands():
"""Ensures that the function removes duplicates and sorts commands.""" """Ensures that the function removes duplicates and sorts commands."""
commands = [CorrectedCommand('ls'), CorrectedCommand('ls -la', priority=9000), commands = [CorrectedCommand('ls'), CorrectedCommand('ls -la', priority=9000),
CorrectedCommand('ls -lh', priority=100), CorrectedCommand('ls -lh', priority=100),
CorrectedCommand(u'echo café', priority=200),
CorrectedCommand('ls -lh', priority=9999)] CorrectedCommand('ls -lh', priority=9999)]
assert list(organize_commands(iter(commands))) \ assert list(organize_commands(iter(commands))) \
== [CorrectedCommand('ls'), CorrectedCommand('ls -lh', priority=100), == [CorrectedCommand('ls'), CorrectedCommand('ls -lh', priority=100),
CorrectedCommand(u'echo café', priority=200),
CorrectedCommand('ls -la', priority=9000)] CorrectedCommand('ls -la', priority=9000)]

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest import pytest
from thefuck import shells from thefuck import shells
@ -35,6 +37,7 @@ class TestGeneric(object):
def test_put_to_history(self, builtins_open, shell): def test_put_to_history(self, builtins_open, shell):
assert shell.put_to_history('ls') is None assert shell.put_to_history('ls') is None
assert shell.put_to_history(u'echo café') is None
assert builtins_open.call_count == 0 assert builtins_open.call_count == 0
def test_and_(self, shell): def test_and_(self, shell):
@ -48,6 +51,7 @@ class TestGeneric(object):
assert 'alias FUCK' in shell.app_alias('FUCK') assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck') assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck') assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell): def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm']) history_lines(['ls', 'rm'])
@ -55,6 +59,10 @@ class TestGeneric(object):
# so just ignore them: # so just ignore them:
assert list(shell.get_history()) == [] assert list(shell.get_history()) == []
def test_split_command(self, shell):
assert shell.split_command('ls') == ['ls']
assert shell.split_command(u'echo café') == [u'echo', u'café']
@pytest.mark.usefixtures('isfile') @pytest.mark.usefixtures('isfile')
class TestBash(object): class TestBash(object):
@ -83,10 +91,13 @@ class TestBash(object):
def test_to_shell(self, shell): def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd' assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, shell): @pytest.mark.parametrize('entry, entry_utf8', [
shell.put_to_history('ls') ('ls', 'ls\n'),
(u'echo café', 'echo café\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, shell):
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \ builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('ls\n') write.assert_called_once_with(entry_utf8)
def test_and_(self, shell): def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd' assert shell.and_('ls', 'cd') == 'ls && cd'
@ -102,6 +113,7 @@ class TestBash(object):
assert 'alias FUCK' in shell.app_alias('FUCK') assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck') assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck') assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell): def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm']) history_lines(['ls', 'rm'])
@ -152,12 +164,15 @@ class TestFish(object):
def test_to_shell(self, shell): def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd' assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker, shell): @pytest.mark.parametrize('entry, entry_utf8', [
('ls', '- cmd: ls\n when: 1430707243\n'),
(u'echo café', '- cmd: echo café\n when: 1430707243\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time', mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463) return_value=1430707243.3517463)
shell.put_to_history('ls') shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \ builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('- cmd: ls\n when: 1430707243\n') write.assert_called_once_with(entry_utf8)
def test_and_(self, shell): def test_and_(self, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar' assert shell.and_('foo', 'bar') == 'foo; and bar'
@ -179,6 +194,12 @@ class TestFish(object):
assert 'function FUCK' in shell.app_alias('FUCK') assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck') assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck') assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['- cmd: ls', ' when: 1432613911',
'- cmd: rm', ' when: 1432613916'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.usefixtures('isfile') @pytest.mark.usefixtures('isfile')
@ -207,12 +228,15 @@ class TestZsh(object):
def test_to_shell(self, shell): def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd' assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, mocker, shell): @pytest.mark.parametrize('entry, entry_utf8', [
('ls', ': 1430707243:0;ls\n'),
(u'echo café', ': 1430707243:0;echo café\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time', mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463) return_value=1430707243.3517463)
shell.put_to_history('ls') shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \ builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(': 1430707243:0;ls\n') write.assert_called_once_with(entry_utf8)
def test_and_(self, shell): def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd' assert shell.and_('ls', 'cd') == 'ls && cd'
@ -229,6 +253,7 @@ class TestZsh(object):
assert 'alias FUCK' in shell.app_alias('FUCK') assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck') assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck') assert 'TF_ALIAS' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell): def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm']) history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from subprocess import PIPE from subprocess import PIPE
from mock import Mock from mock import Mock
from pathlib import Path from pathlib import Path
@ -19,6 +21,12 @@ class TestCorrectedCommand(object):
assert {CorrectedCommand('ls', None, 100), assert {CorrectedCommand('ls', None, 100),
CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')} CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')}
def test_representable(self):
assert '{}'.format(CorrectedCommand('ls', None, 100)) == \
'CorrectedCommand(script=ls, side_effect=None, priority=100)'
assert u'{}'.format(CorrectedCommand(u'echo café', None, 100)) == \
u'CorrectedCommand(script=echo café, side_effect=None, priority=100)'
class TestRule(object): class TestRule(object):
def test_from_path(self, mocker): def test_from_path(self, mocker):

View File

@ -55,7 +55,7 @@ def organize_commands(corrected_commands):
key=lambda corrected_command: corrected_command.priority) key=lambda corrected_command: corrected_command.priority)
logs.debug('Corrected commands: '.format( logs.debug('Corrected commands: '.format(
', '.join(str(cmd) for cmd in [first_command] + sorted_commands))) ', '.join(u'{}'.format(cmd) for cmd in [first_command] + sorted_commands)))
for command in sorted_commands: for command in sorted_commands:
yield command yield command

View File

@ -1,6 +1,7 @@
"""Attempts to spellcheck and correct failed cd commands""" """Attempts to spellcheck and correct failed cd commands"""
import os import os
import six
from difflib import get_close_matches from difflib import get_close_matches
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir from thefuck.rules import cd_mkdir
@ -36,6 +37,9 @@ def get_new_command(command):
dest = command.script_parts[1].split(os.sep) dest = command.script_parts[1].split(os.sep)
if dest[-1] == '': if dest[-1] == '':
dest = dest[:-1] dest = dest[:-1]
if six.PY2:
cwd = os.getcwdu()
else:
cwd = os.getcwd() cwd = os.getcwd()
for directory in dest: for directory in dest:
if directory == ".": if directory == ".":
@ -48,7 +52,7 @@ def get_new_command(command):
cwd = os.path.join(cwd, best_matches[0]) cwd = os.path.join(cwd, best_matches[0])
else: else:
return cd_mkdir.get_new_command(command) return cd_mkdir.get_new_command(command)
return 'cd "{0}"'.format(cwd) return u'cd "{0}"'.format(cwd)
enabled_by_default = True enabled_by_default = True

View File

@ -19,7 +19,7 @@ def _zip_file(command):
if c.endswith('.zip'): if c.endswith('.zip'):
return c return c
else: else:
return '{}.zip'.format(c) return u'{}.zip'.format(c)
@for_app('unzip') @for_app('unzip')
@ -29,7 +29,7 @@ def match(command):
def get_new_command(command): def get_new_command(command):
return '{} -d {}'.format(command.script, quote(_zip_file(command)[:-4])) return u'{} -d {}'.format(command.script, quote(_zip_file(command)[:-4]))
def side_effect(old_cmd, command): def side_effect(old_cmd, command):

View File

@ -58,7 +58,7 @@ def match(command):
return _search(command.stderr) or _search(command.stdout) return _search(command.stderr) or _search(command.stdout)
@default_settings({'fixlinecmd': '{editor} {file} +{line}', @default_settings({'fixlinecmd': u'{editor} {file} +{line}',
'fixcolcmd': None}) 'fixcolcmd': None})
def get_new_command(command): def get_new_command(command):
m = _search(command.stderr) or _search(command.stdout) m = _search(command.stderr) or _search(command.stdout)

View File

@ -7,4 +7,4 @@ def match(command):
def get_new_command(command): def get_new_command(command):
return 'grep -r {}'.format(command.script[5:]) return u'grep -r {}'.format(command.script[5:])

View File

@ -13,6 +13,6 @@ def get_new_command(command):
command.stderr) command.stderr)
old_cmd = cmd.group(1) old_cmd = cmd.group(1)
suggestions = [cmd.strip() for cmd in cmd.group(2).split(',')] suggestions = [c.strip() for c in cmd.group(2).split(',')]
return replace_command(command, old_cmd, suggestions) return replace_command(command, old_cmd, suggestions)

View File

@ -51,7 +51,11 @@ class Generic(object):
history_file_name = self._get_history_file_name() history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name): if os.path.isfile(history_file_name):
with open(history_file_name, 'a') as history: with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script)) entry = self._get_history_line(command_script)
if six.PY2:
history.write(entry.encode('utf-8'))
else:
history.write(entry)
def get_history(self): def get_history(self):
"""Returns list of history entries.""" """Returns list of history entries."""
@ -78,6 +82,8 @@ class Generic(object):
def split_command(self, command): def split_command(self, command):
"""Split the command using shell-like syntax.""" """Split the command using shell-like syntax."""
if six.PY2:
return [s.decode('utf8') for s in shlex.split(command.encode('utf8'))]
return shlex.split(command) return shlex.split(command)
def quote(self, s): def quote(self, s):
@ -144,8 +150,8 @@ class Fish(Generic):
def app_alias(self, fuck): def app_alias(self, fuck):
return ('function {0} -d "Correct your previous console command"\n' return ('function {0} -d "Correct your previous console command"\n'
' set -l exit_code $status\n' ' set -l exit_code $status\n'
' set -x TF_ALIAS {0}\n'
' set -l fucked_up_command $history[1]\n' ' set -l fucked_up_command $history[1]\n'
' env TF_ALIAS={0} PYTHONIOENCODING=utf-8'
' thefuck $fucked_up_command | read -l unfucked_command\n' ' thefuck $fucked_up_command | read -l unfucked_command\n'
' if [ "$unfucked_command" != "" ]\n' ' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n' ' eval $unfucked_command\n'
@ -182,6 +188,12 @@ class Fish(Generic):
def _get_history_line(self, command_script): def _get_history_line(self, command_script):
return u'- cmd: {}\n when: {}\n'.format(command_script, int(time())) return u'- cmd: {}\n when: {}\n'.format(command_script, int(time()))
def _script_from_history(self, line):
if '- cmd: ' in line:
return line.split('- cmd: ', 1)[1]
else:
return ''
def and_(self, *commands): def and_(self, *commands):
return u'; and '.join(commands) return u'; and '.join(commands)

View File

@ -31,7 +31,7 @@ class Command(object):
try: try:
self._script_parts = shells.split_command(self.script) self._script_parts = shells.split_command(self.script)
except Exception: except Exception:
logs.debug("Can't split command script {} because:\n {}".format( logs.debug(u"Can't split command script {} because:\n {}".format(
self, sys.exc_info())) self, sys.exc_info()))
self._script_parts = None self._script_parts = None
return self._script_parts return self._script_parts
@ -44,7 +44,7 @@ class Command(object):
return False return False
def __repr__(self): def __repr__(self):
return 'Command(script={}, stdout={}, stderr={})'.format( return u'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr) self.script, self.stdout, self.stderr)
def update(self, **kwargs): def update(self, **kwargs):
@ -267,7 +267,7 @@ class CorrectedCommand(object):
return (self.script, self.side_effect).__hash__() return (self.script, self.side_effect).__hash__()
def __repr__(self): def __repr__(self):
return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format( return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority) self.script, self.side_effect, self.priority)
def run(self, old_cmd): def run(self, old_cmd):
@ -279,4 +279,5 @@ class CorrectedCommand(object):
if self.side_effect: if self.side_effect:
compatibility_call(self.side_effect, old_cmd, self.script) compatibility_call(self.side_effect, old_cmd, self.script)
shells.put_to_history(self.script) shells.put_to_history(self.script)
# This depends on correct setting of PYTHONIOENCODING by the alias:
print(self.script) print(self.script)