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

Compare commits

...

45 Commits
2.0 ... 2.3

Author SHA1 Message Date
nvbn
6c534c52bc Bump to 2.3 2015-07-22 04:45:04 +03:00
nvbn
b4392ba706 #N/A Add heroku_not_command rule 2015-07-22 04:44:37 +03:00
Vladimir Iakovlev
46f918718f Merge pull request #307 from evverx/lc_all
Force LC_ALL to C
2015-07-21 17:11:16 +03:00
Vladimir Iakovlev
d71ce76ae4 Merge pull request #306 from mcarton/hub
Support GitHub's hub command
2015-07-21 16:43:24 +03:00
nvbn
355505a0a8 #N/A Make git_checkout test less dependent on get_closest 2015-07-21 16:40:45 +03:00
Evgeny Vereshchagin
3d425ce831 Force LC_ALL to C
See: http://unix.stackexchange.com/a/87763/120177
2015-07-21 13:39:34 +00:00
mcarton
98a9fb3d7d Remove now redundant checks in git_* rules 2015-07-21 15:35:39 +02:00
nvbn
c8d748e095 Bump to 2.2 2015-07-21 16:31:17 +03:00
nvbn
e0af35819d Merge branch 'master' of github.com:nvbn/thefuck 2015-07-21 16:19:11 +03:00
nvbn
9e4c250e4e #301 Fix bash support on non-eng systems 2015-07-21 16:19:01 +03:00
Vladimir Iakovlev
8c395377f8 Merge pull request #299 from evverx/dnf-history
Add `dnf history` error for the sudo rule
2015-07-21 16:15:08 +03:00
Vladimir Iakovlev
f165523247 Merge pull request #304 from mcarton/fix-git_diff_staged
Fix the `git_diff_staged` rule
2015-07-21 16:12:19 +03:00
mcarton
903abff77e Support hub as well as git in @git_support 2015-07-21 15:06:04 +02:00
Evgeny Vereshchagin
6d39b78824 Add dnf history error for the sudo rule
$ dnf history
You don't have access to the history DB.
2015-07-21 12:56:25 +00:00
mcarton
1285303363 Fix the git_diff_staged rule
The problem was:
```
% git add foo
% git diff foo
% fuck
git diff foo --staged [enter/ctrl+c]
fatal: bad flag '--staged' used after filename
```
2015-07-21 14:06:37 +02:00
nvbn
66e2ec7e3f Merge branch 'mcarton-fix-readme' 2015-07-20 22:49:31 +03:00
nvbn
92cca7b641 #296 Fix [enter/ctrl+c] case in the readme 2015-07-20 22:49:21 +03:00
mcarton
e572cab1f3 Have the README look better 2015-07-20 21:12:39 +02:00
mcarton
33b1536c28 Move misplaced rule in README 2015-07-20 21:07:56 +02:00
mcarton
d4fada8e4c Reflect the new default for require_confirmation 2015-07-20 21:06:59 +02:00
mcarton
afc089bc3c Be more consistent in README 2015-07-20 20:49:21 +02:00
nvbn
300c8f528a #N/A Mention tcsh in readme 2015-07-20 21:27:19 +03:00
nvbn
7b011a504d #N/A Fix tests in travis 2015-07-20 21:24:00 +03:00
nvbn
164103693b Bump to 2.1 2015-07-20 21:16:53 +03:00
nvbn
a21c99200e #294 Mention common shells configs in readme 2015-07-20 21:15:34 +03:00
nvbn
1b961c4b87 #294 Move entry point for alias to main 2015-07-20 21:14:43 +03:00
nvbn
a849b65352 Merge branch 'easy-install' of https://github.com/mcarton/thefuck into mcarton-easy-install 2015-07-20 21:06:21 +03:00
nvbn
dee018e792 #N/A Move get_all_executables (formerly get_all_callables) to utils 2015-07-20 21:04:49 +03:00
nvbn
c67560864a #295 Add git_push_pull rule 2015-07-20 20:51:18 +03:00
Vladimir Iakovlev
b636e9bec7 Merge pull request #295 from mcarton/new-git-rules
New git rules
2015-07-20 20:42:02 +03:00
nvbn
36450b740f #270 Add default priority in the readme 2015-07-20 20:01:45 +03:00
mcarton
0f67aad93b Update README 2015-07-20 18:58:16 +02:00
mcarton
bb7579ead5 Add the git_pull_clone rule 2015-07-20 18:58:16 +02:00
mcarton
569709388d Add a git_push_force rule 2015-07-20 18:58:11 +02:00
nvbn
baf7796295 #129 Ignore thefuck alias in switch_lang rule 2015-07-20 19:40:45 +03:00
nvbn
7b32f1df04 #N/A Fix debug output with unicode commands 2015-07-20 19:35:32 +03:00
nvbn
cd084c8ba6 #N/A Fix history rule with blank history 2015-07-20 19:30:41 +03:00
nvbn
4f5659caad #87 Add ability to fix branch names in git_checkout rule 2015-07-20 19:25:29 +03:00
mcarton
370f258b89 Change installation method in README 2015-07-20 13:40:07 +02:00
mcarton
9a069daada Make thefuck-alias generated alias a parameter 2015-07-20 13:35:22 +02:00
nvbn
ee87d1c547 #N/A Ignore history lines before fuck call in history rule 2015-07-20 01:53:32 +03:00
Vladimir Iakovlev
7e03b55729 Merge pull request #293 from mcarton/git-aliases
#292 #290 Use @git_support in all git rules
2015-07-20 01:25:25 +03:00
mcarton
db76462802 #292 #290 Use @git_support in all git rules 2015-07-20 00:08:01 +02:00
Vladimir Iakovlev
dbf20ebc73 Fix typo 2015-07-19 22:41:10 +03:00
Vladimir Iakovlev
b8a74b1425 Remove barely working coveralls badge 2015-07-19 22:40:25 +03:00
37 changed files with 509 additions and 184 deletions

View File

@@ -7,6 +7,7 @@ install:
- pip install -r requirements.txt
- python setup.py develop
- pip install coveralls
- rm -rf build
script:
- export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1}
- coverage run --source=thefuck,tests -m py.test -v

View File

@@ -1,6 +1,4 @@
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck) [![Coverage Status](https://coveralls.io/repos/nvbn/thefuck/badge.svg?branch=master&service=github)](https://coveralls.io/github/nvbn/thefuck?branch=master)
**Aliases changed in 1.34.**
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck)
Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/)
@@ -16,7 +14,7 @@ E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?
➜ fuck
sudo apt-get install vim
sudo apt-get install vim [enter/ctrl+c]
[sudo] password for nvbn:
Reading package lists... Done
...
@@ -31,7 +29,7 @@ To push the current branch and set the remote as upstream, use
➜ fuck
git push --set-upstream origin master
git push --set-upstream origin master [enter/ctrl+c]
Counting objects: 9, done.
...
```
@@ -44,7 +42,7 @@ No command 'puthon' found, did you mean:
zsh: command not found: puthon
➜ fuck
python
python [enter/ctrl+c]
Python 3.4.2 (default, Oct 8 2014, 13:08:17)
...
```
@@ -57,7 +55,7 @@ Did you mean this?
branch
➜ fuck
git branch
git branch [enter/ctrl+c]
* master
```
@@ -69,13 +67,13 @@ Did you mean this?
repl
➜ fuck
lein repl
lein repl [enter/ctrl+c]
nREPL server started on port 54848 on host 127.0.0.1 - nrepl://127.0.0.1:54848
REPL-y 0.3.1
...
```
If you are scared to blindly run the changed command, there is a `require_confirmation`
If you are not scared to blindly run the changed command, there is a `require_confirmation`
[settings](#settings) option:
```bash
@@ -84,7 +82,7 @@ E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?
➜ fuck
sudo apt-get install vim [Enter/Ctrl+C]
sudo apt-get install vim
[sudo] password for nvbn:
Reading package lists... Done
...
@@ -106,32 +104,15 @@ sudo pip install thefuck
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
And add to the `.bashrc` or `.bash_profile`(for OSX):
You should place this command in your `.bash_profile`, `.bashrc`, `.zshrc` or other startup script:
```bash
alias fuck='eval $(thefuck $(fc -ln -1)); history -r'
eval "$(thefuck-alias)"
# You can use whatever you want as an alias, like for Mondays:
alias FUCK='fuck'
eval "$(thefuck-alias FUCK)"
```
Or in your `.zshrc`:
```bash
alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'
```
If you are using `tcsh`:
```tcsh
alias fuck 'set fucked_cmd=`history -h 2 | head -n 1` && eval `thefuck ${fucked_cmd}`'
```
Alternatively, you can redirect the output of `thefuck-alias`:
```bash
thefuck-alias >> ~/.bashrc
```
[Or in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases)
[Or in your shell config (Bash, Zsh, Fish, Powershell, tcsh).](https://github.com/nvbn/thefuck/wiki/Shell-aliases)
Changes will be available only in a new shell session.
To make them available immediately, run `source ~/.bashrc` (or your shell config file like `.zshrc`).
@@ -143,6 +124,8 @@ To make them available immediately, run `source ~/.bashrc` (or your shell config
sudo pip install thefuck --upgrade
```
**Aliases changed in 1.34.**
## How it works
The Fuck tries to match a rule for the previous command, creates a new command
@@ -155,29 +138,32 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `cd_parent` – changes `cd..` to `cd ..`;
* `composer_not_command` – fixes composer command name;
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `cpp11` – add missing `-std=c++11` to `g++` or `clang++`;
* `cpp11` – adds missing `-std=c++11` to `g++` or `clang++`;
* `django_south_ghost` – adds `--delete-ghost-migrations` to failed because ghosts django south migration;
* `django_south_merge` – adds `--merge` to inconsistent django south migration;
* `dry` – fix repetitions like "git git push";
* `dry` – fixes repetitions like "git git push";
* `fix_alt_space` – replaces Alt+Space with Space character;
* `git_add` – fix *"Did you forget to 'git add'?"*;
* `git_add` – fixes *"Did you forget to 'git add'?"*;
* `git_branch_delete` – changes `git branch -d` to `git branch -D`;
* `git_branch_list` – catches `git branch list` in place of `git branch` and removes created branch;
* `git_checkout` – creates the branch before checking-out;
* `git_checkout` – fixes branch name or creates new branch;
* `git_diff_staged` – adds `--staged` to previous `git diff` with unexpected output;
* `git_no_command` – fixes wrong git commands like `git brnch`;
* `git_not_command` – fixes wrong git commands like `git brnch`;
* `git_pull` – sets upstream before executing previous `git pull`;
* `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_stash` – stashes you local modifications before rebasing or switching branch;
* `go_run` – appends `.go` extension when compiling/running Go programs
* `grep_recursive` – adds `-r` when you trying to grep directory;
* `has_exists_script` – prepends `./` when script/binary exists;
* `heroku_no_command` – fixes wrong heroku commands like `heroku log`;
* `history` – tries to replace command with most similar command from history;
* `java` – removes `.java` extension when running Java programs;
* `javac` – appends missing `.java` when compiling Java files;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `ls_lah` – adds -lah to ls;
* `man` – change manual section;
* `man` – changes manual section;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`;
* `mercurial` – fixes wrong `hg` commands;
* `mkdir_p` – adds `-p` when you trying to create directory without parent;
@@ -204,26 +190,30 @@ Enabled by default only on specific platforms:
* `apt_get` – installs app from apt if it not installed;
* `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
* `brew_upgrade` – appends `--all` to `brew upgrade` as per Homebrew's new behaviour;
* `pacman` – installs app with `pacman` or `yaourt` if it is not installed.
Bundled, but not enabled by default:
* `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 `~/.thefuck/rules`. Rule should contain two functions:
`match(command: Command, settings: Settings) -> bool`
and `get_new_command(command: Command, settings: Settings) -> str`.
Also the rule can contain optional function
`side_effect(command: Command, settings: Settings) -> None` and
in `~/.thefuck/rules`. The rule should contain two functions:
```python
match(command: Command, settings: Settings) -> bool
get_new_command(command: Command, settings: Settings) -> str
```
Also the rule can contain an optional function
`side_effect(command: Command, settings: Settings) -> None` and an
optional boolean `enabled_by_default`.
`Command` has three attributes: `script`, `stdout` and `stderr`.
`Settings` is a special object filled with `~/.thefuck/settings.py` and values from env, [more](#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`:
@@ -242,7 +232,7 @@ enabled_by_default = True
def side_effect(command, settings):
subprocess.call('chmod 777 .', shell=True)
priority = 1000 # Lower first
priority = 1000 # Lower first, default is 1000
```
[More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules),
@@ -257,7 +247,7 @@ The Fuck has a few settings parameters which can be changed in `~/.thefuck/setti
* `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` – enabled debug output, by default `False`;
* `debug` – enables debug output, by default `False`.
Example of `settings.py`:

View File

@@ -11,7 +11,7 @@ elif (3, 0) < sys.version_info < (3, 3):
' ({}.{} detected).'.format(*sys.version_info[:2]))
sys.exit(-1)
VERSION = '2.0'
VERSION = '2.3'
install_requires = ['psutil', 'colorama', 'six']
extras_require = {':python_version<"3.4"': ['pathlib']}
@@ -31,4 +31,4 @@ setup(name='thefuck',
extras_require=extras_require,
entry_points={'console_scripts': [
'thefuck = thefuck.main:main',
'thefuck-alias = thefuck.shells:app_alias']})
'thefuck-alias = thefuck.main:print_alias']})

View File

@@ -12,6 +12,11 @@ def did_not_match(target, did_you_forget=False):
return error
@pytest.fixture
def get_branches(mocker):
return mocker.patch('thefuck.rules.git_checkout')
@pytest.mark.parametrize('command', [
Command(script='git checkout unknown', stderr=did_not_match('unknown')),
Command(script='git commit unknown', stderr=did_not_match('unknown'))])
@@ -28,10 +33,19 @@ def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='git checkout unknown', stderr=did_not_match('unknown')),
@pytest.mark.parametrize('branches, command, new_command', [
([],
Command(script='git checkout unknown', stderr=did_not_match('unknown')),
'git branch unknown && git checkout unknown'),
(Command('git commit unknown', stderr=did_not_match('unknown')),
'git branch unknown && git commit unknown')])
def test_get_new_command(command, new_command):
([],
Command('git commit unknown', stderr=did_not_match('unknown')),
'git branch unknown && git commit unknown'),
(['master'],
Command(script='git checkout mster', stderr=did_not_match('mster')),
'git checkout master'),
(['master'],
Command(script='git commit mster', stderr=did_not_match('mster')),
'git commit master')])
def test_get_new_command(branches, command, new_command, get_branches):
get_branches.return_value = branches
assert get_new_command(command, None) == new_command

View File

@@ -3,7 +3,9 @@ from thefuck.rules.git_diff_staged import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [Command(script='git diff')])
@pytest.mark.parametrize('command', [
Command(script='git diff foo'),
Command(script='git diff')])
def test_match(command):
assert match(command, None)
@@ -18,6 +20,7 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command('git diff'), 'git diff --staged')])
(Command('git diff'), 'git diff --staged'),
(Command('git diff foo'), 'git diff --staged foo')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,21 @@
import pytest
from thefuck.rules.git_pull_clone import match, get_new_command
from tests.utils import Command
git_err = '''
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
'''
@pytest.mark.parametrize('command', [
Command(script='git pull git@github.com:mcarton/thefuck.git', stderr=git_err)])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, output', [
(Command(script='git pull git@github.com:mcarton/thefuck.git', stderr=git_err), 'git clone git@github.com:mcarton/thefuck.git')])
def test_get_new_command(command, output):
assert get_new_command(command, None) == output

View File

@@ -20,5 +20,5 @@ def test_match(stderr):
def test_get_new_command(stderr):
assert get_new_command(Command(stderr=stderr), None)\
assert get_new_command(Command('git push', stderr=stderr), None)\
== "git push --set-upstream origin master"

View File

@@ -0,0 +1,52 @@
import pytest
from thefuck.rules.git_push_force import match, get_new_command
from tests.utils import Command
git_err = '''
To /tmp/foo
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to '/tmp/bar'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
'''
git_uptodate = 'Everything up-to-date'
git_ok = '''
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 282 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To /tmp/bar
514eed3..f269c79 master -> master
'''
@pytest.mark.parametrize('command', [
Command(script='git push', stderr=git_err),
Command(script='git push nvbn', stderr=git_err),
Command(script='git push nvbn master', stderr=git_err)])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='git push', stderr=git_ok),
Command(script='git push', stderr=git_uptodate),
Command(script='git push nvbn', stderr=git_ok),
Command(script='git push nvbn master', stderr=git_uptodate),
Command(script='git push nvbn', stderr=git_ok),
Command(script='git push nvbn master', stderr=git_uptodate)])
def test_not_match(command):
assert not match(command, None)
@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')])
def test_get_new_command(command, output):
assert get_new_command(command, None) == output

View File

@@ -0,0 +1,54 @@
import pytest
from thefuck.rules.git_push_pull import match, get_new_command
from tests.utils import Command
git_err = '''
To /tmp/foo
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to '/tmp/bar'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
'''
git_uptodate = 'Everything up-to-date'
git_ok = '''
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 282 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To /tmp/bar
514eed3..f269c79 master -> master
'''
@pytest.mark.parametrize('command', [
Command(script='git push', stderr=git_err),
Command(script='git push nvbn', stderr=git_err),
Command(script='git push nvbn master', stderr=git_err)])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='git push', stderr=git_ok),
Command(script='git push', stderr=git_uptodate),
Command(script='git push nvbn', stderr=git_ok),
Command(script='git push nvbn master', stderr=git_uptodate),
Command(script='git push nvbn', stderr=git_ok),
Command(script='git push nvbn master', stderr=git_uptodate)])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, output', [
(Command(script='git push', stderr=git_err), 'git pull && git push'),
(Command(script='git push nvbn', stderr=git_err),
'git pull nvbn && git push nvbn'),
(Command(script='git push nvbn master', stderr=git_err),
'git pull nvbn master && git push nvbn master')])
def test_get_new_command(command, output):
assert get_new_command(command, None) == output

View File

@@ -0,0 +1,34 @@
import pytest
from tests.utils import Command
from thefuck.rules.heroku_not_command import match, get_new_command
def suggest_stderr(cmd):
return ''' ! `{}` is not a heroku command.
! Perhaps you meant `logs`, `pg`.
! See `heroku help` for a list of available commands.'''.format(cmd)
no_suggest_stderr = ''' ! `aaaaa` is not a heroku command.
! See `heroku help` for a list of available commands.'''
@pytest.mark.parametrize('cmd', ['log', 'pge'])
def test_match(cmd):
assert match(
Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd)), None)
@pytest.mark.parametrize('script, stderr', [
('cat log', suggest_stderr('log')),
('heroku aaa', no_suggest_stderr)])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr), None)
@pytest.mark.parametrize('cmd, result', [
('log', 'heroku logs'),
('pge', 'heroku pg')])
def test_get_new_command(cmd, result):
command = Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd))
assert get_new_command(command, None) == result

View File

@@ -6,28 +6,35 @@ from tests.utils import Command
@pytest.fixture
def history(mocker):
return mocker.patch('thefuck.rules.history.get_history',
return_value=['ls cat', 'diff x', 'nocommand x'])
return_value=['le cat', 'fuck', 'ls cat',
'diff x', 'nocommand x'])
@pytest.fixture
def alias(mocker):
return mocker.patch('thefuck.rules.history.thefuck_alias',
return_value='fuck')
@pytest.fixture
def callables(mocker):
return mocker.patch('thefuck.rules.history.get_all_callables',
return mocker.patch('thefuck.rules.history.get_all_executables',
return_value=['diff', 'ls'])
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.usefixtures('history', 'callables', 'no_memoize', 'alias')
@pytest.mark.parametrize('script', ['ls cet', 'daff x'])
def test_match(script):
assert match(Command(script=script), None)
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.usefixtures('history', 'callables', 'no_memoize', 'alias')
@pytest.mark.parametrize('script', ['apt-get', 'nocommand y'])
def test_not_match(script):
assert not match(Command(script=script), None)
@pytest.mark.usefixtures('history', 'callables', 'no_memoize')
@pytest.mark.usefixtures('history', 'callables', 'no_memoize', 'alias')
@pytest.mark.parametrize('script, result', [
('ls cet', 'ls cat'),
('daff x', 'diff x')])

View File

@@ -1,29 +1,16 @@
import pytest
from thefuck.rules.no_command import match, get_new_command, get_all_callables
from thefuck.rules.no_command import match, get_new_command
from tests.utils import Command
@pytest.fixture(autouse=True)
def _safe(mocker):
mocker.patch('thefuck.rules.no_command._safe', return_value=[])
@pytest.fixture(autouse=True)
def get_aliases(mocker):
mocker.patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
def get_all_executables(mocker):
mocker.patch('thefuck.rules.no_command.get_all_executables',
return_value=['vim', 'apt-get', 'fsck'])
@pytest.mark.usefixtures('no_memoize')
def test_get_all_callables(*args):
all_callables = get_all_callables()
assert 'vim' in all_callables
assert 'fsck' in all_callables
assert 'fuck' not in all_callables
@pytest.mark.usefixtures('no_memoize')
def test_match(*args):
def test_match():
assert match(Command(stderr='vom: not found', script='vom file.py'), None)
assert match(Command(stderr='fucck: not found', script='fucck'), None)
assert not match(Command(stderr='qweqwe: not found', script='qweqwe'), None)
@@ -31,7 +18,7 @@ def test_match(*args):
@pytest.mark.usefixtures('no_memoize')
def test_get_new_command(*args):
def test_get_new_command():
assert get_new_command(
Command(stderr='vom: not found',
script='vom file.py'),

View File

@@ -11,6 +11,7 @@ from tests.utils import Command
('need to be root', ''),
('need root', ''),
('must be root', ''),
('You don\'t have access to the history DB.', ''),
('', "error: [Errno 13] Permission denied: '/usr/local/lib/python2.7/dist-packages/ipaddr.py'")])
def test_match(stderr, stdout):
assert match(Command(stderr=stderr, stdout=stdout), None)

View File

@@ -15,6 +15,7 @@ def test_match(command):
@pytest.mark.parametrize('command', [
Command(stderr='command not found: pat-get', script=u'pat-get'),
Command(stderr='command not found: ls', script=u'ls'),
Command(stderr='command not found: агсл', script=u'агсл'),
Command(stderr='some info', script=u'фзе-пуе')])
def test_not_match(command):
assert not switch_lang.match(command, None)

View File

@@ -44,9 +44,10 @@ class TestGeneric(object):
assert shell.get_aliases() == {}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias()
assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias()
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
@@ -97,9 +98,10 @@ class TestBash(object):
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias()
assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias()
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm'])
@@ -173,9 +175,10 @@ class TestFish(object):
'ruby': 'ruby'}
def test_app_alias(self, shell):
assert 'function fuck' in shell.app_alias()
assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias()
assert 'function fuck' in shell.app_alias('fuck')
assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
@pytest.mark.usefixtures('isfile')
@@ -222,9 +225,10 @@ class TestZsh(object):
'll': 'ls -alF'}
def test_app_alias(self, shell):
assert 'alias fuck' in shell.app_alias()
assert 'thefuck' in shell.app_alias()
assert 'TF_ALIAS' in shell.app_alias()
assert 'alias fuck' in shell.app_alias('fuck')
assert 'alias FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck')
assert 'TF_ALIAS' in shell.app_alias('fuck')
def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])

View File

@@ -1,6 +1,7 @@
import pytest
from mock import Mock
from thefuck.utils import git_support, sudo_support, wrap_settings, memoize, get_closest
from thefuck.utils import git_support, sudo_support, wrap_settings,\
memoize, get_closest, get_all_executables
from thefuck.types import Settings
from tests.utils import Command
@@ -35,6 +36,20 @@ def test_git_support(called, command, stderr):
assert fn(Command(script=called, stderr=stderr), None) == command
@pytest.mark.parametrize('command, is_git', [
('git pull', True),
('hub pull', True),
('git push --set-upstream origin foo', True),
('hub push --set-upstream origin foo', True),
('ls', False),
('cat git', False),
('cat hub', False)])
def test_git_support_match(command, is_git):
@git_support
def fn(command, settings): return True
assert fn(Command(script=command), None) == is_git
def test_memoize():
fn = Mock(__name__='fn')
memoized = memoize(fn)
@@ -59,3 +74,21 @@ class TestGetClosest(object):
def test_when_cant_match(self):
assert 'status' == get_closest('st', ['status', 'reset'])
def test_without_fallback(self):
assert get_closest('st', ['status', 'reset'],
fallback_to_first=False) is None
@pytest.fixture
def get_aliases(mocker):
mocker.patch('thefuck.shells.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
@pytest.mark.usefixtures('no_memoize', 'get_aliases')
def test_get_all_callables():
all_callables = get_all_executables()
assert 'vim' in all_callables
assert 'fsck' in all_callables
assert 'fuck' not in all_callables

View File

@@ -31,7 +31,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'no_colors': False,
'debug': False,
'priority': {},
'env': {'LANG': 'C', 'GIT_TRACE': '1'}}
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',

View File

@@ -80,11 +80,11 @@ def get_command(settings, args):
return
script = shells.from_shell(script)
logs.debug('Call: {}'.format(script), settings)
logs.debug(u'Call: {}'.format(script), settings)
env = dict(os.environ)
env.update(settings.env)
logs.debug('Executing with env: {}'.format(env), settings)
logs.debug(u'Executing with env: {}'.format(env), settings)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env)
if wait_output(settings, result):
@@ -128,26 +128,35 @@ def run_rule(rule, command, settings):
print(new_command)
# Entry points:
def main():
colorama.init()
user_dir = setup_user_dir()
settings = conf.get_settings(user_dir)
logs.debug('Run with settings: {}'.format(pformat(settings)), settings)
logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv)
if command:
logs.debug('Received stdout: {}'.format(command.stdout), settings)
logs.debug('Received stderr: {}'.format(command.stderr), settings)
logs.debug(u'Received stdout: {}'.format(command.stdout), settings)
logs.debug(u'Received stderr: {}'.format(command.stderr), settings)
rules = get_rules(user_dir, settings)
logs.debug(
'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
settings)
matched_rule = get_matched_rule(command, rules, settings)
if matched_rule:
logs.debug('Matched rule: {}'.format(matched_rule.name), settings)
logs.debug(u'Matched rule: {}'.format(matched_rule.name), settings)
run_rule(matched_rule, command, settings)
return
logs.failed('No fuck given', settings)
def print_alias():
alias = shells.thefuck_alias()
if len(sys.argv) > 1:
alias = sys.argv[1]
print(shells.app_alias(alias))

View File

@@ -4,8 +4,7 @@ from thefuck import utils, shells
@utils.git_support
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
return ('did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" in command.stderr)

View File

@@ -1,7 +1,12 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return ('git branch -d' in command.script
return ('branch -d' in command.script
and 'If you are sure you want to delete it' in command.stderr)
@utils.git_support
def get_new_command(command, settings):
return command.script.replace('-d', '-D')

View File

@@ -4,7 +4,7 @@ from thefuck import utils, shells
@utils.git_support
def match(command, settings):
# catches "git branch list" in place of "git branch"
return command.script.split() == 'git branch list'.split()
return command.script.split()[1:] == 'branch list'.split()
@utils.git_support

View File

@@ -1,19 +1,36 @@
import re
import subprocess
from thefuck import shells, utils
@utils.git_support
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
return ('did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" not in command.stderr)
def get_branches():
proc = subprocess.Popen(
['git', 'branch', '-a', '--no-color', '--no-column'],
stdout=subprocess.PIPE)
for line in proc.stdout.readlines():
line = line.decode('utf-8')
if line.startswith('*'):
line = line.split(' ')[1]
if '/' in line:
line = line.split('/')[-1]
yield line.strip()
@utils.git_support
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
formatme = shells.and_('git branch {}', '{}')
return formatme.format(missing_file, command.script)
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
closest_branch = utils.get_closest(missing_file, get_branches(),
fallback_to_first=False)
if closest_branch:
return command.script.replace(missing_file, closest_branch, 1)
else:
return shells.and_('git branch {}', '{}').format(
missing_file, command.script)

View File

@@ -3,11 +3,10 @@ from thefuck import utils
@utils.git_support
def match(command, settings):
return ('git' in command.script and
'diff' in command.script and
return ('diff' in command.script and
'--staged' not in command.script)
@utils.git_support
def get_new_command(command, settings):
return '{} --staged'.format(command.script)
return command.script.replace(' diff', ' diff --staged')

View File

@@ -4,8 +4,7 @@ from thefuck.utils import get_closest, git_support
@git_support
def match(command, settings):
return ('git' in command.script
and " is not a git command. See 'git --help'." in command.stderr
return (" is not a git command. See 'git --help'." in command.stderr
and 'Did you mean' in command.stderr)

View File

@@ -3,8 +3,7 @@ from thefuck import shells, utils
@utils.git_support
def match(command, settings):
return ('git' in command.script
and 'pull' in command.script
return ('pull' in command.script
and 'set-upstream' in command.stderr)

View File

@@ -0,0 +1,12 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return ('fatal: Not a git repository' in command.stderr
and "Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)." in command.stderr)
@utils.git_support
def get_new_command(command, settings):
return command.script.replace(' pull ', ' clone ')

View File

@@ -3,8 +3,7 @@ from thefuck import utils
@utils.git_support
def match(command, settings):
return ('git' in command.script
and 'push' in command.script
return ('push' in command.script
and 'set-upstream' in command.stderr)

View File

@@ -0,0 +1,17 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return ('push' in command.script
and '! [rejected]' in command.stderr
and 'failed to push some refs to' in command.stderr
and 'Updates were rejected because the tip of your current branch is behind' in command.stderr)
@utils.git_support
def get_new_command(command, settings):
return command.script.replace('push', 'push --force')
enabled_by_default = False

View File

@@ -0,0 +1,16 @@
from thefuck import utils
from thefuck.shells import and_
@utils.git_support
def match(command, settings):
return ('push' in command.script
and '! [rejected]' in command.stderr
and 'failed to push some refs to' in command.stderr
and 'Updates were rejected because the tip of your current branch is behind' in command.stderr)
@utils.git_support
def get_new_command(command, settings):
return and_(command.script.replace('push', 'pull'),
command.script)

View File

@@ -5,7 +5,7 @@ from thefuck import shells, utils
def match(command, settings):
# catches "Please commit or stash them" and "Please, commit your changes or
# stash them before you can switch branches."
return 'git' in command.script and 'or stash them' in command.stderr
return 'or stash them' in command.stderr
@utils.git_support

View File

@@ -0,0 +1,20 @@
import re
from thefuck.utils import get_closest
def match(command, settings):
return command.script.startswith('heroku') and \
'is not a heroku command' in command.stderr and \
'Perhaps you meant' in command.stderr
def _get_suggests(stderr):
for line in stderr.split('\n'):
if 'Perhaps you meant' in line:
return re.findall(r'`([^`]+)`', line)
def get_new_command(command, settings):
wrong = re.findall(r'`(\w+)` is not a heroku command', command.stderr)[0]
correct = get_closest(wrong, _get_suggests(command.stderr))
return command.script.replace(' {}'.format(wrong), ' {}'.format(correct), 1)

View File

@@ -1,16 +1,27 @@
from difflib import get_close_matches
from thefuck.shells import get_history
from thefuck.utils import get_closest, memoize
from thefuck.rules.no_command import get_all_callables
from thefuck.shells import get_history, thefuck_alias
from thefuck.utils import get_closest, memoize, get_all_executables
def _not_corrected(history, tf_alias):
"""Returns all lines from history except that comes before `fuck`."""
previous = None
for line in history:
if previous is not None and line != tf_alias:
yield previous
previous = line
if history:
yield history[-1]
@memoize
def _history_of_exists_without_current(command):
callables = get_all_callables()
return [line for line in get_history()
if line != command.script
and line.split(' ')[0] in callables]
history = get_history()
tf_alias = thefuck_alias()
executables = get_all_executables()
return [line for line in _not_corrected(history, tf_alias)
if not line.startswith(tf_alias) and not line == command.script
and line.split(' ')[0] in executables]
def match(command, settings):
return len(get_close_matches(command.script,
@@ -21,4 +32,5 @@ def get_new_command(command, settings):
return get_closest(command.script,
_history_of_exists_without_current(command))
priority = 9999

View File

@@ -1,39 +1,19 @@
from difflib import get_close_matches
import os
from pathlib import Path
from thefuck.utils import sudo_support, memoize
from thefuck.shells import thefuck_alias, get_aliases
def _safe(fn, fallback):
try:
return fn()
except OSError:
return fallback
@memoize
def get_all_callables():
tf_alias = thefuck_alias()
return [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)] + [
alias for alias in get_aliases() if alias != tf_alias]
from thefuck.utils import sudo_support, get_all_executables
@sudo_support
def match(command, settings):
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
get_all_callables()))
get_all_executables()))
@sudo_support
def get_new_command(command, settings):
old_command = command.script.split(' ')[0]
new_command = get_close_matches(old_command,
get_all_callables())[0]
get_all_executables())[0]
return ' '.join([new_command] + command.script.split(' ')[1:])

View File

@@ -14,6 +14,7 @@ patterns = ['permission denied',
'need to be root',
'need root',
'only root can do that',
'You don\'t have access to the history DB.',
'authentication is required']

View File

@@ -1,4 +1,6 @@
# -*- encoding: utf-8 -*-
from thefuck.shells import thefuck_alias
from thefuck.utils import memoize
target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?'''
@@ -7,6 +9,7 @@ source_layouts = [u'''йцукенгшщзхъфывапролджэячсмит
u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?''']
@memoize
def _get_matched_layout(command):
for source_layout in source_layouts:
if all([ch in source_layout or ch in '-_'
@@ -14,10 +17,6 @@ def _get_matched_layout(command):
return source_layout
def match(command, settings):
return 'not found' in command.stderr and _get_matched_layout(command)
def _switch(ch, layout):
if ch in layout:
return target_layout[layout.index(ch)]
@@ -25,7 +24,18 @@ def _switch(ch, layout):
return ch
def _switch_command(command, layout):
return ''.join(_switch(ch, layout) for ch in command.script)
def match(command, settings):
if 'not found' not in command.stderr:
return False
matched_layout = _get_matched_layout(command)
return matched_layout and \
_switch_command(command, matched_layout) != thefuck_alias()
def get_new_command(command, settings):
matched_layout = _get_matched_layout(command)
return ''.join(_switch(ch, matched_layout) for ch in command.script)
return _switch_command(command, matched_layout)

View File

@@ -4,11 +4,11 @@ methods.
"""
from collections import defaultdict
from psutil import Process
from subprocess import Popen, PIPE
from time import time
import os
import io
from psutil import Process
import os
from .utils import DEVNULL, memoize
@@ -33,8 +33,8 @@ class Generic(object):
"""Prepares command for running in shell."""
return command_script
def app_alias(self):
return "\nalias fuck='TF_ALIAS=fuck eval $(thefuck $(fc -ln -1))'\n"
def app_alias(self, fuck):
return "alias {0}='TF_ALIAS={0} eval $(thefuck $(fc -ln -1))'".format(fuck)
def _get_history_file_name(self):
return ''
@@ -74,9 +74,9 @@ class Generic(object):
class Bash(Generic):
def app_alias(self):
return "\nTF_ALIAS=fuck alias fuck='eval $(thefuck $(fc -ln -1));" \
" history -r'\n"
def app_alias(self, fuck):
return "TF_ALIAS={0} alias {0}='eval $(thefuck $(fc -ln -1));" \
" history -r'".format(fuck)
def _parse_alias(self, alias):
name, value = alias.replace('alias ', '', 1).split('=', 1)
@@ -100,7 +100,6 @@ class Bash(Generic):
return u'{}\n'.format(command_script)
def _script_from_history(self, line):
print(line)
return line
@@ -113,9 +112,9 @@ class Fish(Generic):
else:
return ['cd', 'grep', 'ls', 'man', 'open']
def app_alias(self):
return ("set TF_ALIAS fuck\n"
"function fuck -d 'Correct your previous console command'\n"
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"
@@ -126,7 +125,7 @@ class Fish(Generic):
" if test $exit_code -ne 0\n"
" history --delete $fucked_up_commandd\n"
" end\n"
"end")
"end").format(fuck)
def get_aliases(self):
overridden = self._get_overridden_aliases()
@@ -158,10 +157,10 @@ class Fish(Generic):
class Zsh(Generic):
def app_alias(self):
return "\nTF_ALIAS=fuck" \
" alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" fc -R'\n"
def app_alias(self, fuck):
return "TF_ALIAS={0}" \
" alias {0}='eval $(thefuck $(fc -ln -1 | tail -n 1));" \
" fc -R'".format(fuck)
def _parse_alias(self, alias):
name, value = alias.split('=', 1)
@@ -192,8 +191,10 @@ class Zsh(Generic):
class Tcsh(Generic):
def app_alias(self):
return "\nalias fuck 'setenv TF_ALIAS fuck && set fucked_cmd=`history -h 2 | head -n 1` && eval `thefuck ${fucked_cmd}`'\n"
def app_alias(self, fuck):
return ("alias {0} 'setenv TF_ALIAS {0} && "
"set fucked_cmd=`history -h 2 | head -n 1` && "
"eval `thefuck ${fucked_cmd}`'").format(fuck)
def _parse_alias(self, alias):
name, value = alias.split("\t", 1)
@@ -239,8 +240,8 @@ def to_shell(command):
return _get_shell().to_shell(command)
def app_alias():
print(_get_shell().app_alias())
def app_alias(alias):
return _get_shell().app_alias(alias)
def thefuck_alias():

View File

@@ -1,5 +1,6 @@
from difflib import get_close_matches
from functools import wraps
from pathlib import Path
from shlex import split
import os
import pickle
@@ -74,12 +75,19 @@ def sudo_support(fn):
def git_support(fn):
"""Resolve git aliases."""
"""Resolves git aliases and supports testing for both git and hub."""
@wraps(fn)
def wrapper(command, settings):
if (command.script.startswith('git') and
'trace: alias expansion:' in command.stderr):
# supports GitHub's `hub` command
# which is recommended to be used with `alias git=hub`
# but at this point, shell aliases have already been resolved
is_git_cmd = command.script.startswith(('git', 'hub'))
if not is_git_cmd:
return False
# perform git aliases expansion
if 'trace: alias expansion:' in command.stderr:
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)",
command.stderr)
alias = search.group(1)
@@ -92,6 +100,7 @@ def git_support(fn):
new_script = command.script.replace(alias, expansion)
command = Command._replace(command, script=new_script)
return fn(command, settings)
return wrapper
@@ -113,10 +122,29 @@ def memoize(fn):
memoize.disabled = False
def get_closest(word, possibilities, n=3, cutoff=0.6):
def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True):
"""Returns closest match or just first from possibilities."""
possibilities = list(possibilities)
try:
return get_close_matches(word, possibilities, n, cutoff)[0]
except IndexError:
return possibilities[0]
if fallback_to_first:
return possibilities[0]
@memoize
def get_all_executables():
from thefuck.shells import thefuck_alias, get_aliases
def _safe(fn, fallback):
try:
return fn()
except OSError:
return fallback
tf_alias = thefuck_alias()
return [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)] + [
alias for alias in get_aliases() if alias != tf_alias]