1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-01 23:51:59 +00:00

Compare commits

..

46 Commits
3.8 ... 3.10

Author SHA1 Message Date
nvbn
d5e333b727 Bump to 3.10 2016-06-28 03:30:27 +03:00
nvbn
1755bcd1b5 #524: Run functional tests only with python 3.5 2016-06-28 03:05:42 +03:00
nvbn
f773b57bea #511: Add ln_s_order rule 2016-06-28 03:00:00 +03:00
Vladimir Iakovlev
5866ea8433 Merge pull request #524 from nvbn/fix-func-tests-on-travis-ci
Fix functional tests on travis-ci
2016-06-28 00:38:58 +03:00
nvbn
729508e581 #524: Add commandnotfound to travis config 2016-06-28 00:38:38 +03:00
nvbn
1b7c8b5498 #524: Don't wait for prompt in zsh 2016-06-28 00:22:25 +03:00
nvbn
9b6cd0cd7b #524: Exit with 1 if no fixed command selected 2016-06-28 00:22:04 +03:00
nvbn
77ea630d84 #524: Remove skip_without_docker hacks 2016-06-28 00:11:56 +03:00
nvbn
917c6ac887 #524: Fix tcsh encoding 2016-06-28 00:00:49 +03:00
nvbn
8bea63eb23 #524: Remove unned dependencies from .travis.yml 2016-06-27 23:48:26 +03:00
nvbn
2bf21d9f0e #524: Wait for prompt in zsh tests 2016-06-27 23:48:06 +03:00
nvbn
e2f66cb26b #N/A: Enable docker service 2016-06-27 23:18:38 +03:00
Vladimir Iakovlev
ff1ee979f0 Merge pull request #518 from mklkj/master
Fix a typo
2016-06-27 23:15:41 +03:00
Vladimir Iakovlev
25343dbfb4 Merge pull request #523 from MattKotsenas/refactor/powershell-config
Add semi-colons in powershell alias
2016-06-27 23:15:14 +03:00
Vladimir Iakovlev
ea3671f98c Merge pull request #521 from b1101/master
ui: accept 'q' as quit character
2016-06-27 23:14:16 +03:00
nvbn
4584903beb #N/A: Temporary disable functional tests on travis-ci 2016-06-27 23:13:57 +03:00
nvbn
990ba57159 #N/A: Disable zsh test_with_confirmation without docker 2016-06-27 23:12:05 +03:00
Matt Kotsenas
16c110823d Add semi-colons in powershell alias
Add semi-colons in powershell alias so that if line breaks get lost the
function can still be invoked directly. This makes it possible to add
thefuck to the current session by running

    iex "$(thefuck --alias)"

which mirrors the eval syntax in bash
2016-06-27 13:29:24 -04:00
Romans Volosatovs
01418526b4 ui: accept 'q' as quit character
'q' is a standard character used in traditional UNIX environment
for 'quit', so it makes sense to support it in my opinion
2016-06-25 12:31:08 +02:00
mklkj
d2845a0d2e Fix a typo 2016-06-23 14:28:37 +02:00
nvbn
42853f41bb Merge branch 'TheJakeSchmidt-add-git_rebase_continue_no_changes' 2016-06-15 19:29:14 +04:00
nvbn
5f11ecc4f8 #515: Allow less strict check, use git_support 2016-06-15 19:28:58 +04:00
Jake
4bd4c0f731 Add a new rule git_rebase_no_changes. 2016-06-11 19:20:33 -04:00
Vladimir Iakovlev
b8c5433dc4 Merge pull request #513 from scorphus/cleanup_
#N/A: Cleanup shells/fish.py a bit
2016-06-06 06:46:36 +03:00
Pablo Santiago Blum de Aguiar
e2883430bc #N/A: Cleanup shells/fish.py a bit 2016-06-04 23:18:30 -03:00
Vladimir Iakovlev
a4b690369c Merge pull request #506 from asergi/pathlib2
Switch from pathlib to pathlib2
2016-05-17 02:23:03 +03:00
Vladimir Iakovlev
6bbd680e56 Merge pull request #507 from scorphus/assert-warns
#N/A: Assert deprecated warnings are raised
2016-05-17 02:22:34 +03:00
Pablo Santiago Blum de Aguiar
e5f8e9c0de #N/A: Assert deprecated warnings are raised 2016-05-15 17:04:09 -03:00
Alessio Sergi
ebf1ea88f5 Switch from pathlib to pathlib2
The pathlib backport module is no longer maintained. The development
has moved to the pathlib2 module instead.

Quoting from the pathlib's README:
"Attention: this backport module isn't maintained anymore. If you want
to report issues or contribute patches, please consider the pathlib2
project instead."
2016-05-12 17:17:17 +02:00
Vladimir Iakovlev
d2b0b6e8ec Merge pull request #505 from scorphus/decode-bin
#504: Decode binary name if on Python 2
2016-05-12 04:47:57 +03:00
Pablo Santiago Blum de Aguiar
561eb12c08 #504: UTF8-decode bin names if on Python 2
Fix #504
2016-05-11 13:31:57 -03:00
Pablo Santiago Blum de Aguiar
ed38fedf26 #504: Mock get_all_executables internals instead 2016-05-11 02:04:05 -03:00
nvbn
15bcd7f03f #501: Deprecate installation script 2016-05-09 18:54:40 +03:00
Vladimir Iakovlev
9e39bcd55c Merge pull request #503 from scorphus/readme-osx
#501: Split Ubuntu and OS X installation instructions
2016-05-09 15:14:05 +03:00
Pablo Santiago Blum de Aguiar
cfa73f10d6 #501: Split Ubuntu and OS X installation instructions 2016-05-03 22:54:22 -03:00
Vladimir Iakovlev
c3709682d5 Merge pull request #500 from scorphus/history-merge
#495: Alter history only when configured to do so
2016-05-03 13:18:12 +03:00
Pablo Santiago Blum de Aguiar
96f7e53aa2 #495: Alter history only when configured to do so 2016-04-30 18:39:08 -03:00
Pablo Santiago Blum de Aguiar
d1f55603fe #495: Merge history only when alter_history is set 2016-04-29 23:21:28 -03:00
nvbn
f74bbb7a9a Bump to 3.9 2016-04-24 17:56:06 +03:00
nvbn
d6c2c7266d Merge branch 'scorphus-fish-put-to-history' 2016-04-22 03:17:40 +03:00
nvbn
51839e65cd #495: Add comment in put_to_history 2016-04-22 03:16:16 +03:00
nvbn
d5ae3a6b41 Merge branch 'fish-put-to-history' of https://github.com/scorphus/thefuck into scorphus-fish-put-to-history
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2016-04-22 03:14:31 +03:00
Vladimir Iakovlev
9f421a17e5 Merge pull request #494 from scorphus/brew-update-formula
#N/A Add a new rule `brew_update_formula`
2016-04-21 13:06:11 +03:00
Pablo Santiago Blum de Aguiar
9d9820676a #N/A Add a new rule brew_update_formula 2016-04-20 22:27:39 -03:00
nvbn
5ec4909d2f #N/A: Minor style changes 2016-04-12 00:37:18 +03:00
nvbn
c6d2766553 #N/A: Add chmod +x rule 2016-04-11 16:13:41 +03:00
35 changed files with 371 additions and 147 deletions

View File

@@ -5,17 +5,13 @@ python:
- "3.4"
- "3.3"
- "2.7"
services:
- docker
addons:
apt:
sources:
- fish-shell/release-2
packages:
- bash
- zsh
- fish
- tcsh
- pandoc
- git
- python-commandnotfound
- python3-commandnotfound
install:
- pip install -U pip
- pip install -U coveralls
@@ -24,5 +20,8 @@ install:
- rm -rf build
script:
- export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1}
- coverage run --source=thefuck,tests -m py.test -v --capture=sys --run-without-docker --enable-functional
after_success: coveralls
- export RUN_TESTS="coverage run --source=thefuck,tests -m py.test -v --capture=sys"
- if [[ $TRAVIS_PYTHON_VERSION == 3.5 ]]; then $RUN_TESTS --enable-functional; fi
- if [[ $TRAVIS_PYTHON_VERSION != 3.5 ]]; then $RUN_TESTS; fi
after_success:
- coveralls

View File

@@ -94,17 +94,23 @@ Reading package lists... Done
- pip
- python-dev
## Installation [*experimental*]
## Installation
On Ubuntu and OS X you can install `The Fuck` with installation script:
On OS X you can install `The Fuck` with [Homebrew][homebrew]:
```bash
wget -O - https://raw.githubusercontent.com/nvbn/thefuck/master/install.sh | sh - && $0
brew install thefuck
```
## Manual installation
On Ubuntu you can install `The Fuck` with:
```bash
sudo apt update
sudo apt install python3-dev python3-pip
sudo -H pip3 install thefuck
```
Install `The Fuck` with `pip`:
On other systems you can install `The Fuck` with `pip`:
```bash
sudo -H pip install thefuck
@@ -144,6 +150,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `cd_correction` – spellchecks and correct failed cd commands;
* `cd_mkdir` – creates directories before cd'ing into them;
* `cd_parent` – changes `cd..` to `cd ..`;
* `chmod_x` – add execution bit;
* `composer_not_command` – fixes composer command name;
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `cpp11` – adds missing `-std=c++11` to `g++` or `clang++`;
@@ -168,6 +175,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `git_pull_clone` – clones instead of pulling when the repo does not exist;
* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_push_pull` – runs `git pull` when `push` was rejected;
* `git_rebase_no_changes` – runs `git rebase --skip` instead of `git rebase --continue` when there are no changes;
* `git_rm_recursive` – adds `-r` when you try to `rm` a directory;
* `git_remote_seturl_add` – runs `git remote add` when `git remote set_url` on nonexistant remote;
* `git_stash` – stashes you local modifications before rebasing or switching branch;
@@ -183,6 +191,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `javac` – appends missing `.java` when compiling Java files;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `ln_no_hard_link` – catches hard link creation on directories, suggest symbolic link;
* `ln_s_order` – fixes `ln -s` arguments order;
* `ls_lah` – adds `-lah` to `ls`;
* `man` – changes manual section;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`;
@@ -221,6 +230,7 @@ Enabled by default only on specific platforms:
* `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_update_formula` &ndash; turns `brew update <formula>` into `brew upgrade <formula>`;
* `brew_upgrade` &ndash; appends `--all` to `brew upgrade` as per Homebrew's new behaviour;
* `pacman` &ndash; installs app with `pacman` if it is not installed (uses `yaourt` if available);
* `pacman_not_found` &ndash; fixes package name with `pacman` or `yaourt`.
@@ -371,3 +381,4 @@ Project License can be found [here](LICENSE.md).
[coverage-link]: https://coveralls.io/github/nvbn/thefuck
[license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg
[examples-link]: https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif
[homebrew]: http://brew.sh/

View File

@@ -1,61 +1,4 @@
#!/bin/sh
should_add_alias () {
[ -f $1 ] && ! grep -q thefuck $1
}
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
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
# Generic 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
# 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 ~/.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 ~/.tcshrc; then
echo 'eval `thefuck --alias`' >> ~/.tcshrc
fi
}
install_thefuck
echo "Installation script is deprecated!"
echo "For installation instruction please visit https://github.com/nvbn/thefuck"

View File

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

View File

@@ -1,4 +1,7 @@
from pathlib import Path
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
import pytest
from thefuck import shells
from thefuck import conf, const

View File

@@ -29,25 +29,25 @@ def proc(request, spawnu):
@pytest.mark.functional
@pytest.mark.skip_without_docker
@pytest.mark.once_without_docker
def test_with_confirmation(proc, TIMEOUT):
with_confirmation(proc, TIMEOUT)
@pytest.mark.functional
@pytest.mark.skip_without_docker
@pytest.mark.once_without_docker
def test_select_command_with_arrows(proc, TIMEOUT):
select_command_with_arrows(proc, TIMEOUT)
@pytest.mark.functional
@pytest.mark.skip_without_docker
@pytest.mark.once_without_docker
def test_refuse_with_confirmation(proc, TIMEOUT):
refuse_with_confirmation(proc, TIMEOUT)
@pytest.mark.functional
@pytest.mark.skip_without_docker
@pytest.mark.once_without_docker
def test_without_confirmation(proc, TIMEOUT):
without_confirmation(proc, TIMEOUT)

View File

@@ -1,25 +0,0 @@
import pytest
from thefuck.utils import get_installation_info
envs = ((u'bash', 'thefuck/ubuntu-bash', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy bash
'''), (u'bash', 'thefuck/generic-bash', u'''
FROM fedora:latest
RUN dnf install -yy python-devel sudo wget gcc
'''))
@pytest.mark.functional
@pytest.mark.skip_without_docker
@pytest.mark.parametrize('shell, tag, dockerfile', envs)
def test_installation(spawnu, shell, TIMEOUT, tag, dockerfile):
proc = spawnu(tag, dockerfile, shell)
proc.sendline(u'cat /src/install.sh | sh - && $0')
proc.sendline(u'thefuck --version')
version = get_installation_info().version
assert proc.expect([TIMEOUT, u'thefuck {}'.format(version)],
timeout=600)
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'No fucks given'])

View File

@@ -40,7 +40,7 @@ def plot(proc, TIMEOUT):
@pytest.mark.functional
@pytest.mark.skip_without_docker
@pytest.mark.once_without_docker
@pytest.mark.benchmark(min_rounds=10)
def test_performance(spawnu, TIMEOUT, benchmark):
proc = spawnu(u'thefuck/ubuntu-python3-bash-performance',

View File

@@ -25,6 +25,7 @@ def proc(request, spawnu, run_without_docker):
if not run_without_docker:
proc.sendline(u'pip install /src')
proc.sendline(u'tcsh')
proc.sendline(u'setenv PYTHONIOENCODING utf8')
proc.sendline(u'eval `thefuck --alias`')
return proc

View File

@@ -49,7 +49,7 @@ def test_select_command_with_arrows(proc, TIMEOUT):
@pytest.mark.functional
@pytest.mark.skip_without_docker
@pytest.mark.once_without_docker
def test_refuse_with_confirmation(proc, TIMEOUT):
refuse_with_confirmation(proc, TIMEOUT)
history_not_changed(proc, TIMEOUT)

View File

@@ -0,0 +1,30 @@
import pytest
from tests.utils import Command
from thefuck.rules.brew_update_formula import get_new_command, match
@pytest.fixture
def stderr():
return ("Error: This command updates brew itself, and does not take formula"
" names.\nUse 'brew upgrade <formula>'.")
@pytest.fixture
def new_command(formula):
return 'brew upgrade {}'.format(formula)
@pytest.mark.parametrize('script', ['brew update foo', 'brew update bar zap'])
def test_match(stderr, script):
assert match(Command(script=script, stderr=stderr))
@pytest.mark.parametrize('script', ['brew upgrade foo', 'brew update'])
def test_not_match(script):
assert not match(Command(script=script, stderr=''))
@pytest.mark.parametrize('script, formula, ', [
('brew update foo', 'foo'), ('brew update bar zap', 'bar zap')])
def test_get_new_command(stderr, new_command, script, formula):
assert get_new_command(Command(script=script, stderr=stderr)) == new_command

View File

@@ -0,0 +1,39 @@
import pytest
from tests.utils import Command
from thefuck.rules.chmod_x import match, get_new_command
@pytest.fixture
def file_exists(mocker):
return mocker.patch('os.path.exists', return_value=True)
@pytest.fixture
def file_access(mocker):
return mocker.patch('os.access', return_value=False)
@pytest.mark.usefixtures('file_exists', 'file_access')
@pytest.mark.parametrize('script, stderr', [
('./gradlew build', 'gradlew: Permission denied'),
('./install.sh --help', 'install.sh: permission denied')])
def test_match(script, stderr):
assert match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, stderr, exists, callable', [
('./gradlew build', 'gradlew: Permission denied', True, True),
('./gradlew build', 'gradlew: Permission denied', False, False),
('./gradlew build', 'gradlew: error', True, False),
('gradlew build', 'gradlew: Permission denied', True, False)])
def test_not_match(file_exists, file_access, script, stderr, exists, callable):
file_exists.return_value = exists
file_access.return_value = callable
assert not match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, result', [
('./gradlew build', 'chmod +x gradlew && ./gradlew build'),
('./install.sh --help', 'chmod +x install.sh && ./install.sh --help')])
def test_get_new_command(script, result):
assert get_new_command(Command(script)) == result

View File

@@ -0,0 +1,28 @@
import pytest
from thefuck.rules.git_rebase_no_changes import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stdout():
return '''Applying: Test commit
No changes - did you forget to use 'git add'?
If there is nothing left to stage, chances are that something else
already introduced the same changes; you might want to skip this patch.
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
'''
def test_match(stdout):
assert match(Command('git rebase --continue', stdout=stdout))
assert not match(Command('git rebase --continue'))
assert not match(Command('git rebase --skip'))
def test_get_new_command(stdout):
assert (get_new_command(Command('git rebase --continue', stdout=stdout)) ==
'git rebase --skip')

View File

@@ -20,7 +20,7 @@ def test_match(script, stderr):
("ln a b", "... hard link"),
("sudo ln a b", "... hard link"),
("a b", error)])
def test_assert_not_match(script, stderr):
def test_not_match(script, stderr):
command = Command(script, stderr=stderr)
assert not match(command)

View File

@@ -0,0 +1,41 @@
import pytest
from thefuck.rules.ln_s_order import match, get_new_command
from tests.utils import Command
@pytest.fixture
def file_exists(mocker):
return mocker.patch('os.path.exists', return_value=True)
get_stderr = "ln: failed to create symbolic link '{}': File exists".format
@pytest.mark.usefixtures('file_exists')
@pytest.mark.parametrize('script', [
'ln -s dest source',
'ln dest -s source',
'ln dest source -s'])
def test_match(script):
stderr = get_stderr('source')
assert match(Command(script, stderr=stderr))
@pytest.mark.parametrize('script, stderr, exists', [
('ln dest source', get_stderr('source'), True),
('ls -s dest source', get_stderr('source'), True),
('ln -s dest source', '', True),
('ln -s dest source', get_stderr('source'), False)])
def test_not_match(file_exists, script, stderr, exists):
file_exists.return_value = exists
assert not match(Command(script, stderr=stderr))
@pytest.mark.usefixtures('file_exists')
@pytest.mark.parametrize('script, result', [
('ln -s dest source', 'ln -s source dest'),
('ln dest -s source', 'ln -s source dest'),
('ln dest source -s', 'ln source -s dest')])
def test_match(script, result):
stderr = get_stderr('source')
assert get_new_command(Command(script, stderr=stderr)) == result

View File

@@ -74,7 +74,24 @@ class TestFish(object):
assert 'TF_ALIAS=fuck PYTHONIOENCODING' in shell.app_alias('fuck')
assert 'PYTHONIOENCODING=utf-8 thefuck' in shell.app_alias('fuck')
def test_app_alias_alter_history(self, settings, shell):
settings.alter_history = True
assert 'history --delete' in shell.app_alias('FUCK')
assert 'history --merge' in shell.app_alias('FUCK')
settings.alter_history = False
assert 'history --delete' not in shell.app_alias('FUCK')
assert 'history --merge' not 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.parametrize('entry, entry_utf8', [
('ls', '- cmd: ls\n when: 1430707243\n'),
(u'echo café', '- cmd: echo café\n when: 1430707243\n')])
def test_put_to_history(self, entry, entry_utf8, builtins_open, mocker, shell):
mocker.patch('thefuck.shells.fish.time', return_value=1430707243.3517463)
shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8)

View File

@@ -1,7 +1,12 @@
# -*- coding: utf-8 -*-
import pytest
from pathlib import Path
try:
from pathlib import Path
pathlib_name = 'pathlib'
except ImportError:
from pathlib2 import Path
pathlib_name = 'pathlib2'
from thefuck import corrector, const
from tests.utils import Rule, Command, CorrectedCommand
from thefuck.corrector import get_corrected_commands, organize_commands
@@ -11,7 +16,7 @@ class TestGetRules(object):
@pytest.fixture
def glob(self, mocker):
results = {}
mocker.patch('pathlib.Path.glob',
mocker.patch(pathlib_name + '.Path.glob',
new_callable=lambda: lambda *_: results.pop('value', []))
return lambda value: results.update({'value': value})

View File

@@ -3,7 +3,10 @@
import os
from subprocess import PIPE
from mock import Mock
from pathlib import Path
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
import pytest
from tests.utils import CorrectedCommand, Rule, Command
from thefuck import const

View File

@@ -25,15 +25,16 @@ def test_read_actions(patch_get_key):
# Ignored:
'x', 'y',
# Up:
const.KEY_UP,
const.KEY_UP, 'k',
# Down:
const.KEY_DOWN,
const.KEY_DOWN, 'j',
# Ctrl+C:
const.KEY_CTRL_C])
assert list(islice(ui.read_actions(), 5)) \
const.KEY_CTRL_C, 'q'])
assert list(islice(ui.read_actions(), 8)) \
== [const.ACTION_SELECT, const.ACTION_SELECT,
const.ACTION_PREVIOUS, const.ACTION_NEXT,
const.ACTION_ABORT]
const.ACTION_PREVIOUS, const.ACTION_PREVIOUS,
const.ACTION_NEXT, const.ACTION_NEXT,
const.ACTION_ABORT, const.ACTION_ABORT]
def test_command_selector():

View File

@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
import pytest
import warnings
from mock import Mock
import six
from thefuck.utils import default_settings, \
@@ -199,7 +202,8 @@ class TestCompatibilityCall(object):
assert settings == _settings
return True
assert compatibility_call(match, Command())
with pytest.warns(UserWarning):
assert compatibility_call(match, Command())
def test_get_new_command(self):
def get_new_command(command):
@@ -214,7 +218,8 @@ class TestCompatibilityCall(object):
assert settings == _settings
return True
assert compatibility_call(get_new_command, Command())
with pytest.warns(UserWarning):
assert compatibility_call(get_new_command, Command())
def test_side_effect(self):
def side_effect(command, new_command):
@@ -229,15 +234,22 @@ class TestCompatibilityCall(object):
assert settings == _settings
return True
assert compatibility_call(side_effect, Command(), Command())
with pytest.warns(UserWarning):
assert compatibility_call(side_effect, Command(), Command())
class TestGetValidHistoryWithoutCurrent(object):
@pytest.yield_fixture(autouse=True)
def fail_on_warning(self):
warnings.simplefilter('error')
yield
warnings.resetwarnings()
@pytest.fixture(autouse=True)
def history(self, mocker):
return mocker.patch('thefuck.shells.shell.get_history',
return_value=['le cat', 'fuck', 'ls cat',
'diff x', 'nocommand x'])
'diff x', 'nocommand x', u'café ô'])
@pytest.fixture(autouse=True)
def alias(self, mocker):
@@ -245,14 +257,22 @@ class TestGetValidHistoryWithoutCurrent(object):
return_value='fuck')
@pytest.fixture(autouse=True)
def callables(self, mocker):
return mocker.patch('thefuck.utils.get_all_executables',
return_value=['diff', 'ls'])
def bins(self, mocker, monkeypatch):
monkeypatch.setattr('thefuck.conf.os.environ', {'PATH': 'path'})
callables = list()
for name in ['diff', 'ls', 'café']:
bin_mock = mocker.Mock(name=name)
bin_mock.configure_mock(name=name, is_dir=lambda: False)
callables.append(bin_mock)
path_mock = mocker.Mock(iterdir=mocker.Mock(return_value=callables))
return mocker.patch('thefuck.utils.Path', return_value=path_mock)
@pytest.mark.parametrize('script, result', [
('le cat', ['ls cat', 'diff x']),
('diff x', ['ls cat']),
('fuck', ['ls cat', 'diff x'])])
('le cat', ['ls cat', 'diff x', u'café ô']),
('diff x', ['ls cat', u'café ô']),
('fuck', ['ls cat', 'diff x', u'café ô']),
(u'cafe ô', ['ls cat', 'diff x', u'café ô']),
])
def test_get_valid_history_without_current(self, script, result):
command = Command(script=script)
assert get_valid_history_without_current(command) == result

View File

@@ -1,7 +1,10 @@
from imp import load_source
import os
import sys
from pathlib import Path
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
from six import text_type
from . import const

View File

@@ -1,4 +1,7 @@
from pathlib import Path
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
from .conf import settings
from .types import Rule
from . import logs

View File

@@ -33,6 +33,8 @@ def fix_command():
if selected_command:
selected_command.run(command)
else:
sys.exit(1)
def print_alias(entry_point=True):

View File

@@ -0,0 +1,12 @@
from thefuck.utils import for_app
@for_app('brew', at_least=2)
def match(command):
return ('update' in command.script
and "Error: This command updates brew itself" in command.stderr
and "Use 'brew upgrade <formula>'" in command.stderr)
def get_new_command(command):
return command.script.replace('update', 'upgrade')

15
thefuck/rules/chmod_x.py Normal file
View File

@@ -0,0 +1,15 @@
import os
from thefuck.shells import shell
def match(command):
return (command.script.startswith('./')
and 'permission denied' in command.stderr.lower()
and os.path.exists(command.script_parts[0])
and not os.access(command.script_parts[0], os.X_OK))
def get_new_command(command):
return shell.and_(
'chmod +x {}'.format(command.script_parts[0][2:]),
command.script)

View File

@@ -0,0 +1,15 @@
from thefuck.specific.git import git_support
@git_support
def match(command):
return ({'rebase', '--continue'}.issubset(command.script_parts) and
'No changes - did you forget to use \'git add\'?' in command.stdout)
def get_new_command(command):
return 'git rebase --skip'
enabled_by_default = True
requires_output = True

View File

@@ -15,7 +15,7 @@ from thefuck.specific.sudo import sudo_support
@sudo_support
def match(command):
return (command.stderr.endswith("hard link not allowed for directory") and
command.script.startswith("ln "))
command.script_parts[0] == 'ln')
@sudo_support

View File

@@ -0,0 +1,26 @@
import os
from thefuck.specific.sudo import sudo_support
def _get_destination(script_parts):
"""When arguments order is wrong first argument will be destination."""
for part in script_parts:
if part not in {'ln', '-s', '--symbolic'} and os.path.exists(part):
return part
@sudo_support
def match(command):
return (command.script_parts[0] == 'ln'
and {'-s', '--symbolic'}.intersection(command.script_parts)
and 'File exists' in command.stderr
and _get_destination(command.script_parts))
@sudo_support
def get_new_command(command):
destination = _get_destination(command.script_parts)
parts = command.script_parts[:]
parts.remove(destination)
parts.append(destination)
return ' '.join(parts)

View File

@@ -22,8 +22,7 @@ shells = {'bash': Bash,
def _get_shell():
proc = Process(os.getpid())
while (proc is not None):
name = None
while proc is not None:
try:
name = proc.name()
except TypeError:

View File

@@ -1,6 +1,10 @@
from subprocess import Popen, PIPE
from time import time
import os
import sys
import six
from .. import logs
from ..conf import settings
from ..utils import DEVNULL, memoize, cache
from .generic import Generic
@@ -15,17 +19,20 @@ class Fish(Generic):
return default
def app_alias(self, fuck):
if settings.alter_history:
alter_history = (' history --delete $fucked_up_command\n'
' history --merge ^ /dev/null\n')
else:
alter_history = ''
# It is VERY important to have the variables declared WITHIN the alias
return ('function {0} -d "Correct your previous console command"\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'
' history --delete $fucked_up_command\n'
' history --merge ^ /dev/null\n'
' eval $unfucked_command\n{1}'
' end\n'
'end').format(fuck)
'end').format(fuck, alter_history)
@memoize
@cache('.config/fish/config.fish', '.config/fish/functions')
@@ -43,10 +50,6 @@ class Fish(Generic):
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def _get_history_file_name(self):
return os.path.expanduser('~/.config/fish/fish_history')
@@ -65,3 +68,20 @@ class Fish(Generic):
def how_to_configure(self):
return (r"eval (thefuck --alias | tr '\n' ';')",
'~/.config/fish/config.fish')
def put_to_history(self, command):
try:
return self._put_to_history(command)
except IOError:
logs.exception("Can't update history", sys.exc_info())
def _put_to_history(self, command_script):
"""Puts command script to shell history."""
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)

View File

@@ -81,3 +81,11 @@ class Generic(object):
def _script_from_history(self, line):
return line
def put_to_history(self, command):
"""Adds fixed command to shell history.
In most of shells we change history on shell-level, but not
all shells support it (Fish).
"""

View File

@@ -4,10 +4,10 @@ from .generic import Generic
class Powershell(Generic):
def app_alias(self, fuck):
return 'function ' + fuck + ' { \n' \
' $fuck = $(thefuck (Get-History -Count 1).CommandLine)\n' \
' $fuck = $(thefuck (Get-History -Count 1).CommandLine);\n' \
' if (-not [string]::IsNullOrWhiteSpace($fuck)) {\n' \
' if ($fuck.StartsWith("echo")) { $fuck = $fuck.Substring(5) }\n' \
' else { iex "$fuck" }\n' \
' if ($fuck.StartsWith("echo")) { $fuck = $fuck.Substring(5); }\n' \
' else { iex "$fuck"; }\n' \
' }\n' \
'}\n'

View File

@@ -280,6 +280,8 @@ class CorrectedCommand(object):
"""
if self.side_effect:
compatibility_call(self.side_effect, old_cmd, self.script)
if settings.alter_history:
shell.put_to_history(self.script)
# This depends on correct setting of PYTHONIOENCODING by the alias:
logs.debug(u'PYTHONIOENCODING: {}'.format(
os.environ.get('PYTHONIOENCODING', '!!not-set!!')))

View File

@@ -16,7 +16,7 @@ def read_actions():
yield const.ACTION_PREVIOUS
elif key in (const.KEY_DOWN, 'j'):
yield const.ACTION_NEXT
elif key == const.KEY_CTRL_C:
elif key in (const.KEY_CTRL_C, 'q'):
yield const.ACTION_ABORT
elif key in ('\n', '\r'):
yield const.ACTION_SELECT

View File

@@ -10,7 +10,10 @@ from decorator import decorator
from difflib import get_close_matches
from functools import wraps
from inspect import getargspec
from pathlib import Path
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
from warnings import warn
DEVNULL = open(os.devnull, 'w')
@@ -110,7 +113,7 @@ def get_all_executables():
tf_entry_points = get_installation_info().get_entry_map()\
.get('console_scripts', {})\
.keys()
bins = [exe.name
bins = [exe.name.decode('utf8') if six.PY2 else exe.name
for path in os.environ.get('PATH', '').split(':')
for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)