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

Compare commits

..

1 Commits

Author SHA1 Message Date
nvbn
0fcc35c227 #380 merge cd_mkdir and cd_correction rules 2015-10-12 17:52:44 +08:00
78 changed files with 429 additions and 993 deletions

View File

@@ -1,25 +0,0 @@
# Report issues
If you have any issue with The Fuck, sorry about that, but we will do what we
can to fix that. Actually, maybe we already have, so first thing to do is to
update The Fuck and see if the bug is still there.
If it is (sorry again), check if the problem has not already been reported and
if not, just open an issue on [GitHub](https://github.com/nvbn/thefuck) with
the following basic information:
- the output of `thefuck --version` (something like `The Fuck 3.1 using
Python 3.5.0`);
- your shell and its version (`bash`, `zsh`, *Windows PowerShell*, etc.);
- your system (Debian 7, ArchLinux, Windows, etc.);
- how to reproduce the bug;
- the output of The Fuck with `THEFUCK_DEBUG=true` exported (typically execute
`export THEFUCK_DEBUG=true` in your shell before The Fuck);
- if the bug only appears with a specific application, the output of that
application and its version;
- anything else you think is relevant.
It's only with enough information that we can do something to fix the problem.
# Make a pull request
We gladly accept pull request on the [official
repository](https://github.com/nvbn/thefuck) for new rules, new features, bug
fixes, etc.

View File

@@ -141,8 +141,8 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `cargo` – runs `cargo build` instead of `cargo`;
* `cargo_no_command` – fixes wrongs commands like `cargo buid`;
* `cd_correction` – spellchecks and correct failed cd commands;
* `cd_mkdir` – creates directories before cd'ing into them;
* `cd_correction` – spellchecks and correct failed cd commands, when it's not possible
creates directories before cd'ing into them;
* `cd_parent` – changes `cd..` to `cd ..`;
* `composer_not_command` – fixes composer command name;
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
@@ -167,7 +167,6 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_push_pull` – runs `git pull` when `push` was rejected;
* `git_stash` – stashes you local modifications before rebasing or switching branch;
* `git_two_dashes` – adds a missing dash to commands like `git commit -amend` or `git rebase -continue`;
* `go_run` – appends `.go` extension when compiling/running Go programs
* `grep_recursive` – adds `-r` when you trying to `grep` directory;
* `gulp_not_task` – fixes misspelled `gulp` tasks;
@@ -211,7 +210,6 @@ 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;
@@ -220,13 +218,13 @@ Enabled by default only on specific platforms:
Bundled, but not enabled by default:
* `git_push_force` – adds `--force-with-lease` to a `git push` (may conflict with `git_push_pull`);
* `git_push_force` – adds `--force` to a `git push` (may conflict with `git_push_pull`);
* `rm_root` – adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules
For adding your own rule you should create `your-rule-name.py`
in `~/.config/thefuck/rules`. The rule should contain two functions:
in `~/.thefuck/rules`. The rule should contain two functions:
```python
match(command: Command) -> bool
@@ -243,7 +241,7 @@ and optional `enabled_by_default`, `requires_output` and `priority` variables.
`Command` has three attributes: `script`, `stdout` and `stderr`.
*Rules api changed in 3.0:* For accessing settings in rule you need to import it with `from thefuck.conf import settings`.
`settings` is a special object filled with `~/.config/thefuck/settings.py` and values from env ([see more below](#settings)).
`settings` is a special object filled with `~/.thefuck/settings.py` and values from env ([see more below](#settings)).
Simple example of the rule for running script with `sudo`:
@@ -273,7 +271,7 @@ requires_output = True
## Settings
The Fuck has a few settings parameters which can be changed in `$XDG_CONFIG_HOME/thefuck/settings.py` (`$XDG_CONFIG_HOME` defaults to `~/.config`):
The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`:
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `exclude_rules` – list of disabled rules, by default `[]`;
@@ -281,9 +279,7 @@ 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`;
* `history_limit` – numeric value of how many history commands will be scanned, like `2000`;
* `alter_history` – push fixed command to history, by default `True`.
* `debug` – enables debug output, by default `False`.
Example of `settings.py`:
@@ -306,9 +302,7 @@ 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_HISTORY_LIMIT` – how many history commands will be scanned, like `2000`;
* `THEFUCK_ALTER_HISTORY` – push fixed command to history `true/false`.
* `THEFUCK_DEBUG` – enables debug output, `true/false`.
For example:
@@ -319,7 +313,6 @@ 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

@@ -8,54 +8,50 @@ installed () {
hash $1 2>/dev/null
}
install_thefuck () {
# Install OS dependencies:
if installed apt-get; then
# Debian/Ubuntu:
sudo apt-get update -yy
sudo apt-get install -yy python-pip python-dev command-not-found python-gdbm
# Install os dependencies:
if installed apt-get; then
# Debian/ubuntu:
sudo apt-get update -yy
sudo apt-get install -yy python-pip python-dev command-not-found
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
# In case of different python versions:
sudo apt-get install -yy python-commandnotfound
fi
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
# In case of different python versions:
sudo apt-get install -yy python-commandnotfound
fi
else
if installed brew; then
# OS X:
brew update
brew install python
else
if installed brew; then
# OS X:
brew update
brew install python
else
# Generic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py
fi
# Genreic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py
fi
fi
# thefuck requires fresh versions of setuptools and pip:
sudo pip install -U pip setuptools
sudo pip install -U thefuck
# thefuck requires fresh versions of setuptools and pip:
sudo pip install -U pip setuptools
sudo pip install -U thefuck
# Setup aliases:
if should_add_alias ~/.bashrc; then
echo 'eval $(thefuck --alias)' >> ~/.bashrc
fi
# Setup aliases:
if should_add_alias ~/.bashrc; then
echo 'eval $(thefuck --alias)' >> ~/.bashrc
fi
if should_add_alias ~/.bash_profile; then
echo 'eval $(thefuck --alias)' >> ~/.bash_profile
fi
if should_add_alias ~/.bash_profile; then
echo 'eval $(thefuck --alias)' >> ~/.bash_profile
fi
if should_add_alias ~/.zshrc; then
echo 'eval $(thefuck --alias)' >> ~/.zshrc
fi
if should_add_alias ~/.zshrc; then
echo 'eval $(thefuck --alias)' >> ~/.zshrc
fi
if should_add_alias ~/.config/fish/config.fish; then
thefuck --alias >> ~/.config/fish/config.fish
fi
if should_add_alias ~/.config/fish/config.fish; then
thefuck --alias >> ~/.config/fish/config.fish
fi
if should_add_alias ~/.tcshrc; then
echo 'eval `thefuck --alias`' >> ~/.tcshrc
fi
}
install_thefuck
if should_add_alias ~/.tcshrc; then
echo 'eval `thefuck --alias`' >> ~/.tcshrc
fi

View File

@@ -20,11 +20,10 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '3.3'
VERSION = '3.1'
install_requires = ['psutil', 'colorama', 'six', 'decorator']
extras_require = {':python_version<"3.4"': ['pathlib'],
":sys_platform=='win32'": ['win_unicode_console']}
extras_require = {':python_version<"3.4"': ['pathlib']}
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: u' && '.join(x))
monkeypatch.setattr('thefuck.shells.and_', lambda *x: ' && '.join(x))

View File

@@ -1,122 +0,0 @@
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

@@ -31,7 +31,8 @@ def test_match(brew_no_available_formula, brew_already_installed,
stderr=brew_no_available_formula))
assert not match(Command('brew install git',
stderr=brew_already_installed))
assert not match(Command('brew install', stderr=brew_install_no_argument))
assert not match(Command('brew install', stderr=brew_install_no_argument),
None)
@pytest.mark.skipif(_is_not_okay_to_test(),
@@ -42,5 +43,5 @@ def test_get_new_command(brew_no_available_formula):
== 'brew install elasticsearch'
assert get_new_command(Command('brew install aa',
stderr=brew_no_available_formula))\
!= 'brew install aha'
stderr=brew_no_available_formula),
None) != 'brew install aha'

View File

@@ -1,5 +1,5 @@
import pytest
from thefuck.rules.cd_mkdir import match, get_new_command
from thefuck.rules.cd_correction import match, get_new_command
from tests.utils import Command

View File

@@ -1,8 +1,7 @@
import os
import pytest
import tarfile
from thefuck.rules.dirty_untar import match, get_new_command, side_effect, \
tar_extensions
from thefuck.rules.dirty_untar import match, get_new_command, side_effect
from tests.utils import Command
@@ -33,41 +32,34 @@ def tar_error(tmpdir):
return fixture
parametrize_extensions = pytest.mark.parametrize('ext', tar_extensions)
# (filename as typed by the user, unquoted filename, quoted filename as per shells.quote)
parametrize_filename = pytest.mark.parametrize('filename, unquoted, quoted', [
('foo{}', 'foo{}', 'foo{}'),
('foo\ bar{}', 'foo bar{}', "'foo bar{}'"),
('"foo bar{}"', 'foo bar{}', "'foo bar{}'")])
parametrize_filename = pytest.mark.parametrize('filename', [
'foo.tar',
'foo.tar.gz',
'foo.tgz'])
parametrize_script = pytest.mark.parametrize('script, fixed', [
('tar xvf {}', 'mkdir -p {dir} && tar xvf {filename} -C {dir}'),
('tar -xvf {}', 'mkdir -p {dir} && tar -xvf {filename} -C {dir}'),
('tar --extract -f {}', 'mkdir -p {dir} && tar --extract -f {filename} -C {dir}')])
('tar xvf {}', 'mkdir -p foo && tar xvf {} -C foo'),
('tar -xvf {}', 'mkdir -p foo && tar -xvf {} -C foo'),
('tar --extract -f {}', 'mkdir -p foo && tar --extract -f {} -C foo')])
@parametrize_extensions
@parametrize_filename
@parametrize_script
def test_match(ext, tar_error, filename, unquoted, quoted, script, fixed):
tar_error(unquoted.format(ext))
assert match(Command(script=script.format(filename.format(ext))))
def test_match(tar_error, filename, script, fixed):
tar_error(filename)
assert match(Command(script=script.format(filename)))
@parametrize_extensions
@parametrize_filename
@parametrize_script
def test_side_effect(ext, tar_error, filename, unquoted, quoted, script, fixed):
tar_error(unquoted.format(ext))
side_effect(Command(script=script.format(filename.format(ext))), None)
assert set(os.listdir('.')) == {unquoted.format(ext), 'd'}
def test_side_effect(tar_error, filename, script, fixed):
tar_error(filename)
side_effect(Command(script=script.format(filename)), None)
assert set(os.listdir('.')) == {filename, 'd'}
@parametrize_extensions
@parametrize_filename
@parametrize_script
def test_get_new_command(ext, tar_error, filename, unquoted, quoted, script, fixed):
tar_error(unquoted.format(ext))
assert (get_new_command(Command(script=script.format(filename.format(ext))))
== fixed.format(dir=quoted.format(''), filename=filename.format(ext)))
def test_get_new_command(tar_error, filename, script, fixed):
tar_error(filename)
assert get_new_command(Command(script=script.format(filename))) == fixed.format(filename)

View File

@@ -1,72 +1,48 @@
# -*- 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):
def zip_error_inner(filename):
path = os.path.join(str(tmpdir), filename)
path = os.path.join(str(tmpdir), 'foo.zip')
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)
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
assert set(os.listdir('.')) == {'foo.zip', 'a', 'b', 'c', 'd'}
assert set(os.listdir('./d')) == {'e'}
@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)
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_match(zip_error, script):
assert match(Command(script=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)
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_side_effect(zip_error, script):
side_effect(Command(script=script), None)
dir_list = os.listdir(u'.')
if filename not in set(dir_list):
filename = normalize('NFD', filename)
assert set(dir_list) == {filename, 'd'}
assert set(os.listdir('.')) == {'foo.zip', 'd'}
@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)
@pytest.mark.parametrize('script,fixed', [
('unzip foo', 'unzip foo -d foo'),
('unzip foo.zip', 'unzip foo.zip -d foo')])
def test_get_new_command(zip_error, script, fixed):
assert get_new_command(Command(script=script)) == fixed

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest
import os
from thefuck.rules.fix_file import match, get_new_command
@@ -89,20 +87,6 @@ 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
@@ -243,7 +227,7 @@ def test_get_new_command_with_settings(mocker, monkeypatch, test, settings):
if test[3]:
assert (get_new_command(cmd) ==
u'dummy_editor {} +{}:{} && {}'.format(test[1], test[2], test[3], test[0]))
'dummy_editor {} +{}:{} && {}'.format(test[1], test[2], test[3], test[0]))
else:
assert (get_new_command(cmd) ==
u'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0]))
'dummy_editor {} +{} && {}'.format(test[1], test[2], test[0]))

View File

@@ -45,8 +45,8 @@ def test_not_match(command):
@pytest.mark.parametrize('command, output', [
(Command(script='git push', stderr=git_err), 'git push --force-with-lease'),
(Command(script='git push nvbn', stderr=git_err), 'git push --force-with-lease nvbn'),
(Command(script='git push nvbn master', stderr=git_err), 'git push --force-with-lease nvbn master')])
(Command(script='git push', stderr=git_err), 'git push --force'),
(Command(script='git push nvbn', stderr=git_err), 'git push --force nvbn'),
(Command(script='git push nvbn master', stderr=git_err), 'git push --force nvbn master')])
def test_get_new_command(command, output):
assert get_new_command(command) == output

View File

@@ -1,47 +0,0 @@
import pytest
from thefuck.rules.git_two_dashes import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr(meant):
return 'error: did you mean `%s` (with two dashes ?)' % meant
@pytest.mark.parametrize('command', [
Command(script='git add -patch', stderr=stderr('--patch')),
Command(script='git checkout -patch', stderr=stderr('--patch')),
Command(script='git commit -amend', stderr=stderr('--amend')),
Command(script='git push -tags', stderr=stderr('--tags')),
Command(script='git rebase -continue', stderr=stderr('--continue'))])
def test_match(command):
assert match(command)
@pytest.mark.parametrize('command', [
Command(script='git add --patch'),
Command(script='git checkout --patch'),
Command(script='git commit --amend'),
Command(script='git push --tags'),
Command(script='git rebase --continue')])
def test_not_match(command):
assert not match(command)
@pytest.mark.parametrize('command, output', [
(Command(script='git add -patch', stderr=stderr('--patch')),
'git add --patch'),
(Command(script='git checkout -patch', stderr=stderr('--patch')),
'git checkout --patch'),
(Command(script='git checkout -patch', stderr=stderr('--patch')),
'git checkout --patch'),
(Command(script='git init -bare', stderr=stderr('--bare')),
'git init --bare'),
(Command(script='git commit -amend', stderr=stderr('--amend')),
'git commit --amend'),
(Command(script='git push -tags', stderr=stderr('--tags')),
'git push --tags'),
(Command(script='git rebase -continue', stderr=stderr('--continue')),
'git rebase --continue')])
def test_get_new_command(command, output):
assert get_new_command(command) == output

View File

@@ -1,15 +1,12 @@
# -*- 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(u'grep café .')) == u'grep -r café .'
assert get_new_command(
Command('grep blah .')) == 'grep -r blah .'

View File

@@ -1,18 +1,17 @@
from mock import patch
from mock import Mock, patch
from thefuck.rules.has_exists_script import match, get_new_command
from ..utils import Command
def test_match():
with patch('os.path.exists', return_value=True):
assert match(Command(script='main', stderr='main: command not found'))
assert match(Command(script='main --help',
assert match(Mock(script='main', stderr='main: command not found'))
assert match(Mock(script='main --help',
stderr='main: command not found'))
assert not match(Command(script='main', stderr=''))
assert not match(Mock(script='main', stderr=''))
with patch('os.path.exists', return_value=False):
assert not match(Command(script='main', stderr='main: command not found'))
assert not match(Mock(script='main', stderr='main: command not found'))
def test_get_new_command():
assert get_new_command(Command(script='main --help')) == './main --help'
assert get_new_command(Mock(script='main --help')) == './main --help'

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,8 +17,7 @@ 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)
@@ -26,7 +25,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,5 +1,6 @@
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,13 +19,11 @@ 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"'),
('mkdir && touch a', 'sudo sh -c "mkdir && touch a"')])
('echo "a" >> b', 'sudo sh -c "echo \\"a\\" >> b"')])
def test_get_new_command(before, after):
assert get_new_command(Command(before)) == after

View File

@@ -1,3 +1,4 @@
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,3 +32,4 @@ 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,3 +32,4 @@ 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,4 +1,5 @@
import pytest
from mock import Mock
from thefuck.specific.sudo import sudo_support
from tests.utils import Command

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest
from pathlib import PosixPath
from thefuck import corrector, conf
@@ -55,9 +53,7 @@ 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,4 +1,5 @@
import pytest
from mock import Mock
from thefuck import logs

View File

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import pytest
from thefuck import shells
@@ -20,7 +18,7 @@ def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__\
.return_value.readlines.return_value = lines
.return_value.__iter__.return_value = lines
return aux
@@ -37,7 +35,6 @@ 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):
@@ -51,7 +48,6 @@ 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'])
@@ -59,10 +55,6 @@ 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):
@@ -91,13 +83,10 @@ class TestBash(object):
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
@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)
def test_put_to_history(self, builtins_open, shell):
shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)
write.assert_called_once_with('ls\n')
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
@@ -113,7 +102,6 @@ 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'])
@@ -164,15 +152,12 @@ class TestFish(object):
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
@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):
def test_put_to_history(self, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shell.put_to_history(entry)
shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)
write.assert_called_once_with('- cmd: ls\n when: 1430707243\n')
def test_and_(self, shell):
assert shell.and_('foo', 'bar') == 'foo; and bar'
@@ -194,12 +179,6 @@ 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')
@@ -228,15 +207,12 @@ class TestZsh(object):
def test_to_shell(self, shell):
assert shell.to_shell('pwd') == 'pwd'
@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):
def test_put_to_history(self, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shell.put_to_history(entry)
shell.put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)
write.assert_called_once_with(': 1430707243:0;ls\n')
def test_and_(self, shell):
assert shell.and_('ls', 'cd') == 'ls && cd'
@@ -253,7 +229,6 @@ 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,5 +1,3 @@
# -*- coding: utf-8 -*-
from subprocess import PIPE
from mock import Mock
from pathlib import Path
@@ -21,12 +19,6 @@ 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):
@@ -130,3 +122,4 @@ class TestCommand(object):
else:
with pytest.raises(EmptyCommand):
Command.from_raw_script(script)

View File

@@ -4,32 +4,37 @@ import pytest
from itertools import islice
from thefuck import ui
from thefuck.types import CorrectedCommand
from thefuck import const
@pytest.fixture
def patch_get_key(monkeypatch):
def patch_getch(monkeypatch):
def patch(vals):
vals = iter(vals)
monkeypatch.setattr('thefuck.ui.get_key', lambda: next(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))
return patch
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])
def test_read_actions(patch_getch):
patch_getch([ # Enter:
'\n',
# Enter:
'\r',
# Ignored:
'x', 'y',
# Up:
'\x1b', '[', 'A',
# Down:
'\x1b', '[', 'B',
# Ctrl+C:
KeyboardInterrupt], )
assert list(islice(ui.read_actions(), 5)) \
== [ui.SELECT, ui.SELECT, ui.PREVIOUS, ui.NEXT, ui.ABORT]
@@ -75,25 +80,25 @@ class TestSelectCommand(object):
== commands_with_side_effect[0]
assert capsys.readouterr() == ('', 'ls (+side effect)\n')
def test_with_confirmation(self, capsys, patch_get_key, commands):
patch_get_key(['\n'])
def test_with_confirmation(self, capsys, patch_getch, commands):
patch_getch(['\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_get_key, commands):
patch_get_key([const.KEY_CTRL_C])
def test_with_confirmation_abort(self, capsys, patch_getch, commands):
patch_getch([KeyboardInterrupt])
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_get_key,
def test_with_confirmation_with_side_effct(self, capsys, patch_getch,
commands_with_side_effect):
patch_get_key(['\n'])
assert ui.select_command(iter(commands_with_side_effect)) \
patch_getch(['\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_get_key, commands):
patch_get_key([const.KEY_DOWN, '\n'])
def test_with_confirmation_select_second(self, capsys, patch_getch, commands):
patch_getch(['\x1b', '[', 'B', '\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,8 +16,6 @@ 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',
@@ -25,12 +23,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_HISTORY_LIMIT': 'history_limit',
'THEFUCK_ALTER_HISTORY': 'alter_history'}
'THEFUCK_DEBUG': 'debug'}
SETTINGS_HEADER = u"""# The Fuck settings file
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
#
# The rules are defined as in the example bellow:
#
@@ -75,21 +71,9 @@ class Settings(dict):
for setting in DEFAULT_SETTINGS.items():
settings_file.write(u'# {} = {}\n'.format(*setting))
def _get_user_dir_path(self):
# for backward compatibility, use `~/.thefuck` if it exists
legacy_user_dir = Path(os.path.expanduser('~/.thefuck'))
if legacy_user_dir.is_dir():
return legacy_user_dir
else:
default_xdg_config_dir = os.path.expanduser("~/.config")
xdg_config_dir = os.getenv("XDG_CONFIG_HOME", default_xdg_config_dir)
return Path(os.path.join(xdg_config_dir, 'thefuck'))
def _setup_user_dir(self):
"""Returns user config dir, create it when it doesn't exist."""
user_dir = self._get_user_dir_path()
user_dir = Path(os.path.expanduser('~/.thefuck'))
rules_dir = user_dir.joinpath('rules')
if not rules_dir.is_dir():
rules_dir.mkdir(parents=True)
@@ -128,11 +112,8 @@ 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',
'alter_history'):
elif attr in ('require_confirmation', 'no_colors', 'debug'):
return val.lower() == 'true'
elif attr == 'history_limit':
return int(val)
else:
return val

View File

@@ -1,14 +0,0 @@
# -*- 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(u'{}'.format(cmd) for cmd in [first_command] + sorted_commands)))
', '.join(str(cmd) for cmd in [first_command] + sorted_commands)))
for command in sorted_commands:
yield command

View File

@@ -28,29 +28,29 @@ def exception(title, exc_info):
def rule_failed(rule, exc_info):
exception(u'Rule {}'.format(rule.name), exc_info)
exception('Rule {}'.format(rule.name), exc_info)
def failed(msg):
sys.stderr.write(u'{red}{msg}{reset}\n'.format(
sys.stderr.write('{red}{msg}{reset}\n'.format(
msg=msg,
red=color(colorama.Fore.RED),
reset=color(colorama.Style.RESET_ALL)))
def show_corrected_command(corrected_command):
sys.stderr.write(u'{bold}{script}{reset}{side_effect}\n'.format(
sys.stderr.write('{bold}{script}{reset}{side_effect}\n'.format(
script=corrected_command.script,
side_effect=u' (+side effect)' if corrected_command.side_effect else u'',
side_effect=' (+side effect)' if corrected_command.side_effect else '',
bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL)))
def confirm_text(corrected_command):
sys.stderr.write(
(u'{clear}{bold}{script}{reset}{side_effect} '
u'[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
u'/{red}ctrl+c{reset}]').format(
('{clear}{bold}{script}{reset}{side_effect} '
'[{green}enter{reset}/{blue}{reset}/{blue}{reset}'
'/{red}ctrl+c{reset}]').format(
script=corrected_command.script,
side_effect=' (+side effect)' if corrected_command.side_effect else '',
clear='\033[1K\r',

View File

@@ -1,12 +1,8 @@
# 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
@@ -17,6 +13,7 @@ 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)))
@@ -54,6 +51,7 @@ 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

@@ -1,53 +0,0 @@
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

@@ -2,14 +2,14 @@ import re
from thefuck.utils import replace_argument, for_app
@for_app('cargo', at_least=1)
@for_app('cargo')
def match(command):
return ('No such subcommand' in command.stderr
and 'Did you mean' in command.stderr)
def get_new_command(command):
broken = command.script_parts[1]
broken = command.script.split()[1]
fix = re.findall(r'Did you mean `([^`]*)`', command.stderr)[0]
return replace_argument(command.script, broken, fix)

View File

@@ -1,11 +1,11 @@
"""Attempts to spellcheck and correct failed cd commands"""
import os
import six
from difflib import get_close_matches
import re
from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir
from thefuck.utils import for_app
from thefuck import shells
__author__ = "mmussomele"
@@ -34,25 +34,24 @@ def get_new_command(command):
defaults to the rules of cd_mkdir.
Change sensitivity by changing MAX_ALLOWED_DIFF. Default value is 0.6
"""
dest = command.script_parts[1].split(os.sep)
dest = command.script.split()[1].split(os.sep)
if dest[-1] == '':
dest = dest[:-1]
if six.PY2:
cwd = os.getcwdu()
else:
cwd = os.getcwd()
cwd = os.getcwd()
for directory in dest:
if directory == ".":
continue
elif directory == "..":
cwd = os.path.split(cwd)[0]
continue
best_matches = get_close_matches(directory, _get_sub_dirs(cwd), cutoff=MAX_ALLOWED_DIFF)
best_matches = get_close_matches(
directory, _get_sub_dirs(cwd), cutoff=MAX_ALLOWED_DIFF)
if best_matches:
cwd = os.path.join(cwd, best_matches[0])
else:
return cd_mkdir.get_new_command(command)
return u'cd "{0}"'.format(cwd)
repl = shells.and_('mkdir -p \\1', 'cd \\1')
return re.sub(r'^cd (.*)', repl, command.script)
return 'cd "{0}"'.format(cwd)
enabled_by_default = True

View File

@@ -1,18 +0,0 @@
import re
from thefuck import shells
from thefuck.utils import for_app
from thefuck.specific.sudo import sudo_support
@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 'the system cannot find the path specified.' in command.stderr.lower()))
@sudo_support
def get_new_command(command):
repl = shells.and_('mkdir -p \\1', 'cd \\1')
return re.sub(r'^cd (.*)', repl, command.script)

View File

@@ -1,7 +1,7 @@
from thefuck.utils import for_app
@for_app('g++', 'clang++')
@for_app(['g++', 'clang++'])
def match(command):
return ('This file requires compiler and library support for the '
'ISO C++ 2011 standard.' in command.stderr or

View File

@@ -4,11 +4,6 @@ from thefuck import shells
from thefuck.utils import for_app
tar_extensions = ('.tar', '.tar.Z', '.tar.bz2', '.tar.gz', '.tar.lz',
'.tar.lzma', '.tar.xz', '.taz', '.tb2', '.tbz', '.tbz2',
'.tgz', '.tlz', '.txz', '.tz')
def _is_tar_extract(cmd):
if '--extract' in cmd:
return True
@@ -19,7 +14,11 @@ def _is_tar_extract(cmd):
def _tar_file(cmd):
for c in cmd:
tar_extensions = ('.tar', '.tar.Z', '.tar.bz2', '.tar.gz', '.tar.lz',
'.tar.lzma', '.tar.xz', '.taz', '.tb2', '.tbz', '.tbz2',
'.tgz', '.tlz', '.txz', '.tz')
for c in cmd.split():
for ext in tar_extensions:
if c.endswith(ext):
return (c, c[0:len(c) - len(ext)])
@@ -29,17 +28,16 @@ def _tar_file(cmd):
def match(command):
return ('-C' not in command.script
and _is_tar_extract(command.script)
and _tar_file(command.script_parts) is not None)
and _tar_file(command.script) is not None)
def get_new_command(command):
dir = shells.quote(_tar_file(command.script_parts)[1])
return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \
.format(dir=dir, cmd=command.script)
.format(dir=_tar_file(command.script)[1], cmd=command.script)
def side_effect(old_cmd, command):
with tarfile.TarFile(_tar_file(old_cmd.script_parts)[0]) as archive:
with tarfile.TarFile(_tar_file(old_cmd.script)[0]) as archive:
for file in archive.getnames():
try:
os.remove(file)

View File

@@ -1,15 +1,11 @@
import os
import zipfile
from thefuck.utils import for_app
from thefuck.shells import quote
def _is_bad_zip(file):
try:
with zipfile.ZipFile(file, 'r') as archive:
return len(archive.namelist()) > 1
except:
return False
with zipfile.ZipFile(file, 'r') as archive:
return len(archive.namelist()) > 1
def _zip_file(command):
@@ -17,28 +13,22 @@ def _zip_file(command):
# unzip [-flags] file[.zip] [file(s) ...] [-x file(s) ...]
# ^ ^ files to unzip from the archive
# archive to unzip
for c in command.script_parts[1:]:
for c in command.script.split()[1:]:
if not c.startswith('-'):
if c.endswith('.zip'):
return c
else:
return u'{}.zip'.format(c)
return '{}.zip'.format(c)
@for_app('unzip')
def match(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
return ('-d' not in command.script
and _is_bad_zip(_zip_file(command)))
def get_new_command(command):
return u'{} -d {}'.format(command.script, quote(_zip_file(command)[:-4]))
return '{} -d {}'.format(command.script, _zip_file(command)[:-4])
def side_effect(old_cmd, command):

View File

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

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': u'{editor} {file} +{line}',
@default_settings({'fixlinecmd': '{editor} {file} +{line}',
'fixcolcmd': None})
def get_new_command(command):
m = _search(command.stderr) or _search(command.stdout)

View File

@@ -5,8 +5,7 @@ from thefuck.specific.git import git_support
@git_support
def match(command):
# catches "git branch list" in place of "git branch"
return (command.script_parts
and command.script_parts[1:] == 'branch list'.split())
return command.script.split()[1:] == 'branch list'.split()
@git_support

View File

@@ -5,8 +5,9 @@ from thefuck.specific.git import git_support
@git_support
def match(command):
if command.script_parts and len(command.script_parts) > 1:
return (command.script_parts[1] == 'stash'
splited_script = command.script.split()
if len(splited_script) > 1:
return (splited_script[1] == 'stash'
and 'usage:' in command.stderr)
else:
return False
@@ -25,12 +26,12 @@ stash_commands = (
@git_support
def get_new_command(command):
stash_cmd = command.script_parts[2]
stash_cmd = command.script.split()[2]
fixed = utils.get_closest(stash_cmd, stash_commands, fallback_to_first=False)
if fixed is not None:
return replace_argument(command.script, stash_cmd, fixed)
else:
cmd = command.script_parts[:]
cmd = command.script.split()
cmd.insert(2, 'save')
return ' '.join(cmd)

View File

@@ -12,7 +12,7 @@ def match(command):
@git_support
def get_new_command(command):
return replace_argument(command.script, 'push', 'push --force-with-lease')
return replace_argument(command.script, 'push', 'push --force')
enabled_by_default = False

View File

@@ -1,14 +0,0 @@
from thefuck.utils import replace_argument
from thefuck.specific.git import git_support
@git_support
def match(command):
return ('error: did you mean `' in command.stderr
and '` (with two dashes ?)' in command.stderr)
@git_support
def get_new_command(command):
to = command.stderr.split('`')[1]
return replace_argument(command.script, to[1:], to)

View File

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

View File

@@ -4,7 +4,7 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
return command.script_parts and os.path.exists(command.script_parts[0]) \
return os.path.exists(command.script.split()[0]) \
and 'command not found' in command.stderr

View File

@@ -3,10 +3,10 @@ from thefuck.utils import for_app
@for_app('ls')
def match(command):
return command.script_parts and 'ls -' not in command.script
return 'ls -' not in command.script
def get_new_command(command):
command = command.script_parts[:]
command = command.script.split(' ')
command[0] = 'ls -lah'
return ' '.join(command)

View File

@@ -1,9 +1,5 @@
from thefuck.utils import for_app
@for_app('man', at_least=1)
def match(command):
return True
return command.script.strip().startswith('man ')
def get_new_command(command):
@@ -12,7 +8,7 @@ def get_new_command(command):
if '2' in command.script:
return command.script.replace("2", "3")
split_cmd2 = command.script_parts
split_cmd2 = command.script.split()
split_cmd3 = split_cmd2[:]
split_cmd2.insert(1, ' 2 ')

View File

@@ -21,7 +21,7 @@ def match(command):
def get_new_command(command):
script = command.script_parts[:]
script = command.script.split(' ')
possibilities = extract_possibilities(command)
script[1] = get_closest(script[1], possibilities)
return ' '.join(script)

View File

@@ -5,17 +5,16 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
return (command.script_parts
and 'not found' in command.stderr
and bool(get_close_matches(command.script_parts[0],
get_all_executables())))
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
get_all_executables()))
@sudo_support
def get_new_command(command):
old_command = command.script_parts[0]
old_command = command.script.split(' ')[0]
new_cmds = get_close_matches(old_command, get_all_executables(), cutoff=0.1)
return [' '.join([new_command] + command.script_parts[1:])
return [' '.join([new_command] + command.script.split(' ')[1:])
for new_command in new_cmds]

View File

@@ -11,14 +11,12 @@ from thefuck.specific.archlinux import get_pkgfile, archlinux_env
def match(command):
return (command.script_parts
and (command.script_parts[0] in ('pacman', 'yaourt')
or command.script_parts[0:2] == ['sudo', 'pacman'])
return (command.script.startswith(('pacman', 'sudo pacman', 'yaourt'))
and 'error: target not found:' in command.stderr)
def get_new_command(command):
pgr = command.script_parts[-1]
pgr = command.script.split()[-1]
return replace_command(command, pgr, get_pkgfile(pgr))

View File

@@ -6,8 +6,8 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
toks = command.script_parts
return (toks
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))

View File

@@ -5,8 +5,7 @@ enabled_by_default = False
@sudo_support
def match(command):
return (command.script_parts
and {'rm', '/'}.issubset(command.script_parts)
return ({'rm', '/'}.issubset(command.script.split())
and '--no-preserve-root' not in command.script
and '--no-preserve-root' in command.stderr)

View File

@@ -1,6 +1,5 @@
import shlex
from thefuck.shells import quote
from thefuck.utils import for_app
from thefuck.utils import quote, for_app
@for_app('sed')

View File

@@ -16,14 +16,10 @@ patterns = ['permission denied',
'need root',
'only root can ',
'You don\'t have access to the history DB.',
'authentication is required',
'eDSPermissionError']
'authentication is required']
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():
@@ -32,9 +28,7 @@ def match(command):
def get_new_command(command):
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:
if '>' in command.script:
return u'sudo sh -c "{}"'.format(command.script.replace('"', '\\"'))
else:
return u'sudo {}'.format(command.script)

View File

@@ -11,11 +11,9 @@ source_layouts = [u'''йцукенгшщзхъфывапролджэячсмит
@memoize
def _get_matched_layout(command):
# don't use command.split_script here because a layout mismatch will likely
# result in a non-splitable sript as per shlex
cmd = command.script.split(' ')
for source_layout in source_layouts:
if all([ch in source_layout or ch in '-_' for ch in cmd[0]]):
if all([ch in source_layout or ch in '-_'
for ch in command.script.split(' ')[0]]):
return source_layout

View File

@@ -8,15 +8,15 @@ from thefuck.utils import for_app
@sudo_support
@for_app('systemctl')
def match(command):
# Catches "Unknown operation 'service'." when executing systemctl with
# Catches 'Unknown operation 'service'.' when executing systemctl with
# misordered arguments
cmd = command.script_parts
return (cmd and 'Unknown operation \'' in command.stderr and
cmd = command.script.split()
return ('Unknown operation \'' in command.stderr and
len(cmd) - cmd.index('systemctl') == 3)
@sudo_support
def get_new_command(command):
cmd = command.script_parts
cmd = command.script.split()
cmd[-1], cmd[-2] = cmd[-2], cmd[-1]
return ' '.join(cmd)

View File

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

View File

@@ -1,6 +1,6 @@
import re
from thefuck import shells
from thefuck.utils import for_app
from thefuck.shells import and_
@for_app('touch')
@@ -10,4 +10,4 @@ def match(command):
def get_new_command(command):
path = re.findall(r"touch: cannot touch '(.+)/.+':", command.stderr)[0]
return shells.and_(u'mkdir -p {}'.format(path), command.script)
return and_(u'mkdir -p {}'.format(path), command.script)

View File

@@ -1,7 +1,6 @@
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)
@@ -11,3 +10,4 @@ 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

@@ -8,13 +8,13 @@ def match(command):
def get_new_command(command):
cmds = command.script_parts
cmds = command.script.split(' ')
machine = None
if len(cmds) >= 3:
machine = cmds[2]
startAllInstances = shells.and_("vagrant up", command.script)
if machine is None:
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

@@ -1,9 +1,7 @@
# -*- encoding: utf-8 -*-
from six.moves.urllib.parse import urlparse
from thefuck.utils import for_app
@for_app('whois', at_least=1)
def match(command):
"""
What the `whois` command returns depends on the 'Whois server' it contacted
@@ -21,11 +19,11 @@ def match(command):
- www.google.fr → subdomain: www, domain: 'google.fr';
- google.co.uk → subdomain: None, domain; 'google.co.uk'.
"""
return True
return 'whois ' in command.script.strip()
def get_new_command(command):
url = command.script_parts[1]
url = command.script.split()[1]
if '/' in command.script:
return 'whois ' + urlparse(url).netloc

View File

@@ -1,6 +1,6 @@
"""Module with shell specific actions, each shell class should
implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and
`get_aliases` methods.
implement `from_shell`, `to_shell`, `app_alias`, `put_to_history` and `get_aliases`
methods.
"""
from collections import defaultdict
@@ -9,15 +9,11 @@ from subprocess import Popen, PIPE
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 {}
@@ -38,8 +34,7 @@ class Generic(object):
return command_script
def app_alias(self, fuck):
return "alias {0}='TF_ALIAS={0} PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1))'".format(fuck)
return "alias {0}='TF_ALIAS={0} eval $(thefuck $(fc -ln -1))'".format(fuck)
def _get_history_file_name(self):
return ''
@@ -52,26 +47,25 @@ 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:
entry = self._get_history_line(command_script)
if six.PY2:
history.write(entry.encode('utf-8'))
else:
history.write(entry)
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 ''
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_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()
encoding='utf-8', errors='ignore') as history:
for line in history:
prepared = self._script_from_history(line)\
.strip()
if prepared:
yield prepared
@@ -81,30 +75,10 @@ class Generic(object):
def how_to_configure(self):
return
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):
"""Return a shell-escaped version of the string s."""
if six.PY2:
from pipes import quote
else:
from shlex import quote
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}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1));" \
return "TF_ALIAS={0} alias {0}='eval $(thefuck $(fc -ln -1));" \
" history -r'".format(fuck)
def _parse_alias(self, alias):
@@ -118,9 +92,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",
@@ -129,6 +103,9 @@ 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'
@@ -140,6 +117,7 @@ class Bash(Generic):
class Fish(Generic):
def _get_overridden_aliases(self):
overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip()
if overridden_aliases:
@@ -148,20 +126,19 @@ class Fish(Generic):
return ['cd', 'grep', 'ls', 'man', 'open']
def app_alias(self, fuck):
return ('function {0} -d "Correct your previous console command"\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'
' end\n'
'end').format(fuck)
return ("set TF_ALIAS {0}\n"
"function {0} -d 'Correct your previous console command'\n"
" set -l exit_code $status\n"
" set -l eval_script"
" (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n"
" set -l fucked_up_command $history[1]\n"
" thefuck $fucked_up_command > $eval_script\n"
" . $eval_script\n"
" /bin/rm $eval_script\n"
" if test $exit_code -ne 0\n"
" history --delete $fucked_up_command\n"
" end\n"
"end").format(fuck)
@memoize
def get_aliases(self):
@@ -188,12 +165,6 @@ 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)
@@ -204,8 +175,7 @@ class Fish(Generic):
class Zsh(Generic):
def app_alias(self, fuck):
return "TF_ALIAS={0}" \
" alias {0}='PYTHONIOENCODING=utf-8 " \
"eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" alias {0}='eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" fc -R'".format(fuck)
def _parse_alias(self, alias):
@@ -219,9 +189,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",
@@ -254,9 +224,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",
@@ -303,10 +273,7 @@ def thefuck_alias():
def put_to_history(command):
try:
return _get_shell().put_to_history(command)
except IOError:
logs.exception("Can't update history", sys.exc_info())
return _get_shell().put_to_history(command)
def and_(*commands):
@@ -317,18 +284,9 @@ def get_aliases():
return list(_get_shell().get_aliases().keys())
def split_command(command):
return _get_shell().split_command(command)
def quote(s):
return _get_shell().quote(s)
@memoize
def get_history():
return list(_get_shell().get_history())
def how_to_configure():
return _get_shell().how_to_configure()

View File

@@ -1,7 +1,8 @@
import re
from shlex import split
from decorator import decorator
from ..utils import is_app
from ..shells import quote, split_command
from ..types import Command
from ..utils import quote, is_app
@decorator
@@ -23,7 +24,7 @@ def git_support(fn, command):
# 'commit' '--amend'
# which is surprising and does not allow to easily test for
# eg. 'git commit'
expansion = ' '.join(map(quote, split_command(search.group(2))))
expansion = ' '.join(map(quote, split(search.group(2))))
new_script = command.script.replace(alias, expansion)
command = command.update(script=new_script)

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
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

View File

@@ -1,25 +0,0 @@
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

@@ -1,13 +1,13 @@
from imp import load_source
from subprocess import Popen, PIPE
import os
from subprocess import Popen, PIPE
import sys
import six
from psutil import Process, TimeoutExpired
from . import logs, shells
import six
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from .exceptions import EmptyCommand
from .utils import compatibility_call
from .exceptions import EmptyCommand
from . import logs, shells
class Command(object):
@@ -25,27 +25,16 @@ class Command(object):
self.stdout = stdout
self.stderr = stderr
@property
def script_parts(self):
if not hasattr(self, '_script_parts'):
try:
self._script_parts = shells.split_command(self.script)
except Exception:
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 u'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr)
return 'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr)
def update(self, **kwargs):
"""Returns new command with replaced fields.
@@ -166,9 +155,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 +256,9 @@ class CorrectedCommand(object):
return (self.script, self.side_effect).__hash__()
def __repr__(self):
return u'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority)
return '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,7 +267,5 @@ class CorrectedCommand(object):
"""
if self.side_effect:
compatibility_call(self.side_effect, old_cmd, self.script)
if settings.alter_history:
shells.put_to_history(self.script)
# This depends on correct setting of PYTHONIOENCODING by the alias:
shells.put_to_history(self.script)
print(self.script)

View File

@@ -3,8 +3,25 @@
import sys
from .conf import settings
from .exceptions import NoRuleMatched
from .system import get_key
from . import logs, const
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)
SELECT = 0
ABORT = 1
@@ -14,18 +31,24 @@ NEXT = 3
def read_actions():
"""Yields actions for pressed keys."""
buffer = []
while True:
key = get_key()
if key in (const.KEY_UP, 'k'):
yield PREVIOUS
elif key in (const.KEY_DOWN, 'j'):
yield NEXT
elif key == const.KEY_CTRL_C:
try:
ch = getch()
except KeyboardInterrupt: # Ctrl+C
yield ABORT
elif key in ('\n', '\r'):
if ch in ('\n', '\r'): # Enter
yield SELECT
buffer.append(ch)
buffer = buffer[-3:]
if buffer == ['\x1b', '[', 'A'] or ch == 'k': # ↑
yield PREVIOUS
elif buffer == ['\x1b', '[', 'B'] or ch == 'j': # ↓
yield NEXT
class CommandSelector(object):
"""Helper for selecting rule from rules list."""

View File

@@ -1,20 +1,27 @@
import dbm
import os
import pickle
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
import shelve
from warnings import warn
from decorator import decorator
from contextlib import closing
import os
import pickle
import re
from inspect import getargspec
from pathlib import Path
import pkg_resources
import six
from .conf import settings
DEVNULL = open(os.devnull, 'w')
if six.PY2:
from pipes import quote
else:
from shlex import quote
def memoize(fn):
"""Caches previous calls to the function."""
@@ -22,16 +29,11 @@ def memoize(fn):
@wraps(fn)
def wrapper(*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)
key = pickle.dumps((args, kwargs))
if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs)
return value
return memo[key]
return wrapper
memoize.disabled = False
@@ -110,7 +112,7 @@ def get_all_executables():
def replace_argument(script, from_, to):
"""Replaces command line argument."""
replaced_in_the_end = re.sub(u' {}$'.format(re.escape(from_)), u' {}'.format(to),
replaced_in_the_end = re.sub(u' {}$'.format(from_), u' {}'.format(to),
script, count=1)
if replaced_in_the_end != script:
return replaced_in_the_end
@@ -142,23 +144,19 @@ def replace_command(command, broken, matched):
@memoize
def is_app(command, *app_names, **kwargs):
def is_app(command, *app_names):
"""Returns `True` if command is call to one of passed app names."""
at_least = kwargs.pop('at_least', 0)
if kwargs:
raise TypeError("got an unexpected keyword argument '{}'".format(kwargs.keys()))
if command.script_parts is not None and len(command.script_parts) > at_least:
return command.script_parts[0] in app_names
for name in app_names:
if command.script == name \
or command.script.startswith(u'{} '.format(name)):
return True
return False
def for_app(*app_names, **kwargs):
def for_app(*app_names):
"""Specifies that matching script is for on of app names."""
def _for_app(fn, command):
if is_app(command, *app_names, **kwargs):
if is_app(command, *app_names):
return fn(command)
else:
return False
@@ -182,51 +180,25 @@ def cache(*depends_on):
except OSError:
return '0'
def _get_cache_path():
default_xdg_cache_dir = os.path.expanduser("~/.cache")
cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir)
cache_path = Path(cache_dir).joinpath('thefuck').as_posix()
# Ensure the cache_path exists, Python 2 does not have the exist_ok
# parameter
try:
os.makedirs(cache_dir)
except OSError:
if not os.path.isdir(cache_dir):
raise
return cache_path
@decorator
def _cache(fn, *args, **kwargs):
if cache.disabled:
return fn(*args, **kwargs)
cache_path = settings.user_dir.joinpath('.thefuck-cache').as_posix()
# A bit obscure, but simplest way to generate unique key for
# functions and methods in python 2 and 3:
key = '{}.{}'.format(fn.__module__, repr(fn).split('at')[0])
etag = '.'.join(_get_mtime(name) for name in depends_on)
cache_path = _get_cache_path()
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:
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
return _cache
cache.disabled = False

View File

@@ -1,5 +1,5 @@
[tox]
envlist = py27,py33,py34,py35
envlist = py27,py33,py34
[testenv]
deps = -rrequirements.txt