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

Compare commits

..

32 Commits

Author SHA1 Message Date
nvbn
89f868c115 Bump to 3.2 2015-11-03 17:38:07 +08:00
nvbn
81f6a25abc #398: Fix UnicodeDecodeError in logs 2015-11-01 14:42:48 +08:00
nvbn
cc9af78787 Merge branch 'master' of github.com:nvbn/thefuck 2015-11-01 13:17:15 +08:00
nvbn
1fc3f1b5bf #398: Fix UnicodeDecodeError in logs 2015-11-01 13:16:58 +08:00
Vladimir Iakovlev
45574d06c9 Merge pull request #397 from janek-warchol/use-force-with-lease
Use --force-with-lease instead of --force for git push
2015-10-31 02:47:51 +08:00
Jan Warchoł
dc23d67a42 Use --force-with-lease instead of --force for git push
--force flag can be very dangerous, because it unconditionally
overwrites remote branch - if someone pushed new commits to the remote
repo after you last fetched/pulled, and you do push --force, you will
overwrite his commits without even knowing that you did that.  Using
--force-with-lease is much safer because it only overwrites remote
branch when it points to the same commit that you think it points to.

Read more:
https://developer.atlassian.com/blog/2015/04/force-with-lease/
2015-10-30 16:17:56 +01:00
nvbn
959b96cf6e #392: Show only debug message if script isn't splitable 2015-10-29 01:03:27 +08:00
nvbn
f20311fa89 #392: Little refactoring 2015-10-29 00:13:59 +08:00
nvbn
a4c391096a Merge branch 'fix-split' of https://github.com/mcarton/thefuck into mcarton-fix-split 2015-10-29 00:04:29 +08:00
mcarton
e71a3e0cdb Replace (almost) all instance of script.split 2015-10-28 16:43:24 +01:00
mcarton
2d995d464f Fix the cpp11 rule 2015-10-28 15:27:10 +01:00
mcarton
280751b36e Fix the unzip rules and filenames with spaces 2015-10-28 15:13:33 +01:00
mcarton
0a6a3db65d Fix the untar rules and filenames with spaces 2015-10-28 15:12:59 +01:00
mcarton
ecfc180280 Add shells.quote 2015-10-28 14:16:01 +01:00
mcarton
dae58211ba Parse command line with shlex
I put that in shells so that weird shells might try to parse it
differently.
2015-10-28 14:01:14 +01:00
Vladimir Iakovlev
5e9b2c56da Merge pull request #391 from mcarton/tox-3.5
#374 Test python 3.5 with tox
2015-10-28 20:52:45 +08:00
mcarton
192ab0bfb0 Test python 3.5 with tox 2015-10-28 13:32:37 +01:00
nvbn
346cb99217 #385 Little refactoring 2015-10-21 18:13:22 +08:00
nvbn
bbfedb861f Merge branch 'xdg' of https://github.com/mcarton/thefuck into mcarton-xdg 2015-10-21 18:03:39 +08:00
nvbn
f5b0e96747 #382 Prevent partial execution of install.sh 2015-10-21 18:00:08 +08:00
Vladimir Iakovlev
12a33f56bc Merge pull request #389 from scorphus/fix-touch
Fix `rules.touch` tests
2015-10-21 17:50:09 +08:00
Pablo Santiago Blum de Aguiar
590fdba2aa Fix rules.touch tests
Move them to `rules` sub-directory and import `shells` instead of `and_`
which in turn triggers the `generic_shell` fixture fixing the tests on
Fish Shell.
2015-10-18 19:49:46 -02:00
Vladimir Iakovlev
f374142bf8 Merge pull request #384 from scorphus/fish-func
Improve the Fish Shell function making it faster
2015-10-19 01:50:53 +08:00
Vladimir Iakovlev
540ff7e16d Merge pull request #387 from scorphus/git-two-dashes
Add `git_two_dashes` rule
2015-10-19 01:50:04 +08:00
Vladimir Iakovlev
806dad18bf Merge pull request #386 from mcarton/CONTRIBUTING
Add a CONTRIBUTING file
2015-10-19 01:49:02 +08:00
Vladimir Iakovlev
8b416f269f Merge pull request #388 from scorphus/fix-brew-tests
Fix rules.brew_install tests on Mac
2015-10-19 01:48:53 +08:00
Pablo Santiago Blum de Aguiar
5e44fb22be Fix rules.brew_install tests on Mac 2015-10-17 18:46:07 -03:00
Pablo Santiago Blum de Aguiar
5389d0c106 Add git_two_dashes rule 2015-10-17 18:40:53 -03:00
mcarton
c970f190d2 Add a CONTRIBUTING file 2015-10-17 15:45:13 +02:00
mcarton
8f25c95f06 Use XDG_CACHE_HOME for cache 2015-10-16 17:33:52 +02:00
mcarton
4a48108c69 Follow the XDG Base Directory Specification 2015-10-16 16:52:03 +02:00
Pablo Santiago Blum de Aguiar
f5e8fe954e Improve the Fish Shell function making it faster
It works now with no temp file involved, which makes it a lot faster.
Also, `history --merge`, although only supported on Fish Shell 2.2+,
merges the corrected entry back into history. Neat!

Ref #89
2015-10-16 00:42:34 -03:00
44 changed files with 378 additions and 184 deletions

25
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,25 @@
# 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, when it's not possible
creates directories before cd'ing into them;
* `cd_correction` – spellchecks and correct failed cd commands;
* `cd_mkdir` – 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,6 +167,7 @@ creates directories before cd'ing into them;
* `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;
@@ -218,13 +219,13 @@ Enabled by default only on specific platforms:
Bundled, but not enabled by default:
* `git_push_force` – adds `--force` to a `git push` (may conflict with `git_push_pull`);
* `git_push_force` – adds `--force-with-lease` 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 `~/.thefuck/rules`. The rule should contain two functions:
in `~/.config/thefuck/rules`. The rule should contain two functions:
```python
match(command: Command) -> bool
@@ -241,7 +242,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 `~/.thefuck/settings.py` and values from env ([see more below](#settings)).
`settings` is a special object filled with `~/.config/thefuck/settings.py` and values from env ([see more below](#settings)).
Simple example of the rule for running script with `sudo`:
@@ -271,7 +272,7 @@ requires_output = True
## Settings
The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`:
The Fuck has a few settings parameters which can be changed in `$XDG_CONFIG_HOME/thefuck/settings.py` (`$XDG_CONFIG_HOME` defaults to `~/.config`):
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `exclude_rules` – list of disabled rules, by default `[]`;

View File

@@ -8,50 +8,54 @@ installed () {
hash $1 2>/dev/null
}
# 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
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
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
if [[ -n $(apt-cache search python-commandnotfound) ]]; then
# In case of different python versions:
sudo apt-get install -yy python-commandnotfound
fi
else
# Genreic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py
if installed brew; then
# OS X:
brew update
brew install python
else
# Genreic way:
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
rm get-pip.py
fi
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
if should_add_alias ~/.tcshrc; then
echo 'eval `thefuck --alias`' >> ~/.tcshrc
fi
}
install_thefuck

View File

@@ -20,7 +20,7 @@ elif (3, 0) < version < (3, 3):
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '3.1'
VERSION = '3.2'
install_requires = ['psutil', 'colorama', 'six', 'decorator']
extras_require = {':python_version<"3.4"': ['pathlib']}

View File

@@ -31,8 +31,7 @@ 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),
None)
assert not match(Command('brew install', stderr=brew_install_no_argument))
@pytest.mark.skipif(_is_not_okay_to_test(),
@@ -43,5 +42,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),
None) != 'brew install aha'
stderr=brew_no_available_formula))\
!= 'brew install aha'

View File

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

View File

@@ -1,7 +1,8 @@
import os
import pytest
import tarfile
from thefuck.rules.dirty_untar import match, get_new_command, side_effect
from thefuck.rules.dirty_untar import match, get_new_command, side_effect, \
tar_extensions
from tests.utils import Command
@@ -32,34 +33,40 @@ def tar_error(tmpdir):
return fixture
parametrize_filename = pytest.mark.parametrize('filename', [
'foo.tar',
'foo.tar.gz',
'foo.tgz'])
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_script = pytest.mark.parametrize('script, fixed', [
('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')])
('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}')])
@parametrize_extensions
@parametrize_filename
@parametrize_script
def test_match(tar_error, filename, script, fixed):
tar_error(filename)
assert match(Command(script=script.format(filename)))
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))))
@parametrize_extensions
@parametrize_filename
@parametrize_script
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'}
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'}
@parametrize_extensions
@parametrize_filename
@parametrize_script
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)
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)))

View File

@@ -43,6 +43,8 @@ def test_side_effect(zip_error, script):
@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):
assert get_new_command(Command(script=script)) == fixed

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'),
(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')])
(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')])
def test_get_new_command(command, output):
assert get_new_command(command) == output

View File

@@ -0,0 +1,47 @@
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,17 +1,18 @@
from mock import Mock, patch
from mock import 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(Mock(script='main', stderr='main: command not found'))
assert match(Mock(script='main --help',
assert match(Command(script='main', stderr='main: command not found'))
assert match(Command(script='main --help',
stderr='main: command not found'))
assert not match(Mock(script='main', stderr=''))
assert not match(Command(script='main', stderr=''))
with patch('os.path.exists', return_value=False):
assert not match(Mock(script='main', stderr='main: command not found'))
assert not match(Command(script='main', stderr='main: command not found'))
def test_get_new_command():
assert get_new_command(Mock(script='main --help')) == './main --help'
assert get_new_command(Command(script='main --help')) == './main --help'

View File

@@ -26,7 +26,7 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_PRIORITY': 'priority',
'THEFUCK_DEBUG': 'debug'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
SETTINGS_HEADER = u"""# The Fuck settings file
#
# The rules are defined as in the example bellow:
#
@@ -71,9 +71,21 @@ 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 = Path(os.path.expanduser('~/.thefuck'))
user_dir = self._get_user_dir_path()
rules_dir = user_dir.joinpath('rules')
if not rules_dir.is_dir():
rules_dir.mkdir(parents=True)

View File

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

@@ -2,14 +2,14 @@ import re
from thefuck.utils import replace_argument, for_app
@for_app('cargo')
@for_app('cargo', at_least=1)
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.split()[1]
broken = command.script_parts[1]
fix = re.findall(r'Did you mean `([^`]*)`', command.stderr)[0]
return replace_argument(command.script, broken, fix)

View File

@@ -2,10 +2,9 @@
import os
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,7 +33,7 @@ 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.split()[1].split(os.sep)
dest = command.script_parts[1].split(os.sep)
if dest[-1] == '':
dest = dest[:-1]
cwd = os.getcwd()
@@ -44,13 +43,11 @@ def get_new_command(command):
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:
repl = shells.and_('mkdir -p \\1', 'cd \\1')
return re.sub(r'^cd (.*)', repl, command.script)
return cd_mkdir.get_new_command(command)
return 'cd "{0}"'.format(cwd)

17
thefuck/rules/cd_mkdir.py Normal file
View File

@@ -0,0 +1,17 @@
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()))
@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,6 +4,11 @@ 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
@@ -14,11 +19,8 @@ def _is_tar_extract(cmd):
def _tar_file(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 c in cmd:
for ext in tar_extensions:
if c.endswith(ext):
return (c, c[0:len(c) - len(ext)])
@@ -28,16 +30,17 @@ def _tar_file(cmd):
def match(command):
return ('-C' not in command.script
and _is_tar_extract(command.script)
and _tar_file(command.script) is not None)
and _tar_file(command.script_parts) 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=_tar_file(command.script)[1], cmd=command.script)
.format(dir=dir, cmd=command.script)
def side_effect(old_cmd, command):
with tarfile.TarFile(_tar_file(old_cmd.script)[0]) as archive:
with tarfile.TarFile(_tar_file(old_cmd.script_parts)[0]) as archive:
for file in archive.getnames():
try:
os.remove(file)

View File

@@ -1,6 +1,7 @@
import os
import zipfile
from thefuck.utils import for_app
from thefuck.shells import quote
def _is_bad_zip(file):
@@ -13,7 +14,7 @@ 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.split()[1:]:
for c in command.script_parts[1:]:
if not c.startswith('-'):
if c.endswith('.zip'):
return c
@@ -28,7 +29,7 @@ def match(command):
def get_new_command(command):
return '{} -d {}'.format(command.script, _zip_file(command)[:-4])
return '{} -d {}'.format(command.script, quote(_zip_file(command)[:-4]))
def side_effect(old_cmd, command):

View File

@@ -1,11 +1,13 @@
def match(command):
split_command = command.script.split()
split_command = command.script_parts
return len(split_command) >= 2 and split_command[0] == split_command[1]
return (split_command
and len(split_command) >= 2
and split_command[0] == split_command[1])
def get_new_command(command):
return command.script[command.script.find(' ')+1:]
return ' '.join(command.script_parts[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

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

View File

@@ -5,9 +5,8 @@ from thefuck.specific.git import git_support
@git_support
def match(command):
splited_script = command.script.split()
if len(splited_script) > 1:
return (splited_script[1] == 'stash'
if command.script_parts and len(command.script_parts) > 1:
return (command.script_parts[1] == 'stash'
and 'usage:' in command.stderr)
else:
return False
@@ -26,12 +25,12 @@ stash_commands = (
@git_support
def get_new_command(command):
stash_cmd = command.script.split()[2]
stash_cmd = command.script_parts[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.split()
cmd = command.script_parts[:]
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')
return replace_argument(command.script, 'push', 'push --force-with-lease')
enabled_by_default = False

View File

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

@@ -4,7 +4,7 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
return os.path.exists(command.script.split()[0]) \
return command.script_parts and os.path.exists(command.script_parts[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 'ls -' not in command.script
return command.script_parts and 'ls -' not in command.script
def get_new_command(command):
command = command.script.split(' ')
command = command.script_parts[:]
command[0] = 'ls -lah'
return ' '.join(command)

View File

@@ -1,5 +1,9 @@
from thefuck.utils import for_app
@for_app('man', at_least=1)
def match(command):
return command.script.strip().startswith('man ')
return True
def get_new_command(command):
@@ -8,7 +12,7 @@ def get_new_command(command):
if '2' in command.script:
return command.script.replace("2", "3")
split_cmd2 = command.script.split()
split_cmd2 = command.script_parts
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.split(' ')
script = command.script_parts[:]
possibilities = extract_possibilities(command)
script[1] = get_closest(script[1], possibilities)
return ' '.join(script)

View File

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

View File

@@ -11,12 +11,14 @@ from thefuck.specific.archlinux import get_pkgfile, archlinux_env
def match(command):
return (command.script.startswith(('pacman', 'sudo pacman', 'yaourt'))
return (command.script_parts
and (command.script_parts[0] in ('pacman', 'yaourt')
or command.script_parts[0:2] == ['sudo', 'pacman'])
and 'error: target not found:' in command.stderr)
def get_new_command(command):
pgr = command.script.split()[-1]
pgr = command.script_parts[-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.split()
return (len(toks) > 0
toks = command.script_parts
return (toks
and toks[0].endswith('.py')
and ('Permission denied' in command.stderr or
'command not found' in command.stderr))

View File

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

View File

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

View File

@@ -11,9 +11,11 @@ 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 command.script.split(' ')[0]]):
if all([ch in source_layout or ch in '-_' for ch in cmd[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.split()
return ('Unknown operation \'' in command.stderr and
cmd = command.script_parts
return (cmd and 'Unknown operation \'' in command.stderr and
len(cmd) - cmd.index('systemctl') == 3)
@sudo_support
def get_new_command(command):
cmd = command.script.split()
cmd = command.script_parts
cmd[-1], cmd[-2] = cmd[-2], cmd[-1]
return ' '.join(cmd)

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 and_(u'mkdir -p {}'.format(path), command.script)
return shells.and_(u'mkdir -p {}'.format(path), command.script)

View File

@@ -8,13 +8,13 @@ def match(command):
def get_new_command(command):
cmds = command.script.split(' ')
cmds = command.script_parts
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]

View File

@@ -1,7 +1,9 @@
# -*- 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
@@ -19,11 +21,11 @@ def match(command):
- www.google.fr → subdomain: www, domain: 'google.fr';
- google.co.uk → subdomain: None, domain; 'google.co.uk'.
"""
return 'whois ' in command.script.strip()
return True
def get_new_command(command):
url = command.script.split()[1]
url = command.script_parts[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,6 +9,8 @@ from subprocess import Popen, PIPE
from time import time
import io
import os
import shlex
import six
from .utils import DEVNULL, memoize, cache
@@ -75,6 +77,20 @@ class Generic(object):
def how_to_configure(self):
return
def split_command(self, command):
"""Split the command using shell-like syntax."""
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)
class Bash(Generic):
def app_alias(self, fuck):
@@ -126,19 +142,20 @@ class Fish(Generic):
return ['cd', 'grep', 'ls', 'man', 'open']
def app_alias(self, 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)
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'
' 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)
@memoize
def get_aliases(self):
@@ -284,9 +301,18 @@ 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,8 +1,7 @@
import re
from shlex import split
from decorator import decorator
from ..types import Command
from ..utils import quote, is_app
from ..utils import is_app
from ..shells import quote, split_command
@decorator
@@ -24,7 +23,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(search.group(2))))
expansion = ' '.join(map(quote, split_command(search.group(2))))
new_script = command.script.replace(alias, expansion)
command = command.update(script=new_script)

View File

@@ -1,13 +1,13 @@
from imp import load_source
import os
from subprocess import Popen, PIPE
import os
import sys
from psutil import Process, TimeoutExpired
import six
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from .utils import compatibility_call
from .exceptions import EmptyCommand
from psutil import Process, TimeoutExpired
from . import logs, shells
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from .exceptions import EmptyCommand
from .utils import compatibility_call
class Command(object):
@@ -25,6 +25,17 @@ 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("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) \

View File

@@ -12,16 +12,10 @@ 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."""
@@ -144,19 +138,23 @@ def replace_command(command, broken, matched):
@memoize
def is_app(command, *app_names):
def is_app(command, *app_names, **kwargs):
"""Returns `True` if command is call to one of passed app names."""
for name in app_names:
if command.script == name \
or command.script.startswith(u'{} '.format(name)):
return True
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
return False
def for_app(*app_names):
def for_app(*app_names, **kwargs):
"""Specifies that matching script is for on of app names."""
def _for_app(fn, command):
if is_app(command, *app_names):
if is_app(command, *app_names, **kwargs):
return fn(command)
else:
return False
@@ -180,17 +178,32 @@ 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()
with closing(shelve.open(cache_path)) as db:
if db.get(key, {}).get('etag') == etag:

View File

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