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

Compare commits

..

71 Commits
1.47 ... 2.2

Author SHA1 Message Date
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
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
nvbn
4fb990742d Bump to 2.0 2015-07-19 22:33:56 +03:00
nvbn
cf3dca6f51 #284 Add coveralls support 2015-07-19 21:57:19 +03:00
nvbn
5187bada1b #N/A Update readme 2015-07-19 21:53:08 +03:00
nvbn
0238569b71 #N/A Require confirmation by default 2015-07-19 21:52:46 +03:00
nvbn
463b4fef2f Merge branch 'mcarton-git-aliases' 2015-07-19 21:29:39 +03:00
nvbn
f90bac10ed #290: Fix typo 2015-07-19 21:29:28 +03:00
nvbn
90014b2b05 Merge branch 'git-aliases' of https://github.com/mcarton/thefuck into mcarton-git-aliases 2015-07-19 21:27:04 +03:00
Vladimir Iakovlev
4276cacaf6 Merge pull request #292 from SimenB/delete-git-branch
Add git_branch_delete rule
2015-07-19 21:26:39 +03:00
Simen Bekkhus
b31aea3737 Add git_branch_delete rule 2015-07-19 13:45:46 +02:00
nvbn
fbfb4b5e41 Merge branch 'petr-tichy-master' 2015-07-18 17:19:57 +03:00
Petr Tichý
51c37bc5ab Fix wheel dependencies for Python 2 2015-07-17 18:51:35 +02:00
mcarton
5d0912fee8 Unquote over-quoted commands in @git_support
This allows writing rules more easily (eg. the git_branch_list rule
tests for `command.script.split() == 'git branch list'.split()`) and
looks nicer when `require_confirmation` is set.
2015-07-17 14:07:17 +02:00
mcarton
f6a4902074 Use @git_support in all git_* rules 2015-07-17 13:11:36 +02:00
mcarton
707d91200e Make the environment a setting
This would allow other rules to set the environment as needed for
`@git_support` and `GIT_TRACE`.
2015-07-17 11:37:13 +02:00
mcarton
b3e09d68df Start support for git aliases 2015-07-16 20:23:31 +02:00
nvbn
78769e4fbc Bump to 1.49 2015-07-15 07:49:18 +03:00
nvbn
3e4c043ccc #280: Add debug output 2015-07-15 07:47:54 +03:00
nvbn
934099fe9e #289: Add is a directory pattern to cp_omitting_directory rule 2015-07-15 07:12:07 +03:00
Vladimir Iakovlev
464f86eccf Merge pull request #288 from scorphus/overridden-aliases
fix(fish.get_aliases): do not include overridden aliases
2015-07-15 06:58:32 +03:00
Pablo Santiago Blum de Aguiar
891fbe7ed1 fix(fish.get_aliases): do not include overridden aliases
Fish Shell overrides some shell commands, such as `cd` and `ls` and
therefore some rules fail to match. The following aliases are excluded
by default:

 * cd
 * grep
 * ls
 * man
 * open

To change them, one can use the `TF_OVERRIDDEN_ALIASES` environment
variable such as:

```
set TF_OVERRIDDEN_ALIASES 'cd,grep,ls'
```

Fix #262
2015-07-13 22:53:15 -03:00
nvbn
5abab8bd1e Merge branch 'master' of github.com:nvbn/thefuck 2015-07-10 17:58:53 +03:00
nvbn
7ebc8a38af #N/A Add history rule 2015-07-10 17:58:41 +03:00
nvbn
f40b63f44b #N/A Add ability to disable memoization in tests 2015-07-10 17:06:05 +03:00
nvbn
4b4e7acc0f #N/A Add ability to get shell history 2015-07-10 16:42:21 +03:00
Vladimir Iakovlev
a8587d3871 Merge pull request #285 from mcarton/tmux
Use `get_closest` in the tmux rule
2015-07-10 15:54:14 +03:00
mcarton
370c58e679 Use get_closest in the tmux rule 2015-07-10 09:49:49 +02:00
Vladimir Iakovlev
328e65179e Merge pull request #283 from mcarton/mercurial
Some fixes in REAME.md
2015-07-09 20:13:38 +03:00
Vladimir Iakovlev
63bb4da8e1 Merge pull request #282 from mcarton/sudo
Add systemd's kind of error for the sudo rule
2015-07-09 20:13:17 +03:00
mcarton
0b5a7a8e2d Fix rule name in README 2015-07-09 18:35:33 +02:00
mcarton
5693bd49f7 #281 Add the mercurial rule to README.md 2015-07-09 18:01:44 +02:00
mcarton
12f8d017b9 Add systemd's kind of error for the sudo rule
A complete error would be:

```
% systemctl daemon-reload
==== AUTHENTICATING FOR org.freedesktop.systemd1.reload-daemon ===
Authentication is required to reload the systemd state.
Authenticating as: martin
Password:
```
2015-07-09 17:24:45 +02:00
nvbn
c7071763a3 Bump to 1.48 2015-07-08 21:34:39 +03:00
nvbn
27b5b9de6a #229 Use closest git command 2015-07-08 21:33:30 +03:00
nvbn
c0eae8b85c #N/A Add get_closest utility function 2015-07-08 21:30:24 +03:00
50 changed files with 842 additions and 237 deletions

View File

@@ -4,6 +4,11 @@ python:
- "3.3"
- "2.7"
install:
- python setup.py develop
- pip install -r requirements.txt
script: py.test -v
- 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
after_success: coveralls

View File

@@ -1,12 +1,12 @@
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck)
**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/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320).
Few examples:
![gif with examples](https://raw.githubusercontent.com/nvbn/thefuck/master/example.gif)
Few more examples:
```bash
➜ apt-get install vim
@@ -14,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
...
@@ -29,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.
...
```
@@ -42,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)
...
```
@@ -55,7 +55,7 @@ Did you mean this?
branch
➜ fuck
git branch
git branch [enter/ctrl+c]
* master
```
@@ -67,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
@@ -82,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
...
@@ -104,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`).
@@ -141,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
@@ -153,28 +138,33 @@ 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_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;
* `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;
* `no_command` – fixes wrong console commands, for example `vom/vim`;
* `no_such_file` – creates missing directories with `mv` and `cp` commands;
@@ -184,7 +174,7 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `python_execute` – appends missing `.py` when executing Python files;
* `quotation_marks` – fixes uneven usage of `'` and `"` when containing args'
* `rm_dir` – adds `-rf` when you trying to remove directory;
* `sed` – adds missing '/' to `sed`'s `s` commands;
* `sed_unterminated_s` – adds missing '/' to `sed`'s `s` commands;
* `sl_ls` – changes `sl` to `ls`;
* `ssh_known_hosts` – removes host from `known_hosts` on warning;
* `sudo` – prepends `sudo` to previous command if it failed because of permissions;
@@ -199,26 +189,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`:
@@ -237,7 +231,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),
@@ -248,10 +242,11 @@ priority = 1000 # Lower first
The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`:
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` – requires confirmation before running new command, by default `False`;
* `require_confirmation` – requires confirmation before running new command, by default `True`;
* `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.
* `priority` – dict with rules priorities, rule with lower `priority` will be matched first;
* `debug` – enables debug output, by default `False`.
Example of `settings.py`:
@@ -261,6 +256,7 @@ require_confirmation = True
wait_command = 10
no_colors = False
priority = {'sudo': 100, 'no_command': 9999}
debug = False
```
Or via environment variables:
@@ -270,7 +266,8 @@ Or via environment variables:
* `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`;
* `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`,
rule with lower `priority` will be matched first.
rule with lower `priority` will be matched first;
* `THEFUCK_DEBUG` – enables debug output, `true/false`.
For example:

BIN
example.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

View File

@@ -2,3 +2,4 @@ pytest
mock
pytest-mock
wheel
setuptools>=17.1

View File

@@ -11,12 +11,10 @@ elif (3, 0) < sys.version_info < (3, 3):
' ({}.{} detected).'.format(*sys.version_info[:2]))
sys.exit(-1)
VERSION = '1.47'
VERSION = '2.2'
install_requires = ['psutil', 'colorama', 'six']
if sys.version_info < (3, 4):
install_requires.append('pathlib')
extras_require = {':python_version<"3.4"': ['pathlib']}
setup(name='thefuck',
version=VERSION,
@@ -30,6 +28,7 @@ setup(name='thefuck',
include_package_data=True,
zip_safe=False,
install_requires=install_requires,
extras_require=extras_require,
entry_points={'console_scripts': [
'thefuck = thefuck.main:main',
'thefuck-alias = thefuck.shells:app_alias']})
'thefuck-alias = thefuck.main:print_alias']})

6
tests/conftest.py Normal file
View File

@@ -0,0 +1,6 @@
import pytest
@pytest.fixture
def no_memoize(monkeypatch):
monkeypatch.setattr('thefuck.utils.memoize.disabled', True)

View File

@@ -1,14 +1,22 @@
from mock import Mock
import pytest
from thefuck.rules.cp_omitting_directory import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Mock(script='cp dir', stderr="cp: omitting directory 'dir'"),
None)
assert not match(Mock(script='some dir',
stderr="cp: omitting directory 'dir'"), None)
assert not match(Mock(script='cp dir', stderr=""), None)
@pytest.mark.parametrize('script, stderr', [
('cp dir', 'cp: dor: is a directory'),
('cp dir', "cp: omitting directory 'dir'")])
def test_match(script, stderr):
assert match(Command(script, stderr=stderr), None)
@pytest.mark.parametrize('script, stderr', [
('some dir', 'cp: dor: is a directory'),
('some dir', "cp: omitting directory 'dir'"),
('cp dir', '')])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr), None)
def test_get_new_command():
assert get_new_command(Mock(script='cp dir'), None) == 'cp -a dir'
assert get_new_command(Command(script='cp dir'), None) == 'cp -a dir'

View File

@@ -0,0 +1,22 @@
import pytest
from thefuck.rules.git_branch_delete import match, get_new_command
from tests.utils import Command
@pytest.fixture
def stderr():
return '''error: The branch 'branch' is not fully merged.
If you are sure you want to delete it, run 'git branch -D branch'.
'''
def test_match(stderr):
assert match(Command('git branch -d branch', stderr=stderr), None)
assert not match(Command('git branch -d branch'), None)
assert not match(Command('ls', stderr=stderr), None)
def test_get_new_command(stderr):
assert get_new_command(Command('git branch -d branch', stderr=stderr), None)\
== "git branch -D branch"

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 amster', stderr=did_not_match('amster')),
'git checkout master'),
(['master'],
Command(script='git commit amster', stderr=did_not_match('amster')),
'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

@@ -4,14 +4,14 @@ from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='git diff'),
Command(script='git df'),
Command(script='git ds')])
Command(script='git diff foo'),
Command(script='git diff')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='git diff --staged'),
Command(script='git tag'),
Command(script='git branch'),
Command(script='git log')])
@@ -21,7 +21,6 @@ def test_not_match(command):
@pytest.mark.parametrize('command, new_command', [
(Command('git diff'), 'git diff --staged'),
(Command('git df'), 'git df --staged'),
(Command('git ds'), 'git ds --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

@@ -25,6 +25,16 @@ stats
"""
@pytest.fixture
def git_not_command_closest():
return '''git: 'tags' is not a git command. See 'git --help'.
Did you mean one of these?
stage
tag
'''
@pytest.fixture
def git_command():
return "* master"
@@ -37,8 +47,11 @@ def test_match(git_not_command, git_command, git_not_command_one_of_this):
assert not match(Command('git branch', stderr=git_command), None)
def test_get_new_command(git_not_command, git_not_command_one_of_this):
assert get_new_command(Command('git brnch', stderr=git_not_command), None)\
== 'git branch'
def test_get_new_command(git_not_command, git_not_command_one_of_this,
git_not_command_closest):
assert get_new_command(Command('git brnch', stderr=git_not_command), None) \
== 'git branch'
assert get_new_command(Command('git st', stderr=git_not_command_one_of_this),
None) == 'git status'
assert get_new_command(Command('git tags', stderr=git_not_command_closest),
None) == 'git tag'

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

@@ -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

@@ -3,22 +3,20 @@ from thefuck.rules.git_stash import match, get_new_command
from tests.utils import Command
@pytest.fixture
def cherry_pick_error():
return ('error: Your local changes would be overwritten by cherry-pick.\n'
'hint: Commit your changes or stash them to proceed.\n'
'fatal: cherry-pick failed')
cherry_pick_error = (
'error: Your local changes would be overwritten by cherry-pick.\n'
'hint: Commit your changes or stash them to proceed.\n'
'fatal: cherry-pick failed')
@pytest.fixture
def rebase_error():
return ('Cannot rebase: Your index contains uncommitted changes.\n'
'Please commit or stash them.')
rebase_error = (
'Cannot rebase: Your index contains uncommitted changes.\n'
'Please commit or stash them.')
@pytest.mark.parametrize('command', [
Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error()),
Command(script='git rebase -i HEAD~7', stderr=rebase_error())])
Command(script='git cherry-pick a1b2c3d', stderr=cherry_pick_error),
Command(script='git rebase -i HEAD~7', stderr=rebase_error)])
def test_match(command):
assert match(command, None)

View File

@@ -0,0 +1,42 @@
import pytest
from thefuck.rules.history import match, get_new_command
from tests.utils import Command
@pytest.fixture
def history(mocker):
return mocker.patch('thefuck.rules.history.get_history',
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_executables',
return_value=['diff', 'ls'])
@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', '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', 'alias')
@pytest.mark.parametrize('script, result', [
('ls cet', 'ls cat'),
('daff x', 'diff x')])
def test_get_new_command(script, result):
assert get_new_command(Command(script), None) == result

View File

@@ -1,36 +1,29 @@
from mock import patch, Mock
from thefuck.rules.no_command import match, get_new_command, _get_all_callables
import pytest
from thefuck.rules.no_command import match, get_new_command
from tests.utils import Command
@patch('thefuck.rules.no_command._safe', return_value=[])
@patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
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.fixture(autouse=True)
def get_all_executables(mocker):
mocker.patch('thefuck.rules.no_command.get_all_executables',
return_value=['vim', 'apt-get', 'fsck'])
@patch('thefuck.rules.no_command._safe', return_value=[])
@patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
def test_match(*args):
assert match(Mock(stderr='vom: not found', script='vom file.py'), None)
assert match(Mock(stderr='fucck: not found', script='fucck'), None)
assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None)
assert not match(Mock(stderr='some text', script='vom file.py'), None)
@pytest.mark.usefixtures('no_memoize')
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)
assert not match(Command(stderr='some text', script='vom file.py'), None)
@patch('thefuck.rules.no_command._safe', return_value=[])
@patch('thefuck.rules.no_command.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
def test_get_new_command(*args):
@pytest.mark.usefixtures('no_memoize')
def test_get_new_command():
assert get_new_command(
Mock(stderr='vom: not found',
script='vom file.py'),
Command(stderr='vom: not found',
script='vom file.py'),
None) == 'vim file.py'
assert get_new_command(
Mock(stderr='fucck: not found',
script='fucck'),
None) == 'fsck'
Command(stderr='fucck: not found',
script='fucck'),
Command) == 'fsck'

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

@@ -16,4 +16,4 @@ def test_match(tmux_ambiguous):
def test_get_new_command(tmux_ambiguous):
assert get_new_command(Command('tmux list', stderr=tmux_ambiguous), None)\
== 'tmux list-buffers'
== 'tmux list-keys'

View File

@@ -5,3 +5,10 @@ from thefuck import logs
def test_color():
assert logs.color('red', Mock(no_colors=False)) == 'red'
assert logs.color('red', Mock(no_colors=True)) == ''
def test_debug(capsys):
logs.debug('test', Mock(no_colors=True, debug=True))
assert capsys.readouterr() == ('', 'DEBUG: test\n')
logs.debug('test', Mock(no_colors=True, debug=False))
assert capsys.readouterr() == ('', '')

View File

@@ -77,23 +77,23 @@ class TestGetCommand(object):
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_get_command_calls(self, Popen):
assert main.get_command(Mock(),
assert main.get_command(Mock(env={}),
['thefuck', 'apt-get', 'search', 'vim']) \
== Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdout=PIPE,
stderr=PIPE,
env={'LANG': 'C'})
env={})
@pytest.mark.parametrize('args, result', [
(['thefuck', 'ls', '-la'], 'ls -la'),
(['thefuck', 'ls'], 'ls')])
def test_get_command_script(self, args, result):
if result:
assert main.get_command(Mock(), args).script == result
assert main.get_command(Mock(env={}), args).script == result
else:
assert main.get_command(Mock(), args) is None
assert main.get_command(Mock(env={}), args) is None
class TestGetMatchedRule(object):
@@ -110,7 +110,7 @@ class TestGetMatchedRule(object):
def test_when_rule_failed(self, capsys):
main.get_matched_rule(
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))],
Mock(no_colors=True))
Mock(no_colors=True, debug=False))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
@@ -126,7 +126,7 @@ class TestRunRule(object):
def test_run_rule_with_side_effect(self, capsys):
side_effect = Mock()
settings = Mock()
settings = Mock(debug=False)
command = Command()
main.run_rule(Rule(get_new_command=lambda *_: 'new-command',
side_effect=side_effect),

View File

@@ -12,6 +12,16 @@ def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True)
@pytest.fixture
@pytest.mark.usefixtures('isfile')
def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__\
.return_value.__iter__.return_value = lines
return aux
class TestGeneric(object):
@pytest.fixture
def shell(self):
@@ -34,9 +44,16 @@ 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'])
# We don't know what to do in generic shell with history lines,
# so just ignore them:
assert list(shell.get_history()) == []
@pytest.mark.usefixtures('isfile')
@@ -81,9 +98,14 @@ 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'])
assert list(shell.get_history()) == ['ls', 'rm']
@pytest.mark.usefixtures('isfile')
@@ -96,18 +118,34 @@ class TestFish(object):
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'fish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nmath')
b'cd\nfish_config\nfuck\nfunced\nfuncsave\ngrep\nhistory\nll\nls\n'
b'man\nmath\npopd\npushd\nruby')
return mock
@pytest.fixture
def environ(self, monkeypatch):
data = {'TF_OVERRIDDEN_ALIASES': 'cd, ls, man, open'}
monkeypatch.setattr('thefuck.shells.os.environ', data)
return data
@pytest.mark.usefixture('environ')
def test_get_overridden_aliases(self, shell, environ):
assert shell._get_overridden_aliases() == ['cd', 'ls', 'man', 'open']
@pytest.mark.parametrize('before, after', [
('cd', 'cd'),
('pwd', 'pwd'),
('fuck', 'fish -ic "fuck"'),
('find', 'find'),
('funced', 'fish -ic "funced"'),
('grep', 'grep'),
('awk', 'awk'),
('math "2 + 2"', r'fish -ic "math \"2 + 2\""'),
('man', 'man'),
('open', 'open'),
('vim', 'vim'),
('ll', 'fish -ic "ll"')]) # Fish has no aliases but functions
('ll', 'fish -ic "ll"'),
('ls', 'ls')]) # Fish has no aliases but functions
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
@@ -129,15 +167,18 @@ class TestFish(object):
'fuck': 'fuck',
'funced': 'funced',
'funcsave': 'funcsave',
'grep': 'grep',
'history': 'history',
'll': 'll',
'math': 'math'}
'math': 'math',
'popd': 'popd',
'pushd': 'pushd',
'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')
@@ -184,6 +225,11 @@ 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'])
assert list(shell.get_history()) == ['ls', 'rm']

View File

@@ -1,6 +1,7 @@
import pytest
from mock import Mock
from thefuck.utils import sudo_support, wrap_settings, memoize
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
@@ -26,9 +27,54 @@ def test_sudo_support(return_value, command, called, result):
fn.assert_called_once_with(Command(called), None)
@pytest.mark.parametrize('called, command, stderr', [
('git co', 'git checkout', "19:22:36.299340 git.c:282 trace: alias expansion: co => 'checkout'"),
('git com file', 'git commit --verbose file', "19:23:25.470911 git.c:282 trace: alias expansion: com => 'commit' '--verbose'")])
def test_git_support(called, command, stderr):
@git_support
def fn(command, settings): return command.script
assert fn(Command(script=called, stderr=stderr), None) == command
def test_memoize():
fn = Mock(__name__='fn')
memoized = memoize(fn)
memoized()
memoized()
fn.assert_called_once_with()
@pytest.mark.usefixtures('no_memoize')
def test_no_memoize():
fn = Mock(__name__='fn')
memoized = memoize(fn)
memoized()
memoized()
assert fn.call_count == 2
class TestGetClosest(object):
def test_when_can_match(self):
assert 'branch' == get_closest('brnch', ['branch', 'status'])
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

@@ -27,15 +27,18 @@ DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3,
'require_confirmation': False,
'require_confirmation': True,
'no_colors': False,
'priority': {}}
'debug': False,
'priority': {},
'env': {'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_PRIORITY': 'priority'}
'THEFUCK_PRIORITY': 'priority',
'THEFUCK_DEBUG': 'debug'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
@@ -87,7 +90,7 @@ def _val_from_env(env, attr):
return dict(_priority_from_env(val))
elif attr == 'wait_command':
return int(val)
elif attr in ('require_confirmation', 'no_colors'):
elif attr in ('require_confirmation', 'no_colors', 'debug'):
return val.lower() == 'true'
else:
return val

View File

@@ -1,3 +1,4 @@
from pprint import pformat
import sys
from traceback import format_exception
import colorama
@@ -52,3 +53,12 @@ def failed(msg, settings):
msg=msg,
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def debug(msg, settings):
if settings.debug:
sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format(
msg=msg,
reset=color(colorama.Style.RESET_ALL, settings),
blue=color(colorama.Fore.BLUE, settings),
bold=color(colorama.Style.BRIGHT, settings)))

View File

@@ -1,6 +1,7 @@
from imp import load_source
from pathlib import Path
from os.path import expanduser
from pprint import pformat
from subprocess import Popen, PIPE
import os
import sys
@@ -79,8 +80,13 @@ def get_command(settings, args):
return
script = shells.from_shell(script)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C'))
logs.debug(u'Call: {}'.format(script), settings)
env = dict(os.environ)
env.update(settings.env)
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):
return types.Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8'))
@@ -90,6 +96,7 @@ def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command."""
for rule in rules:
try:
logs.debug(u'Trying rule: {}'.format(rule.name), settings)
if rule.match(command, settings):
return rule
except Exception:
@@ -121,17 +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(u'Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv)
if command:
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(
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(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

@@ -1,9 +1,10 @@
import difflib
import os
import re
from subprocess import check_output
from thefuck.utils import get_closest
# Formulars are base on each local system's status
brew_formulas = []
try:
brew_path_prefix = check_output(['brew', '--prefix'],
@@ -17,8 +18,8 @@ except:
pass
def _get_similar_formulars(formula_name):
return difflib.get_close_matches(formula_name, brew_formulas, 1, 0.85)
def _get_similar_formula(formula_name):
return get_closest(formula_name, brew_formulas, 1, 0.85)
def match(command, settings):
@@ -29,7 +30,7 @@ def match(command, settings):
if is_proper_command:
formula = re.findall(r'Error: No available formula for ([a-z]+)',
command.stderr)[0]
has_possible_formulas = len(_get_similar_formulars(formula)) > 0
has_possible_formulas = bool(_get_similar_formula(formula))
return has_possible_formulas
@@ -37,6 +38,6 @@ def match(command, settings):
def get_new_command(command, settings):
not_exist_formula = re.findall(r'Error: No available formula for ([a-z]+)',
command.stderr)[0]
exist_formula = _get_similar_formulars(not_exist_formula)[0]
exist_formula = _get_similar_formula(not_exist_formula)
return command.script.replace(not_exist_formula, exist_formula, 1)

View File

@@ -1,8 +1,7 @@
import difflib
import os
import re
import subprocess
from thefuck.utils import get_closest
BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps'
@@ -78,8 +77,8 @@ if brew_path_prefix:
pass
def _get_similar_commands(command):
return difflib.get_close_matches(command, brew_commands)
def _get_similar_command(command):
return get_closest(command, brew_commands)
def match(command, settings):
@@ -90,7 +89,7 @@ def match(command, settings):
if is_proper_command:
broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)',
command.stderr)[0]
has_possible_commands = len(_get_similar_commands(broken_cmd)) > 0
has_possible_commands = bool(_get_similar_command(broken_cmd))
return has_possible_commands
@@ -98,6 +97,6 @@ def match(command, settings):
def get_new_command(command, settings):
broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)',
command.stderr)[0]
new_cmd = _get_similar_commands(broken_cmd)[0]
new_cmd = _get_similar_command(broken_cmd)
return command.script.replace(broken_cmd, new_cmd, 1)

View File

@@ -4,8 +4,9 @@ from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
stderr = command.stderr.lower()
return command.script.startswith('cp ') \
and 'cp: omitting directory' in command.stderr.lower()
and ('omitting directory' in stderr or 'is a directory' in stderr)
@sudo_support

View File

@@ -1,13 +1,15 @@
import re
from thefuck import shells
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
and "Did you forget to 'git add'?" in command.stderr)
@utils.git_support
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "

View File

@@ -0,0 +1,12 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return ('git 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

@@ -1,10 +1,12 @@
from thefuck import shells
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()
@utils.git_support
def get_new_command(command, settings):
return shells.and_('git branch --delete list', 'git branch')

View File

@@ -1,17 +1,37 @@
import re
from thefuck import shells
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
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

@@ -1,6 +1,13 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return command.script.startswith('git d')
return ('git' in command.script and
'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

@@ -1,16 +1,28 @@
import re
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
and 'Did you mean' in command.stderr)
def _get_all_git_matched_commands(stderr):
should_yield = False
for line in stderr.split('\n'):
if 'Did you mean' in line:
should_yield = True
elif should_yield and line:
yield line.strip()
@git_support
def get_new_command(command, settings):
broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
command.stderr)[0]
new_cmd = re.findall(r'Did you mean[^\n]*\n\s*([^\n]*)',
command.stderr)[0]
new_cmd = get_closest(broken_cmd,
_get_all_git_matched_commands(command.stderr))
return command.script.replace(broken_cmd, new_cmd, 1)

View File

@@ -1,12 +1,14 @@
from thefuck import shells
from thefuck import shells, utils
@utils.git_support
def match(command, settings):
return ('git' in command.script
and 'pull' in command.script
and 'set-upstream' in command.stderr)
@utils.git_support
def get_new_command(command, settings):
line = command.stderr.split('\n')[-3].strip()
branch = line.split(' ')[-1]

View File

@@ -0,0 +1,14 @@
import re
from thefuck import utils, shells
@utils.git_support
def match(command, settings):
return ('git pull' in command.script
and '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

@@ -1,8 +1,13 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return ('git' in command.script
and 'push' in command.script
and 'set-upstream' in command.stderr)
@utils.git_support
def get_new_command(command, settings):
return command.stderr.split('\n')[-3].strip()

View File

@@ -0,0 +1,18 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return ('git' in command.script
and '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,17 @@
from thefuck import utils
from thefuck.shells import and_
@utils.git_support
def match(command, settings):
return ('git' in command.script
and '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

@@ -1,12 +1,14 @@
from thefuck import shells
from thefuck import shells, utils
@utils.git_support
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
@utils.git_support
def get_new_command(command, settings):
formatme = shells.and_('git stash', '{}')
return formatme.format(command.script)

36
thefuck/rules/history.py Normal file
View File

@@ -0,0 +1,36 @@
from difflib import get_close_matches
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):
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,
_history_of_exists_without_current(command)))
def get_new_command(command, settings):
return get_closest(command.script,
_history_of_exists_without_current(command))
priority = 9999

View File

@@ -1,6 +1,5 @@
import re
from difflib import get_close_matches
from thefuck.utils import get_closest
def extract_possisiblities(command):
@@ -26,9 +25,5 @@ def match(command, settings):
def get_new_command(command, settings):
script = command.script.split(' ')
possisiblities = extract_possisiblities(command)
matches = get_close_matches(script[1], possisiblities)
if matches:
script[1] = matches[0]
else:
script[1] = possisiblities[0]
script[1] = get_closest(script[1], possisiblities)
return ' '.join(script)

View File

@@ -1,38 +1,19 @@
from difflib import get_close_matches
import os
from pathlib import Path
from thefuck.utils import sudo_support
from thefuck.shells import thefuck_alias, get_aliases
def _safe(fn, fallback):
try:
return fn()
except OSError:
return fallback
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

@@ -13,7 +13,9 @@ patterns = ['permission denied',
'must be root',
'need to be root',
'need root',
'only root can do that']
'only root can do that',
'You don\'t have access to the history DB.',
'authentication is required']
def match(command, settings):

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

@@ -1,3 +1,4 @@
from thefuck.utils import get_closest
import re
@@ -8,7 +9,12 @@ def match(command, settings):
def get_new_command(command, settings):
cmd = re.match(r"ambiguous command: (.*), could be: ([^, \n]*)",
cmd = re.match(r"ambiguous command: (.*), could be: (.*)",
command.stderr)
return command.script.replace(cmd.group(1), cmd.group(2))
old_cmd = cmd.group(1)
suggestions = [cmd.strip() for cmd in cmd.group(2).split(',')]
new_cmd = get_closest(old_cmd, suggestions)
return command.script.replace(old_cmd, new_cmd)

View File

@@ -4,10 +4,11 @@ methods.
"""
from collections import defaultdict
from psutil import Process
from subprocess import Popen, PIPE
from time import time
import io
import os
from psutil import Process
from .utils import DEVNULL, memoize
@@ -32,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 ''
@@ -48,14 +49,34 @@ class Generic(object):
with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script))
def _script_from_history(self, line):
"""Returns prepared history line.
Should return a blank line if history line is corrupted or empty.
"""
return ''
def get_history(self):
"""Returns list of history entries."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with io.open(history_file_name, 'r',
encoding='utf-8', errors='ignore') as history:
for line in history:
prepared = self._script_from_history(line)\
.strip()
if prepared:
yield prepared
def and_(self, *commands):
return u' && '.join(commands)
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)
@@ -63,7 +84,6 @@ class Bash(Generic):
value = value[1:-1]
return name, value
@memoize
def get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
@@ -79,12 +99,23 @@ class Bash(Generic):
def _get_history_line(self, command_script):
return u'{}\n'.format(command_script)
def _script_from_history(self, line):
return line
class Fish(Generic):
def app_alias(self):
return ("function fuck -d 'Correct your previous console command'\n"
def _get_overridden_aliases(self):
overridden_aliases = os.environ.get('TF_OVERRIDDEN_ALIASES', '').strip()
if overridden_aliases:
return [alias.strip() for alias in overridden_aliases.split(',')]
else:
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 TF_ALIAS fuck\n"
" set -l eval_script"
" (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n"
" set -l fucked_up_commandd $history[1]\n"
@@ -94,14 +125,14 @@ class Fish(Generic):
" if test $exit_code -ne 0\n"
" history --delete $fucked_up_commandd\n"
" end\n"
"end")
"end").format(fuck)
@memoize
def get_aliases(self):
overridden = self._get_overridden_aliases()
proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL,
shell=True)
functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {function: function for function in functions}
return {func: func for func in functions if func not in overridden}
def _expand_aliases(self, command_script):
aliases = self.get_aliases()
@@ -126,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)
@@ -137,7 +168,6 @@ class Zsh(Generic):
value = value[1:-1]
return name, value
@memoize
def get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
@@ -153,16 +183,23 @@ class Zsh(Generic):
def _get_history_line(self, command_script):
return u': {}:0;{}\n'.format(int(time()), command_script)
def _script_from_history(self, line):
if ';' in line:
return line.split(';', 1)[1]
else:
return ''
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)
return name, value
@memoize
def get_aliases(self):
proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
@@ -203,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():
@@ -219,5 +256,11 @@ def and_(*commands):
return _get_shell().and_(*commands)
@memoize
def get_aliases():
return list(_get_shell().get_aliases().keys())
@memoize
def get_history():
return list(_get_shell().get_history())

View File

@@ -1,6 +1,10 @@
from difflib import get_close_matches
from functools import wraps
from pathlib import Path
from shlex import split
import os
import pickle
import re
import six
from .types import Command
@@ -8,11 +12,9 @@ from .types import Command
DEVNULL = open(os.devnull, 'w')
if six.PY2:
import pipes
quote = pipes.quote
from pipes import quote
else:
import shlex
quote = shlex.quote
from shlex import quote
def which(program):
@@ -72,6 +74,30 @@ def sudo_support(fn):
return wrapper
def git_support(fn):
"""Resolve git aliases."""
@wraps(fn)
def wrapper(command, settings):
if (command.script.startswith('git') and
'trace: alias expansion:' in command.stderr):
search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)",
command.stderr)
alias = search.group(1)
# by default git quotes everything, for example:
# 'commit' '--amend'
# which is surprising and does not allow to easily test for
# eg. 'git commit'
expansion = ' '.join(map(quote, split(search.group(2))))
new_script = command.script.replace(alias, expansion)
command = Command._replace(command, script=new_script)
return fn(command, settings)
return wrapper
def memoize(fn):
"""Caches previous calls to the function."""
memo = {}
@@ -79,9 +105,38 @@ def memoize(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs))
if key not in memo:
if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs)
return memo[key]
return wrapper
memoize.disabled = False
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:
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]