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

Compare commits

...

54 Commits
3.2 ... 3.3

Author SHA1 Message Date
nvbn
bd5cf38271 Bump to 3.3 2016-01-13 22:33:04 +03:00
Vladimir Iakovlev
3c3d17e0ea Merge pull request #432 from nvbn/422-not-alter-history
#422: Add `alter_history` settings option
2016-01-13 22:31:19 +03:00
nvbn
2f353498de #422: Add alter_history settings option 2016-01-13 22:22:51 +03:00
Vladimir Iakovlev
f0f49c1865 Merge pull request #430 from nvbn/429-apt-invalid-operation
#429: Add `apt_invalid_operation` rule
2016-01-13 22:12:35 +03:00
nvbn
20fff3142c #429: Fix tests with python 2 2016-01-13 22:08:24 +03:00
Vladimir Iakovlev
6e22b9ec6c Merge pull request #431 from nvbn/428-readonly-history
#428: Don't fail when history is readonly
2016-01-13 22:03:31 +03:00
nvbn
d53240b777 #428: Don't fail when history is readonly 2016-01-13 22:00:20 +03:00
nvbn
cab933e7e6 #429: Add apt_invalid_operation rule 2016-01-13 21:53:11 +03:00
Vladimir Iakovlev
8b05f6d46f Merge pull request #427 from makalaaneesh/master
#425 command had to be re escaped
2016-01-08 16:38:05 +03:00
Vladimir Iakovlev
ec64fbd5ea Merge pull request #426 from web-connect/patch-1
Fixing typos
2016-01-08 16:37:23 +03:00
makalaaneesh
4f9fb796c4 fixes #425. command had to be re escaped 2016-01-08 00:50:26 +05:30
Justin Turner
be744f20ba Fixing typos 2016-01-07 09:36:37 -06:00
Vladimir Iakovlev
1b12cd85e9 Merge pull request #423 from MattKotsenas/bugfix/cd_mkdir
Add Windows error message support to cd_mkdir rule
2016-01-06 03:34:03 +03:00
Matt Kotsenas
47df80f6b8 Add Windows error message support to cd_mkdir rule
Add the Windows error message 'the system cannot find the path specified'
to the list of recognized messages for cd_mkdir.
2016-01-05 13:55:59 -08:00
Vladimir Iakovlev
a0ef0efe46 Merge pull request #419 from mcarton/fix-unzip
Fix the `dirty_unzip` rule
2015-12-30 00:58:30 +03:00
Vladimir Iakovlev
25662ad737 Merge pull request #418 from makalaaneesh/master
sudo sh execute for && in commands - preventing double sudo
2015-12-30 00:57:50 +03:00
mcarton
42b344676e Fix dirty_unzip rule on non-zip files 2015-12-29 18:46:35 +01:00
mcarton
a3e1cb6718 Fix thefuck unzip, fix #416 2015-12-29 18:38:58 +01:00
makalaaneesh
f249098336 sudo sh execute for && in commands - preventing double sudo 2015-12-23 14:35:47 +05:30
nvbn
c3b1ba7637 #415: Prevent double sudo 2015-12-11 07:41:13 +08:00
nvbn
b65a9a0a4f #414: Initialize output before any colorama import 2015-12-04 18:34:52 +08:00
nvbn
29c1d1efcf #414: Move system-dependent utils in system module 2015-12-03 20:03:27 +08:00
nvbn
0560f4ba8e #414: Install and use win_unicode_console only on windows 2015-12-01 20:15:27 +08:00
Pavel Krymets
f9aa0e7c6b Fix windows unicode output issues 2015-11-30 16:24:31 -08:00
Pavel Krymets
b18a049886 Fix getch on windows 2015-11-30 12:33:28 -08:00
nvbn
9192b555b5 Merge branch 'master' of github.com:nvbn/thefuck 2015-11-26 03:42:16 +08:00
nvbn
d750d3d6d1 #412: Add _script_from_history for generic shell 2015-11-26 03:42:03 +08:00
Vladimir Iakovlev
3ad953001d Merge pull request #411 from scorphus/unicode
Support non-ascii content in Python 2
2015-11-25 20:41:20 +08:00
Pablo Santiago Blum de Aguiar
3b4b87d8ed #398: Test PYTHONIOENCODING=utf-8 in shell aliases 2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
6c3d67763a #398: Add PYTHONIOENCODING=utf-8 to Fish Shell alias 2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
959680d24d #N/A Set TF_ALIAS as an environment variable
For more info, check:

http://fishshell.com/docs/current/faq.html#faq-single-env
2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
b0adc7f2ca #N/A Indent Fish alias with two spaces (default) 2015-11-25 02:34:33 -02:00
Pablo Santiago Blum de Aguiar
fc05364233 #398 & #408: Support non-ascii IO in Python 2 2015-11-25 02:34:19 -02:00
Pablo Santiago Blum de Aguiar
ad3db4ac67 #N/A Fix F812 list comprehension redefines cmd 2015-11-25 02:34:15 -02:00
Pablo Santiago Blum de Aguiar
4a7b335d7c #N/A Add ability to get Fish Shell history 2015-11-25 02:34:02 -02:00
Pablo Santiago Blum de Aguiar
465f6191b0 #N/A Cleanup and adjust syntax 2015-11-25 01:58:07 -02:00
Vladimir Iakovlev
b2836319ad Update README.md 2015-11-19 11:22:56 +08:00
Vladimir Iakovlev
b3e9b36bd1 Merge pull request #409 from nvbn/394-history-limit
#394 history limit
2015-11-19 11:17:21 +08:00
lovedboy
ae2949cfa2 python2.7 unicode error 2015-11-19 09:40:44 +08:00
nvbn
1bb04b41eb #398: Add PYTHONIOENCODING=utf-8 to shell aliases 2015-11-18 18:37:11 +08:00
Vladimir Iakovlev
acd0b3e024 Merge pull request #406 from mcarton/py2→3
Fix cache problem when going from Python 2 to 3
2015-11-18 18:32:24 +08:00
mcarton
7c5676491a Fix some more warnings from flake8 2015-11-15 18:08:59 +01:00
mcarton
8feb722ed0 Fix some pep8 warnings 2015-11-15 18:02:37 +01:00
mcarton
c3ea2fd0c7 Fix cache problem when going from Python 2 to 3 2015-11-15 16:55:07 +01:00
nvbn
b55464b2ea #403 Add sudo rule's pattern for dscl 2015-11-13 15:37:13 +08:00
nvbn
8ddb61ae89 #N/A Add python-gdbm to install script 2015-11-12 18:43:15 +08:00
Vladimir Iakovlev
fe91008a9c Merge pull request #400 from alessio/fix-memoize
Fix misinterpretation of the disabled flag
2015-11-06 02:19:07 +08:00
Alessio Treglia
7f777213c5 Fix misinterpretation of the disabled flag
The old implementation was misinterpretating the disabled flag and
effectively applying memoization even when explicitly disabled.
The 'or' operator is a short-circuit one; namely, it evaluates the
second argument if and only if the first is False. Therefore the
following conditions caused unexpected side effects:

- memoize.disabled = True, key not yet memoized

  Having disabled the memoize function wrapper, the client expects
  that no memoization happens. Instead the execution enters the
  if clause and store the value into the 'memo' dictionary

- memoize.disabled = True, key memoized

  Having disabled the memoize function wrapper, the client expects
  that no memoization happens and the function will be evaluated
  anyway, whether or not its return value had already been stored in
  the 'memo' dictionary by a previous call. On the contrary, the last
  statement of wrapper() access the value stored by the last function
  execution.

This commit attempts to improve the function readability too.
2015-11-04 22:44:50 +00:00
nvbn
2fea0d4c60 #394: Force history_limit to be int 2015-10-30 16:23:19 +08:00
nvbn
8c8abca8d5 #394: readlines isn't lazy 2015-10-29 22:51:30 +08:00
nvbn
bd6ee68c03 #394: Try simpler solution to limit lines count 2015-10-29 20:17:17 +08:00
nvbn
16533e85a7 Merge branch 'debug' of git://github.com/lovedboy/thefuck into lovedboy-debug 2015-10-29 20:00:04 +08:00
lovedboy
b3a19fe439 history limit from settings 2015-10-29 10:14:34 +08:00
lovedboy
372e983459 add THEFUCK_HISTORY_LIMIT, my machine is so slow 2015-10-22 19:25:00 +08:00
48 changed files with 636 additions and 266 deletions

View File

@@ -211,6 +211,7 @@ Enabled by default only on specific platforms:
* `apt_get` – installs app from apt if it not installed (requires `python-commandnotfound` / `python3-commandnotfound`);
* `apt_get_search` – changes trying to search using `apt-get` with searching using `apt-cache`;
* `apt_invalid_operation` – fixes invalid `apt` and `apt-get` calls, like `apt-get isntall vim`;
* `brew_install` – fixes formula name for `brew install`;
* `brew_unknown_command` – fixes wrong brew commands, for example `brew docto/brew doctor`;
* `brew_upgrade` – appends `--all` to `brew upgrade` as per Homebrew's new behaviour;
@@ -280,7 +281,9 @@ The Fuck has a few settings parameters which can be changed in `$XDG_CONFIG_HOME
* `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output;
* `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` – numeric value of how many history commands will be scanned, like `2000`;
* `alter_history` – push fixed command to history, by default `True`.
Example of `settings.py`:
@@ -303,7 +306,9 @@ Or via environment variables:
* `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;
* `THEFUCK_DEBUG` – enables debug output, `true/false`.
* `THEFUCK_DEBUG` – enables debug output, `true/false`;
* `THEFUCK_HISTORY_LIMIT` – how many history commands will be scanned, like `2000`;
* `THEFUCK_ALTER_HISTORY` – push fixed command to history `true/false`.
For example:
@@ -314,6 +319,7 @@ export THEFUCK_REQUIRE_CONFIRMATION='true'
export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
export THEFUCK_HISTORY_LIMIT='2000'
```
## Developing

View File

@@ -9,11 +9,11 @@ installed () {
}
install_thefuck () {
# Install os dependencies:
# Install OS dependencies:
if installed apt-get; then
# Debian/ubuntu:
# Debian/Ubuntu:
sudo apt-get update -yy
sudo apt-get install -yy python-pip python-dev command-not-found
sudo apt-get install -yy python-pip python-dev command-not-found python-gdbm
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
# In case of different python versions:
@@ -25,7 +25,7 @@ install_thefuck () {
brew update
brew install python
else
# Genreic way:
# Generic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py

View File

@@ -20,10 +20,11 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '3.2'
VERSION = '3.3'
install_requires = ['psutil', 'colorama', 'six', 'decorator']
extras_require = {':python_version<"3.4"': ['pathlib']}
extras_require = {':python_version<"3.4"': ['pathlib'],
":sys_platform=='win32'": ['win_unicode_console']}
setup(name='thefuck',
version=VERSION,

View File

@@ -3,4 +3,4 @@ import pytest
@pytest.fixture(autouse=True)
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

@@ -0,0 +1,122 @@
from io import BytesIO
import pytest
from tests.utils import Command
from thefuck.rules.apt_invalid_operation import match, get_new_command, \
_get_operations
invalid_operation = 'E: Invalid operation {}'.format
apt_help = b'''apt 1.0.10.2ubuntu1 for amd64 compiled on Oct 5 2015 15:55:05
Usage: apt [options] command
CLI for apt.
Basic commands:
list - list packages based on package names
search - search in package descriptions
show - show package details
update - update list of available packages
install - install packages
remove - remove packages
upgrade - upgrade the system by installing/upgrading packages
full-upgrade - upgrade the system by removing/installing/upgrading packages
edit-sources - edit the source information file
'''
apt_operations = ['list', 'search', 'show', 'update', 'install', 'remove',
'upgrade', 'full-upgrade', 'edit-sources']
apt_get_help = b'''apt 1.0.10.2ubuntu1 for amd64 compiled on Oct 5 2015 15:55:05
Usage: apt-get [options] command
apt-get [options] install|remove pkg1 [pkg2 ...]
apt-get [options] source pkg1 [pkg2 ...]
apt-get is a simple command line interface for downloading and
installing packages. The most frequently used commands are update
and install.
Commands:
update - Retrieve new lists of packages
upgrade - Perform an upgrade
install - Install new packages (pkg is libc6 not libc6.deb)
remove - Remove packages
autoremove - Remove automatically all unused packages
purge - Remove packages and config files
source - Download source archives
build-dep - Configure build-dependencies for source packages
dist-upgrade - Distribution upgrade, see apt-get(8)
dselect-upgrade - Follow dselect selections
clean - Erase downloaded archive files
autoclean - Erase old downloaded archive files
check - Verify that there are no broken dependencies
changelog - Download and display the changelog for the given package
download - Download the binary package into the current directory
Options:
-h This help text.
-q Loggable output - no progress indicator
-qq No output except for errors
-d Download only - do NOT install or unpack archives
-s No-act. Perform ordering simulation
-y Assume Yes to all queries and do not prompt
-f Attempt to correct a system with broken dependencies in place
-m Attempt to continue if archives are unlocatable
-u Show a list of upgraded packages as well
-b Build the source package after fetching it
-V Show verbose version numbers
-c=? Read this configuration file
-o=? Set an arbitrary configuration option, eg -o dir::cache=/tmp
See the apt-get(8), sources.list(5) and apt.conf(5) manual
pages for more information and options.
This APT has Super Cow Powers.
'''
apt_get_operations = ['update', 'upgrade', 'install', 'remove', 'autoremove',
'purge', 'source', 'build-dep', 'dist-upgrade',
'dselect-upgrade', 'clean', 'autoclean', 'check',
'changelog', 'download']
@pytest.mark.parametrize('script, stderr', [
('apt', invalid_operation('saerch')),
('apt-get', invalid_operation('isntall')),
('apt-cache', invalid_operation('rumove'))])
def test_match(script, stderr):
assert match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, stderr', [
('vim', invalid_operation('vim')),
('apt-get', "")])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr))
@pytest.fixture
def set_help(mocker):
mock = mocker.patch('subprocess.Popen')
def _set_text(text):
mock.return_value.stdout = BytesIO(text)
return _set_text
@pytest.mark.parametrize('app, help_text, operations', [
('apt', apt_help, apt_operations),
('apt-get', apt_get_help, apt_get_operations)
])
def test_get_operations(set_help, app, help_text, operations):
set_help(help_text)
assert _get_operations(app) == operations
@pytest.mark.parametrize('script, stderr, help_text, result', [
('apt-get isntall vim', invalid_operation('isntall'),
apt_get_help, 'apt-get install vim'),
('apt saerch vim', invalid_operation('saerch'),
apt_help, 'apt search vim'),
])
def test_get_new_command(set_help, stderr, script, help_text, result):
set_help(help_text)
assert get_new_command(Command(script, stderr=stderr))[0] == result

View File

@@ -63,6 +63,7 @@ def test_side_effect(ext, tar_error, filename, unquoted, quoted, script, fixed):
side_effect(Command(script=script.format(filename.format(ext))), None)
assert set(os.listdir('.')) == {unquoted.format(ext), 'd'}
@parametrize_extensions
@parametrize_filename
@parametrize_script

View File

@@ -1,50 +1,72 @@
# -*- coding: utf-8 -*-
import os
import pytest
import zipfile
from thefuck.rules.dirty_unzip import match, get_new_command, side_effect
from tests.utils import Command
from unicodedata import normalize
@pytest.fixture
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):
with zipfile.ZipFile(path, 'w') as archive:
archive.writestr('a', '1')
archive.writestr('b', '2')
archive.writestr('c', '3')
def reset(path):
with zipfile.ZipFile(path, 'w') as archive:
archive.writestr('a', '1')
archive.writestr('b', '2')
archive.writestr('c', '3')
archive.writestr('d/e', '4')
archive.writestr('d/e', '4')
archive.extractall()
archive.extractall()
os.chdir(str(tmpdir))
reset(path)
os.chdir(str(tmpdir))
reset(path)
assert set(os.listdir('.')) == {'foo.zip', 'a', 'b', 'c', 'd'}
assert set(os.listdir('./d')) == {'e'}
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'}
return zip_error_inner
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_match(zip_error, script):
@pytest.mark.parametrize('script,filename', [
(u'unzip café', u'café.zip'),
(u'unzip café.zip', u'café.zip'),
(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))
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_side_effect(zip_error, script):
@pytest.mark.parametrize('script,filename', [
(u'unzip café', u'café.zip'),
(u'unzip café.zip', u'café.zip'),
(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)
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', [
('unzip foo', 'unzip foo -d foo'),
(R"unzip foo\ bar.zip", R"unzip foo\ bar.zip -d 'foo bar'"),
(R"unzip 'foo bar.zip'", R"unzip 'foo bar.zip' -d 'foo bar'"),
('unzip foo.zip', 'unzip foo.zip -d foo')])
def test_get_new_command(zip_error, script, fixed):
@pytest.mark.parametrize('script,fixed,filename', [
(u'unzip café', u"unzip café -d 'café'", u'café.zip'),
(u'unzip foo', u'unzip foo -d foo', u'foo.zip'),
(u"unzip foo\\ bar.zip", u"unzip foo\\ bar.zip -d 'foo bar'", u'foo.zip'),
(u"unzip 'foo bar.zip'", u"unzip 'foo bar.zip' -d 'foo bar'", u'foo.zip'),
(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

View File

@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest
import os
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
"""),
(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, '',
"""
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]:
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:
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 tests.utils import Command
def test_match():
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())
def test_get_new_command():
assert get_new_command(
Command('grep blah .')) == 'grep -r blah .'
assert get_new_command(Command('grep blah .')) == 'grep -r blah .'
assert get_new_command(Command(u'grep café .')) == u'grep -r café .'

View File

@@ -7,7 +7,7 @@ from tests.utils import Command
Command('mkdir foo/bar/baz', stderr='mkdir: foo/bar: No such file or directory'),
Command('./bin/hdfs dfs -mkdir foo/bar/baz', stderr='mkdir: `foo/bar/baz\': No such file or directory'),
Command('hdfs dfs -mkdir foo/bar/baz', stderr='mkdir: `foo/bar/baz\': No such file or directory')
])
])
def test_match(command):
assert match(command)
@@ -17,7 +17,8 @@ def test_match(command):
Command('mkdir foo/bar/baz', stderr='foo bar baz'),
Command('hdfs dfs -mkdir foo/bar/baz'),
Command('./bin/hdfs dfs -mkdir foo/bar/baz'),
Command()])
Command(),
])
def test_not_match(command):
assert not match(command)
@@ -25,7 +26,7 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command('mkdir foo/bar/baz'), 'mkdir -p foo/bar/baz'),
(Command('hdfs dfs -mkdir foo/bar/baz'), 'hdfs dfs -mkdir -p foo/bar/baz'),
(Command('./bin/hdfs dfs -mkdir foo/bar/baz'), './bin/hdfs dfs -mkdir -p foo/bar/baz')])
(Command('./bin/hdfs dfs -mkdir foo/bar/baz'), './bin/hdfs dfs -mkdir -p foo/bar/baz'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -32,9 +32,9 @@ def test_match(command):
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package', 'mvn clean install']),
(Command(script='mvn -N', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn -N clean package', 'mvn -N clean install'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -32,9 +32,9 @@ def test_match(command):
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean', 'mvn compile']),
(Command(script='mvn claen package', stdout='[ERROR] Unknown lifecycle phase "claen". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -6,7 +6,7 @@ from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"),
Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"),
])
])
def test_match(command):
assert match(command)
@@ -14,7 +14,7 @@ def test_match(command):
@pytest.mark.parametrize('command', [
Command(script='mv foo bar/', stderr=""),
Command(script='mv foo bar/foo', stderr="mv: permission denied"),
])
])
def test_not_match(command):
assert not match(command)
@@ -22,6 +22,6 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"), 'mkdir -p bar && mv foo bar/foo'),
(Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"), 'mkdir -p bar && mv foo bar/'),
])
])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -7,25 +7,25 @@ from tests.utils import Command
Command('rm foo', stderr='rm: foo: is a directory'),
Command('rm foo', stderr='rm: foo: Is a directory'),
Command('hdfs dfs -rm foo', stderr='rm: `foo`: Is a directory'),
Command('./bin/hdfs dfs -rm foo', stderr='rm: `foo`: Is a directory')
])
Command('./bin/hdfs dfs -rm foo', stderr='rm: `foo`: Is a directory'),
])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command('rm foo'),
Command('rm foo'),
Command('hdfs dfs -rm foo'),
Command('./bin/hdfs dfs -rm foo'),
Command()])
Command('./bin/hdfs dfs -rm foo'),
Command(),
])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, new_command', [
(Command('rm foo'), 'rm -rf foo'),
(Command('hdfs dfs -rm foo'), 'hdfs dfs -rm -r foo')])
(Command('hdfs dfs -rm foo'), 'hdfs dfs -rm -r foo'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -1,6 +1,5 @@
import os
import pytest
from mock import Mock
from thefuck.rules.ssh_known_hosts import match, get_new_command,\
side_effect
from tests.utils import Command

View File

@@ -19,11 +19,13 @@ def test_match(stderr, stdout):
def test_not_match():
assert not match(Command())
assert not match(Command(script='sudo ls', stderr='Permission denied'))
@pytest.mark.parametrize('before, after', [
('ls', 'sudo ls'),
('echo a > b', 'sudo sh -c "echo a > b"'),
('echo "a" >> b', 'sudo sh -c "echo \\"a\\" >> b"')])
('echo "a" >> b', 'sudo sh -c "echo \\"a\\" >> b"'),
('mkdir && touch a', 'sudo sh -c "mkdir && touch a"')])
def test_get_new_command(before, after):
assert get_new_command(Command(before)) == after

View File

@@ -1,4 +1,3 @@
import pytest
from thefuck.rules.systemctl import match, get_new_command
from tests.utils import Command

View File

@@ -14,8 +14,8 @@ def test_match(command):
@pytest.mark.parametrize('command', [
Command(script='./bin/hdfs dfs -ls', stderr=''),
Command(script='./bin/hdfs dfs -ls /foo/bar', stderr=''),
Command(script='hdfs dfs -ls -R /foo/bar', stderr=''),
Command(script='./bin/hdfs dfs -ls /foo/bar', stderr=''),
Command(script='hdfs dfs -ls -R /foo/bar', stderr=''),
Command()])
def test_not_match(command):
assert not match(command)
@@ -32,4 +32,3 @@ def test_not_match(command):
stderr='ls: Unknown command\nDid you mean -ls? This command begins with a dash.'), ['./bin/hdfs dfs -Dtest=fred -ls -R /foo/bar'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -16,8 +16,8 @@ def test_match(command):
@pytest.mark.parametrize('command', [
Command(script='vagrant ssh', stderr=''),
Command(script='vagrant ssh jeff', stderr='The machine with the name \'jeff\' was not found configured for this Vagrant environment.'),
Command(script='vagrant ssh', stderr='A Vagrant environment or target machine is required to run this command. Run `vagrant init` to create a new Vagrant environment. Or, get an ID of a target machine from `vagrant global-status` to run this command on. A final option is to change to a directory with a Vagrantfile and to try again.'),
Command(script='vagrant ssh jeff', stderr='The machine with the name \'jeff\' was not found configured for this Vagrant environment.'),
Command(script='vagrant ssh', stderr='A Vagrant environment or target machine is required to run this command. Run `vagrant init` to create a new Vagrant environment. Or, get an ID of a target machine from `vagrant global-status` to run this command on. A final option is to change to a directory with a Vagrantfile and to try again.'),
Command()])
def test_not_match(command):
assert not match(command)
@@ -32,4 +32,3 @@ def test_not_match(command):
stderr='VM must be created before running this command. Run `vagrant up` first.'), ['vagrant up devbox && vagrant rdp devbox', 'vagrant up && vagrant rdp devbox'])])
def test_get_new_command(command, new_command):
assert get_new_command(command) == new_command

View File

@@ -1,5 +1,4 @@
import pytest
from mock import Mock
from thefuck.specific.sudo import sudo_support
from tests.utils import Command

View File

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

View File

@@ -1,5 +1,4 @@
import pytest
from mock import Mock
from thefuck import logs

View File

@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck import shells
@@ -18,7 +20,7 @@ def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__\
.return_value.__iter__.return_value = lines
.return_value.readlines.return_value = lines
return aux
@@ -35,6 +37,7 @@ class TestGeneric(object):
def test_put_to_history(self, builtins_open, shell):
assert shell.put_to_history('ls') is None
assert shell.put_to_history(u'echo café') is None
assert builtins_open.call_count == 0
def test_and_(self, shell):
@@ -48,6 +51,7 @@ class TestGeneric(object):
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' 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(['ls', 'rm'])
@@ -55,6 +59,10 @@ class TestGeneric(object):
# so just ignore them:
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')
class TestBash(object):
@@ -83,10 +91,13 @@ class TestBash(object):
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, shell):
shell.put_to_history('ls')
@pytest.mark.parametrize('entry, entry_utf8', [
('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. \
write.assert_called_once_with('ls\n')
write.assert_called_once_with(entry_utf8)
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
@@ -102,6 +113,7 @@ class TestBash(object):
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' 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(['ls', 'rm'])
@@ -152,12 +164,15 @@ class TestFish(object):
def test_to_shell(self, shell):
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',
return_value=1430707243.3517463)
shell.put_to_history('ls')
shell.put_to_history(entry)
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):
assert shell.and_('foo', 'bar') == 'foo; and bar'
@@ -179,6 +194,12 @@ class TestFish(object):
assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' 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')
@@ -207,12 +228,15 @@ class TestZsh(object):
def test_to_shell(self, shell):
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',
return_value=1430707243.3517463)
shell.put_to_history('ls')
shell.put_to_history(entry)
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):
assert shell.and_('ls', 'cd') == 'ls && cd'
@@ -229,6 +253,7 @@ class TestZsh(object):
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' 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([': 1432613911:0;ls', ': 1432613916:0;rm'])

View File

@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from subprocess import PIPE
from mock import Mock
from pathlib import Path
@@ -19,6 +21,12 @@ class TestCorrectedCommand(object):
assert {CorrectedCommand('ls', None, 100),
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):
def test_from_path(self, mocker):
@@ -122,4 +130,3 @@ class TestCommand(object):
else:
with pytest.raises(EmptyCommand):
Command.from_raw_script(script)

View File

@@ -4,37 +4,32 @@ import pytest
from itertools import islice
from thefuck import ui
from thefuck.types import CorrectedCommand
from thefuck import const
@pytest.fixture
def patch_getch(monkeypatch):
def patch_get_key(monkeypatch):
def patch(vals):
def getch():
for val in vals:
if val == KeyboardInterrupt:
raise val
else:
yield val
getch_gen = getch()
monkeypatch.setattr('thefuck.ui.getch', lambda: next(getch_gen))
vals = iter(vals)
monkeypatch.setattr('thefuck.ui.get_key', lambda: next(vals))
return patch
def test_read_actions(patch_getch):
patch_getch([ # Enter:
'\n',
# Enter:
'\r',
# Ignored:
'x', 'y',
# Up:
'\x1b', '[', 'A',
# Down:
'\x1b', '[', 'B',
# Ctrl+C:
KeyboardInterrupt], )
def test_read_actions(patch_get_key):
patch_get_key([
# Enter:
'\n',
# Enter:
'\r',
# Ignored:
'x', 'y',
# Up:
const.KEY_UP,
# Down:
const.KEY_DOWN,
# Ctrl+C:
const.KEY_CTRL_C])
assert list(islice(ui.read_actions(), 5)) \
== [ui.SELECT, ui.SELECT, ui.PREVIOUS, ui.NEXT, ui.ABORT]
@@ -80,25 +75,25 @@ class TestSelectCommand(object):
== commands_with_side_effect[0]
assert capsys.readouterr() == ('', 'ls (+side effect)\n')
def test_with_confirmation(self, capsys, patch_getch, commands):
patch_getch(['\n'])
def test_with_confirmation(self, capsys, patch_get_key, commands):
patch_get_key(['\n'])
assert ui.select_command(iter(commands)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_abort(self, capsys, patch_getch, commands):
patch_getch([KeyboardInterrupt])
def test_with_confirmation_abort(self, capsys, patch_get_key, commands):
patch_get_key([const.KEY_CTRL_C])
assert ui.select_command(iter(commands)) is None
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n')
def test_with_confirmation_with_side_effct(self, capsys, patch_getch,
def test_with_confirmation_with_side_effct(self, capsys, patch_get_key,
commands_with_side_effect):
patch_getch(['\n'])
assert ui.select_command(iter(commands_with_side_effect))\
patch_get_key(['\n'])
assert ui.select_command(iter(commands_with_side_effect)) \
== commands_with_side_effect[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_select_second(self, capsys, patch_getch, commands):
patch_getch(['\x1b', '[', 'B', '\n'])
def test_with_confirmation_select_second(self, capsys, patch_get_key, commands):
patch_get_key([const.KEY_DOWN, '\n'])
assert ui.select_command(iter(commands)) == commands[1]
assert capsys.readouterr() == (
'', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n')

View File

@@ -16,6 +16,8 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'no_colors': False,
'debug': False,
'priority': {},
'history_limit': None,
'alter_history': True,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
@@ -23,8 +25,10 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_DEBUG': 'debug',
'THEFUCK_PRIORITY': 'priority',
'THEFUCK_DEBUG': 'debug'}
'THEFUCK_HISTORY_LIMIT': 'history_limit',
'THEFUCK_ALTER_HISTORY': 'alter_history'}
SETTINGS_HEADER = u"""# The Fuck settings file
#
@@ -124,8 +128,11 @@ class Settings(dict):
return dict(self._priority_from_env(val))
elif attr == 'wait_command':
return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug'):
elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history'):
return val.lower() == 'true'
elif attr == 'history_limit':
return int(val)
else:
return val

14
thefuck/const.py Normal file
View File

@@ -0,0 +1,14 @@
# -*- encoding: utf-8 -*-
class _GenConst(object):
def __init__(self, name):
self._name = name
def __repr__(self):
return u'<const: {}>'.format(self._name)
KEY_UP = _GenConst('')
KEY_DOWN = _GenConst('')
KEY_CTRL_C = _GenConst('Ctrl+C')

View File

@@ -55,7 +55,7 @@ def organize_commands(corrected_commands):
key=lambda corrected_command: corrected_command.priority)
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:
yield command

View File

@@ -1,8 +1,12 @@
# Initialize output before importing any module, that can use colorama.
from .system import init_output
init_output()
from argparse import ArgumentParser
from warnings import warn
from pprint import pformat
import sys
import colorama
from . import logs, types, shells
from .conf import settings
from .corrector import get_corrected_commands
@@ -13,7 +17,6 @@ from .ui import select_command
def fix_command():
"""Fixes previous command. Used when `thefuck` called without arguments."""
colorama.init()
settings.init()
with logs.debug_time('Total'):
logs.debug(u'Run with settings: {}'.format(pformat(settings)))
@@ -51,7 +54,6 @@ def how_to_configure_alias():
It'll be only visible when user type fuck and when alias isn't configured.
"""
colorama.init()
settings.init()
logs.how_to_configure_alias(shells.how_to_configure())

View File

@@ -0,0 +1,53 @@
import subprocess
from thefuck.specific.sudo import sudo_support
from thefuck.utils import for_app, eager, replace_command
@for_app('apt', 'apt-get', 'apt-cache')
@sudo_support
def match(command):
return 'E: Invalid operation' in command.stderr
@eager
def _parse_apt_operations(help_text_lines):
is_commands_list = False
for line in help_text_lines:
line = line.decode().strip()
if is_commands_list and line:
yield line.split()[0]
elif line.startswith('Basic commands:'):
is_commands_list = True
@eager
def _parse_apt_get_and_cache_operations(help_text_lines):
is_commands_list = False
for line in help_text_lines:
line = line.decode().strip()
if is_commands_list:
if not line:
return
yield line.split()[0]
elif line.startswith('Commands:'):
is_commands_list = True
def _get_operations(app):
proc = subprocess.Popen([app, '--help'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
lines = proc.stdout.readlines()
if app == 'apt':
return _parse_apt_operations(lines)
else:
return _parse_apt_get_and_cache_operations(lines)
@sudo_support
def get_new_command(command):
invalid_operation = command.stderr.split()[-1]
operations = _get_operations(command.script_parts[0])
return replace_command(command, invalid_operation, operations)

View File

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

View File

@@ -8,7 +8,8 @@ from thefuck.specific.sudo import sudo_support
@for_app('cd')
def match(command):
return (('no such file or directory' in command.stderr.lower()
or 'cd: can\'t cd to' in command.stderr.lower()))
or 'cd: can\'t cd to' in command.stderr.lower()
or 'the system cannot find the path specified.' in command.stderr.lower()))
@sudo_support

View File

@@ -19,7 +19,6 @@ def _is_tar_extract(cmd):
def _tar_file(cmd):
for c in cmd:
for ext in tar_extensions:
if c.endswith(ext):

View File

@@ -5,8 +5,11 @@ from thefuck.shells import quote
def _is_bad_zip(file):
with zipfile.ZipFile(file, 'r') as archive:
return len(archive.namelist()) > 1
try:
with zipfile.ZipFile(file, 'r') as archive:
return len(archive.namelist()) > 1
except:
return False
def _zip_file(command):
@@ -19,17 +22,23 @@ def _zip_file(command):
if c.endswith('.zip'):
return c
else:
return '{}.zip'.format(c)
return u'{}.zip'.format(c)
@for_app('unzip')
def match(command):
return ('-d' not in command.script
and _is_bad_zip(_zip_file(command)))
if '-d' in command.script:
return False
zip_file = _zip_file(command)
if zip_file:
return _is_bad_zip(zip_file)
else:
return False
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):

View File

@@ -7,38 +7,38 @@ from thefuck import shells
# order is important: only the first match is considered
patterns = (
# js, node:
'^ at {file}:{line}:{col}',
# cargo:
'^ {file}:{line}:{col}',
# python, thefuck:
'^ File "{file}", line {line}',
# awk:
'^awk: {file}:{line}:',
# git
'^fatal: bad config file line {line} in {file}',
# llc:
'^llc: {file}:{line}:{col}:',
# lua:
'^lua: {file}:{line}:',
# fish:
'^{file} \\(line {line}\\):',
# bash, sh, ssh:
'^{file}: line {line}: ',
# cargo, clang, gcc, go, pep8, rustc:
'^{file}:{line}:{col}',
# ghc, make, ruby, zsh:
'^{file}:{line}:',
# perl:
'at {file} line {line}',
)
# js, node:
'^ at {file}:{line}:{col}',
# cargo:
'^ {file}:{line}:{col}',
# python, thefuck:
'^ File "{file}", line {line}',
# awk:
'^awk: {file}:{line}:',
# git
'^fatal: bad config file line {line} in {file}',
# llc:
'^llc: {file}:{line}:{col}:',
# lua:
'^lua: {file}:{line}:',
# fish:
'^{file} \\(line {line}\\):',
# bash, sh, ssh:
'^{file}: line {line}: ',
# cargo, clang, gcc, go, pep8, rustc:
'^{file}:{line}:{col}',
# ghc, make, ruby, zsh:
'^{file}:{line}:',
# perl:
'at {file} line {line}',
)
# for the sake of readability do not use named groups above
def _make_pattern(pattern):
pattern = pattern.replace('{file}', '(?P<file>[^:\n]+)') \
.replace('{line}', '(?P<line>[0-9]+)') \
.replace('{col}', '(?P<col>[0-9]+)')
.replace('{col}', '(?P<col>[0-9]+)')
return re.compile(pattern, re.MULTILINE)
patterns = [_make_pattern(p).search for p in patterns]
@@ -58,7 +58,7 @@ def match(command):
return _search(command.stderr) or _search(command.stdout)
@default_settings({'fixlinecmd': '{editor} {file} +{line}',
@default_settings({'fixlinecmd': u'{editor} {file} +{line}',
'fixcolcmd': None})
def get_new_command(command):
m = _search(command.stderr) or _search(command.stdout)

View File

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

View File

@@ -16,10 +16,14 @@ patterns = ['permission denied',
'need root',
'only root can ',
'You don\'t have access to the history DB.',
'authentication is required']
'authentication is required',
'eDSPermissionError']
def match(command):
if command.script_parts and '&&' not in command.script_parts and command.script_parts[0] == 'sudo':
return False
for pattern in patterns:
if pattern.lower() in command.stderr.lower()\
or pattern.lower() in command.stdout.lower():
@@ -28,7 +32,9 @@ def match(command):
def get_new_command(command):
if '>' in command.script:
if '&&' in command.script:
return u'sudo sh -c "{}"'.format(" ".join([part for part in command.script_parts if part != "sudo"]))
elif '>' in command.script:
return u'sudo sh -c "{}"'.format(command.script.replace('"', '\\"'))
else:
return u'sudo {}'.format(command.script)

View File

@@ -13,6 +13,6 @@ def get_new_command(command):
command.stderr)
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)

View File

@@ -1,6 +1,7 @@
import re
from thefuck.utils import replace_command
def match(command):
return (re.search(r"([^:]*): Unknown command.*", command.stderr) != None
and re.search(r"Did you mean ([^?]*)?", command.stderr) != None)
@@ -10,4 +11,3 @@ def get_new_command(command):
broken_cmd = re.findall(r"([^:]*): Unknown command.*", command.stderr)[0]
matched = re.findall(r"Did you mean ([^?]*)?", command.stderr)
return replace_command(command, broken_cmd, matched)

View File

@@ -17,4 +17,4 @@ def get_new_command(command):
if machine is None:
return startAllInstances
else:
return [ shells.and_("vagrant up " + machine, command.script), startAllInstances]
return [shells.and_("vagrant up " + machine, command.script), startAllInstances]

View File

@@ -10,12 +10,14 @@ from time import time
import io
import os
import shlex
import sys
import six
from .utils import DEVNULL, memoize, cache
from .conf import settings
from . import logs
class Generic(object):
def get_aliases(self):
return {}
@@ -36,7 +38,8 @@ class Generic(object):
return command_script
def app_alias(self, fuck):
return "alias {0}='TF_ALIAS={0} eval $(thefuck $(fc -ln -1))'".format(fuck)
return "alias {0}='TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1))'".format(fuck)
def _get_history_file_name(self):
return ''
@@ -49,25 +52,26 @@ class Generic(object):
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))
def _script_from_history(self, line):
"""Returns prepared history line.
Should return a blank line if history line is corrupted or empty.
"""
return ''
entry = self._get_history_line(command_script)
if six.PY2:
history.write(entry.encode('utf-8'))
else:
history.write(entry)
def get_history(self):
"""Returns list of history entries."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with io.open(history_file_name, 'r',
encoding='utf-8', errors='ignore') as history:
for line in history:
prepared = self._script_from_history(line)\
.strip()
encoding='utf-8', errors='ignore') as history_file:
lines = history_file.readlines()
if settings.history_limit:
lines = lines[-settings.history_limit:]
for line in lines:
prepared = self._script_from_history(line) \
.strip()
if prepared:
yield prepared
@@ -79,6 +83,8 @@ class Generic(object):
def split_command(self, command):
"""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)
def quote(self, s):
@@ -91,10 +97,14 @@ class Generic(object):
return quote(s)
def _script_from_history(self, line):
return line
class Bash(Generic):
def app_alias(self, fuck):
return "TF_ALIAS={0} alias {0}='eval $(thefuck $(fc -ln -1));" \
return "TF_ALIAS={0} alias {0}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1));" \
" history -r'".format(fuck)
def _parse_alias(self, alias):
@@ -108,9 +118,9 @@ class Bash(Generic):
def get_aliases(self):
proc = Popen(['bash', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
@@ -119,9 +129,6 @@ class Bash(Generic):
def _get_history_line(self, command_script):
return u'{}\n'.format(command_script)
def _script_from_history(self, line):
return line
def how_to_configure(self):
if os.path.join(os.path.expanduser('~'), '.bashrc'):
config = '~/.bashrc'
@@ -133,7 +140,6 @@ class Bash(Generic):
class Fish(Generic):
def _get_overridden_aliases(self):
overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip()
if overridden_aliases:
@@ -143,18 +149,18 @@ class Fish(Generic):
def app_alias(self, fuck):
return ('function {0} -d "Correct your previous console command"\n'
' set -l exit_code $status\n'
' set -x TF_ALIAS {0}\n'
' set -l fucked_up_command $history[1]\n'
' set -l exit_code $status\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'
' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n'
' if test $exit_code -ne 0\n'
' history --delete $fucked_up_command\n'
' history --merge ^ /dev/null\n'
' return 0\n'
' end\n'
' if [ "$unfucked_command" != "" ]\n'
' eval $unfucked_command\n'
' if test $exit_code -ne 0\n'
' history --delete $fucked_up_command\n'
' history --merge ^ /dev/null\n'
' return 0\n'
' end\n'
' end\n'
'end').format(fuck)
@memoize
@@ -182,6 +188,12 @@ class Fish(Generic):
def _get_history_line(self, command_script):
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):
return u'; and '.join(commands)
@@ -192,7 +204,8 @@ class Fish(Generic):
class Zsh(Generic):
def app_alias(self, fuck):
return "TF_ALIAS={0}" \
" alias {0}='eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" alias {0}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" fc -R'".format(fuck)
def _parse_alias(self, alias):
@@ -206,9 +219,9 @@ class Zsh(Generic):
def get_aliases(self):
proc = Popen(['zsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '=' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
@@ -241,9 +254,9 @@ class Tcsh(Generic):
def get_aliases(self):
proc = Popen(['tcsh', '-ic', 'alias'], stdout=PIPE, stderr=DEVNULL)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '\t' in alias)
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias and '\t' in alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
@@ -290,7 +303,10 @@ def thefuck_alias():
def put_to_history(command):
return _get_shell().put_to_history(command)
try:
return _get_shell().put_to_history(command)
except IOError:
logs.exception("Can't update history", sys.exc_info())
def and_(*commands):

View File

@@ -1,6 +1,5 @@
import six
from decorator import decorator
from ..types import Command
@decorator

View File

@@ -0,0 +1,7 @@
import sys
if sys.platform == 'win32':
from .win32 import *
else:
from .unix import *

35
thefuck/system/unix.py Normal file
View File

@@ -0,0 +1,35 @@
import sys
import tty
import termios
import colorama
from .. import const
init_output = colorama.init
def getch():
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
def get_key():
ch = getch()
if ch == '\x03':
return const.KEY_CTRL_C
elif ch == '\x1b':
next_ch = getch()
if next_ch == '[':
last_ch = getch()
if last_ch == 'A':
return const.KEY_UP
elif last_ch == 'B':
return const.KEY_DOWN
return ch

25
thefuck/system/win32.py Normal file
View File

@@ -0,0 +1,25 @@
import sys
import msvcrt
import win_unicode_console
from .. import const
def init_output():
import colorama
win_unicode_console.enable()
colorama.init()
def get_key():
ch = msvcrt.getch()
if ch in (b'\x00', b'\xe0'): # arrow or function key prefix?
ch = msvcrt.getch() # second call returns the actual key code
if ch == b'\x03':
raise const.KEY_CTRL_C
if ch == b'H':
return const.KEY_UP
if ch == b'P':
return const.KEY_DOWN
return ch.decode(sys.stdout.encoding)

View File

@@ -31,21 +31,21 @@ class Command(object):
try:
self._script_parts = shells.split_command(self.script)
except Exception:
logs.debug("Can't split command script {} because:\n {}".format(
self, sys.exc_info()))
logs.debug(u"Can't split command script {} because:\n {}".format(
self, sys.exc_info()))
self._script_parts = None
return self._script_parts
def __eq__(self, other):
if isinstance(other, Command):
return (self.script, self.stdout, self.stderr) \
== (other.script, other.stdout, other.stderr)
== (other.script, other.stdout, other.stderr)
else:
return False
def __repr__(self):
return 'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr)
return u'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr)
def update(self, **kwargs):
"""Returns new command with replaced fields.
@@ -166,9 +166,9 @@ class Rule(object):
return 'Rule(name={}, match={}, get_new_command={}, ' \
'enabled_by_default={}, side_effect={}, ' \
'priority={}, requires_output)'.format(
self.name, self.match, self.get_new_command,
self.enabled_by_default, self.side_effect,
self.priority, self.requires_output)
self.name, self.match, self.get_new_command,
self.enabled_by_default, self.side_effect,
self.priority, self.requires_output)
@classmethod
def from_path(cls, path):
@@ -267,9 +267,9 @@ class CorrectedCommand(object):
return (self.script, self.side_effect).__hash__()
def __repr__(self):
return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
def run(self, old_cmd):
"""Runs command from rule for passed command.
@@ -278,5 +278,7 @@ class CorrectedCommand(object):
"""
if self.side_effect:
compatibility_call(self.side_effect, old_cmd, self.script)
shells.put_to_history(self.script)
if settings.alter_history:
shells.put_to_history(self.script)
# This depends on correct setting of PYTHONIOENCODING by the alias:
print(self.script)

View File

@@ -3,25 +3,8 @@
import sys
from .conf import settings
from .exceptions import NoRuleMatched
from . import logs
try:
from msvcrt import getch
except ImportError:
def getch():
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
if ch == '\x03': # For compatibility with msvcrt.getch
raise KeyboardInterrupt
return ch
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
from .system import get_key
from . import logs, const
SELECT = 0
ABORT = 1
@@ -31,23 +14,17 @@ NEXT = 3
def read_actions():
"""Yields actions for pressed keys."""
buffer = []
while True:
try:
ch = getch()
except KeyboardInterrupt: # Ctrl+C
yield ABORT
key = get_key()
if ch in ('\n', '\r'): # Enter
yield SELECT
buffer.append(ch)
buffer = buffer[-3:]
if buffer == ['\x1b', '[', 'A'] or ch == 'k': # ↑
if key in (const.KEY_UP, 'k'):
yield PREVIOUS
elif buffer == ['\x1b', '[', 'B'] or ch == 'j': # ↓
elif key in (const.KEY_DOWN, 'j'):
yield NEXT
elif key == const.KEY_CTRL_C:
yield ABORT
elif key in ('\n', '\r'):
yield SELECT
class CommandSelector(object):

View File

@@ -1,18 +1,17 @@
from difflib import get_close_matches
from functools import wraps
import shelve
from warnings import warn
from decorator import decorator
from contextlib import closing
import dbm
import os
import pickle
import re
from inspect import getargspec
from pathlib import Path
import pkg_resources
import re
import shelve
from .conf import settings
from contextlib import closing
from decorator import decorator
from difflib import get_close_matches
from functools import wraps
from inspect import getargspec
from pathlib import Path
from warnings import warn
DEVNULL = open(os.devnull, 'w')
@@ -23,11 +22,16 @@ def memoize(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs))
if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs)
if not memoize.disabled:
key = pickle.dumps((args, kwargs))
if key not in memo:
memo[key] = fn(*args, **kwargs)
value = memo[key]
else:
# Memoize is disabled, call the function
value = fn(*args, **kwargs)
return memo[key]
return value
return wrapper
memoize.disabled = False
@@ -106,7 +110,7 @@ def get_all_executables():
def replace_argument(script, from_, to):
"""Replaces command line argument."""
replaced_in_the_end = re.sub(u' {}$'.format(from_), u' {}'.format(to),
replaced_in_the_end = re.sub(u' {}$'.format(re.escape(from_)), u' {}'.format(to),
script, count=1)
if replaced_in_the_end != script:
return replaced_in_the_end
@@ -205,13 +209,24 @@ def cache(*depends_on):
etag = '.'.join(_get_mtime(name) for name in depends_on)
cache_path = _get_cache_path()
with closing(shelve.open(cache_path)) as db:
if db.get(key, {}).get('etag') == etag:
return db[key]['value']
else:
try:
with closing(shelve.open(cache_path)) as db:
if db.get(key, {}).get('etag') == etag:
return db[key]['value']
else:
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
except dbm.error:
# Caused when going from Python 2 to Python 3
warn("Removing possibly out-dated cache")
os.remove(cache_path)
with closing(shelve.open(cache_path)) as db:
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
return _cache
cache.disabled = False