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

Compare commits

...

136 Commits
1.48 ... 2.5.6

Author SHA1 Message Date
nvbn
44cd1fd7e1 #311 Fix installation without pandoc 2015-07-27 23:31:06 +03:00
nvbn
dc16600871 #311 Manually convert md to rst 2015-07-27 23:23:26 +03:00
nvbn
af40ad84d8 Bump to 2.5 2015-07-27 22:23:26 +03:00
nvbn
63e62fcba3 #311 Use setuptools-markdown 2015-07-27 22:23:20 +03:00
nvbn
368be788d7 Fix tests in python 2 2015-07-27 17:51:33 +03:00
nvbn
cd1468489f Fix history tests in travis-ci? 2015-07-27 17:47:02 +03:00
nvbn
fbce86b92a Merge branch 'mcarton-unzip-clean' 2015-07-27 17:40:04 +03:00
nvbn
3f6652df66 #313 Add new command options to readme 2015-07-27 17:39:52 +03:00
nvbn
cf82af8978 #313 Remove types.Script, use Command with None as stdout and stderr 2015-07-27 17:39:41 +03:00
nvbn
20f51f5ffe Merge branch 'unzip-clean' of https://github.com/mcarton/thefuck into mcarton-unzip-clean 2015-07-27 17:29:09 +03:00
nvbn
8f6d8b1dd1 Add tests for history changes fro bash and zsh 2015-07-27 17:28:09 +03:00
Vladimir Iakovlev
c0002fe6e0 Merge pull request #317 from SanketDG/setup_fix
fix setup.py version checking
2015-07-27 01:05:25 +03:00
Vladimir Iakovlev
6609b8d06a #316 Remove .py from tsuru_login rule name 2015-07-26 22:09:55 +03:00
Vladimir Iakovlev
5b5df9361d Merge pull request #316 from scorphus/tsuru-login
Add `tsuru_login` rule
2015-07-26 22:08:51 +03:00
Vladimir Iakovlev
fa234fde70 Merge pull request #315 from scorphus/fix-tests
Fix git_push_pull and not_match tests
2015-07-26 22:08:09 +03:00
SanketDG
867aec83c3 fix setup.py version checking 2015-07-26 23:47:56 +05:30
Pablo Santiago Blum de Aguiar
2117659c40 Add tsuru_login rule 2015-07-25 23:33:38 -03:00
Pablo Santiago Blum de Aguiar
4985f75d74 Allow generic_shell to act while testing git_push_pull
Fix failing tests on shells that do not use && operator
2015-07-25 23:26:52 -03:00
Pablo Santiago Blum de Aguiar
959d20df78 Add test_not_match to no_such_file tests 2015-07-25 23:26:47 -03:00
mcarton
8529461742 Update README 2015-07-25 23:14:10 +02:00
mcarton
3173ef10c6 Change the message when expecting side effect
The previous behavior is really surprising:
```
    some_command* [enter/ctrl+c]
   |<~~~~~~~~~~~>|<~~~~~~~~~~~~>|
   |  bold text  | normal weight|
```
as if the '*' is part of the command to be executed.
The new behavior is:
```
    some_command (+side effect) [enter/ctrl+c]
   |<~~~~~~~~~~>|<~~~~~~~~~~~~~~~~~~~~~~~~~~~>|
   |  bold text |        normal weight        |
```
2015-07-25 23:10:21 +02:00
mcarton
1c5fef3a34 Add tests for the dirty_untar rule 2015-07-25 23:06:20 +02:00
mcarton
386e6bf0c3 Add the dirty_tar rule 2015-07-25 23:06:09 +02:00
mcarton
1146ab654c Add tests for the dirty_unzip rule 2015-07-25 23:06:00 +02:00
mcarton
4e7eceaa3a Add a dirty_unzip rule 2015-07-25 23:05:06 +02:00
mcarton
71bb1994c3 Allow rules to correct commands that time out 2015-07-25 23:04:08 +02:00
nvbn
bfa3c905a3 Improve assertions in func tests 2015-07-25 21:02:04 +03:00
nvbn
992f488159 Bump to 2.4 2015-07-25 03:40:06 +03:00
nvbn
7770efb86c Fix skipif on fish tests 2015-07-25 03:38:17 +03:00
nvbn
b2457d1587 Fix skipif on fish tests 2015-07-25 03:35:55 +03:00
nvbn
2291a5ba5d Use only one skipif 2015-07-25 03:33:30 +03:00
nvbn
129d67f794 Temporary disable functional tests with fish in travis-ci
https://github.com/travis-ci/apt-source-whitelist/issues/71
2015-07-25 03:30:11 +03:00
nvbn
d00295f9d8 Fix fish version in travis-ci 2015-07-25 03:22:16 +03:00
nvbn
8498b970cc Fix tests with python 2 2015-07-25 03:22:05 +03:00
nvbn
8d981cf9b6 Fix env in travis config 2015-07-25 03:02:04 +03:00
nvbn
2da3d02361 Add BARE option for running functional tests without docker 2015-07-25 03:01:03 +03:00
nvbn
d7c8a43bbb Merge branch 'master' of github.com:nvbn/thefuck 2015-07-24 23:50:30 +03:00
nvbn
14e4158c7a Add tests for tcsh, fix tcsh alias 2015-07-24 23:50:22 +03:00
Vladimir Iakovlev
0d378ccf28 Merge pull request #312 from SanketDG/desc
fix description not appearing on pypi page
2015-07-24 23:32:45 +03:00
Vladimir Iakovlev
ff117f2d69 Merge pull request #310 from mcarton/patch-2
Make a `sudo` pattern more general
2015-07-24 23:32:12 +03:00
nvbn
41350d13a8 Revert "#N/A Run functional tests in travis-ci"
This reverts commit 9e79c4aea3.
2015-07-24 23:31:21 +03:00
nvbn
09a4438d69 Revert "#N/A Run functional tests in travis-ci"
This reverts commit c6ec2df85b.
2015-07-24 23:31:16 +03:00
nvbn
c6ec2df85b #N/A Run functional tests in travis-ci 2015-07-24 23:27:04 +03:00
nvbn
9e79c4aea3 #N/A Run functional tests in travis-ci 2015-07-24 23:24:14 +03:00
nvbn
9ab4491b96 #N/A Add tests for "ctrl+c" 2015-07-24 23:14:58 +03:00
SanketDG
fb8174b5e5 fix description not appearing on pypi page 2015-07-24 22:04:12 +05:30
Martin Carton
aaa66b6268 Make a sudo pattern more general
```
% mount -o uid=martin /dev/sdc1 mnt
mount: only root can use "--options" option
```
2015-07-24 16:33:53 +02:00
nvbn
174ada054d #N/A Implicitly prefix containers names 2015-07-24 08:09:08 +03:00
nvbn
e1416a0127 #N/A Add tests for fish 2015-07-24 08:04:49 +03:00
nvbn
c34a56bc89 #N/A Simplify functional tests 2015-07-24 07:38:45 +03:00
nvbn
7906025cc6 #N/A Add docker-based functional tests 2015-07-24 03:56:21 +03:00
nvbn
b15bc8c423 #N/A Add gulp_not_task rule 2015-07-24 00:47:57 +03:00
nvbn
469c5a60b0 #N/A Add replace_argument helper 2015-07-24 00:39:56 +03:00
nvbn
f9f0948349 #N/A Add docker_not_command rule 2015-07-24 00:12:29 +03:00
nvbn
b5f2d0afb5 #N/A Use get_closest in no_command rule 2015-07-23 23:42:29 +03:00
nvbn
ef2f642ffe #N/A Log common operations time 2015-07-23 06:09:57 +03:00
Vladimir Iakovlev
ca77261b89 Merge pull request #309 from mcarton/git
Add the `git_fix_stash` rule
2015-07-23 05:22:18 +03:00
mcarton
e4da8a2e5a Add the git_fix_stash rule 2015-07-22 23:27:53 +02:00
nvbn
ab1cd665cd #N/A Fix git_checkout tests 2015-07-22 04:52:52 +03:00
nvbn
a6c5b8322a #N/A Install coverall before project deps 2015-07-22 04:49:33 +03:00
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
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
79 changed files with 1832 additions and 275 deletions

View File

@@ -3,7 +3,23 @@ python:
- "3.4"
- "3.3"
- "2.7"
addons:
apt:
sources:
- fish-shell/release-2
packages:
- bash
- zsh
- fish
- tcsh
env:
- FUNCTIONAL=true BARE=true
install:
- python setup.py develop
- pip install coveralls
- pip install -r requirements.txt
script: py.test -v
- python setup.py develop
- rm -rf build
script:
- export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1}
- coverage run --source=thefuck,tests -m py.test -v --capture=sys
after_success: coveralls

133
README.md
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)](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,45 +138,57 @@ using the matched rule and runs it. Rules enabled by default are as follows:
* `cd_parent` &ndash; changes `cd..` to `cd ..`;
* `composer_not_command` &ndash; fixes composer command name;
* `cp_omitting_directory` &ndash; adds `-a` when you `cp` directory;
* `cpp11` &ndash; add missing `-std=c++11` to `g++` or `clang++`;
* `cpp11` &ndash; adds missing `-std=c++11` to `g++` or `clang++`;
* `dirty_untar` &ndash; fixes `tar x` command that untarred in the current directory;
* `dirty_unzip` &ndash; fixes `unzip` command that unzipped in the current directory;
* `django_south_ghost` &ndash; adds `--delete-ghost-migrations` to failed because ghosts django south migration;
* `django_south_merge` &ndash; adds `--merge` to inconsistent django south migration;
* `dry` &ndash; fix repetitions like "git git push";
* `docker_not_command` &ndash; fixes wrong docker commands like `docker tags`;
* `dry` &ndash; fixes repetitions like `git git push`;
* `fix_alt_space` &ndash; replaces Alt+Space with Space character;
* `git_add` &ndash; fix *"Did you forget to 'git add'?"*;
* `git_add` &ndash; fixes *"Did you forget to 'git add'?"*;
* `git_branch_delete` &ndash; changes `git branch -d` to `git branch -D`;
* `git_branch_list` &ndash; catches `git branch list` in place of `git branch` and removes created branch;
* `git_checkout` &ndash; creates the branch before checking-out;
* `git_checkout` &ndash; fixes branch name or creates new branch;
* `git_diff_staged` &ndash; adds `--staged` to previous `git diff` with unexpected output;
* `git_no_command` &ndash; fixes wrong git commands like `git brnch`;
* `git_fix_stash` &ndash; fixes `git stash` commands (misspelled subcommand and missing `save`);
* `git_not_command` &ndash; fixes wrong git commands like `git brnch`;
* `git_pull` &ndash; sets upstream before executing previous `git pull`;
* `git_pull_clone` &ndash; clones instead of pulling when the repo does not exist;
* `git_push` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`;
* `git_push_pull` &ndash; runs `git pull` when `push` was rejected;
* `git_stash` &ndash; stashes you local modifications before rebasing or switching branch;
* `go_run` &ndash; appends `.go` extension when compiling/running Go programs
* `grep_recursive` &ndash; adds `-r` when you trying to grep directory;
* `grep_recursive` &ndash; adds `-r` when you trying to `grep` directory;
* `gulp_not_task` &ndash; fixes misspelled gulp tasks;
* `has_exists_script` &ndash; prepends `./` when script/binary exists;
* `heroku_no_command` &ndash; fixes wrong `heroku` commands like `heroku log`;
* `history` &ndash; tries to replace command with most similar command from history;
* `java` &ndash; removes `.java` extension when running Java programs;
* `javac` &ndash; appends missing `.java` when compiling Java files;
* `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`;
* `ls_lah` &ndash; adds -lah to ls;
* `man` &ndash; change manual section;
* `ls_lah` &ndash; adds `-lah` to `ls`;
* `man` &ndash; changes manual section;
* `man_no_space` &ndash; fixes man commands without spaces, for example `mandiff`;
* `mercurial` &ndash; fixes wrong `hg` commands;
* `mkdir_p` &ndash; adds `-p` when you trying to create directory without parent;
* `no_command` &ndash; fixes wrong console commands, for example `vom/vim`;
* `no_such_file` &ndash; creates missing directories with `mv` and `cp` commands;
* `open` &ndash; prepends `http` to address passed to `open`;
* `pip_unknown_command` &ndash; fixes wrong pip commands, for example `pip instatl/pip install`;
* `pip_unknown_command` &ndash; fixes wrong `pip` commands, for example `pip instatl/pip install`;
* `python_command` &ndash; prepends `python` when you trying to run not executable/without `./` python script;
* `python_execute` &ndash; appends missing `.py` when executing Python files;
* `quotation_marks` &ndash; fixes uneven usage of `'` and `"` when containing args'
* `rm_dir` &ndash; adds `-rf` when you trying to remove directory;
* `sed` &ndash; adds missing '/' to `sed`'s `s` commands;
* `sed_unterminated_s` &ndash; adds missing '/' to `sed`'s `s` commands;
* `sl_ls` &ndash; changes `sl` to `ls`;
* `ssh_known_hosts` &ndash; removes host from `known_hosts` on warning;
* `sudo` &ndash; prepends `sudo` to previous command if it failed because of permissions;
* `switch_layout` &ndash; switches command from your local layout to en;
* `systemctl` &ndash; correctly orders parameters of confusing systemctl;
* `systemctl` &ndash; correctly orders parameters of confusing `systemctl`;
* `test.py` &ndash; runs `py.test` instead of `test.py`;
* `tmux` &ndash; fixes tmux commands;
* `tsuru_login` &ndash; runs `tsuru login` if not authenticated or session expired;
* `tmux` &ndash; fixes `tmux` commands;
* `whois` &ndash; fixes `whois` command.
Enabled by default only on specific platforms:
@@ -199,26 +196,30 @@ Enabled by default only on specific platforms:
* `apt_get` &ndash; installs app from apt if it not installed;
* `brew_install` &ndash; fixes formula name for `brew install`;
* `brew_unknown_command` &ndash; fixes wrong brew commands, for example `brew docto/brew doctor`;
* `brew_upgrade` &ndash; appends `--all` to `brew upgrade` as per Homebrew's new behaviour
* `brew_upgrade` &ndash; appends `--all` to `brew upgrade` as per Homebrew's new behaviour;
* `pacman` &ndash; installs app with `pacman` or `yaourt` if it is not installed.
Bundled, but not enabled by default:
* `git_push_force` &ndash; adds `--force` to a `git push` (may conflict with `git_push_pull`);
* `rm_root` &ndash; 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
optional boolean `enabled_by_default`.
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 optional `enabled_by_default`, `requires_output` and `priority` variables.
`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 +238,9 @@ 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
requires_output = True
```
[More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules),
@@ -248,10 +251,11 @@ priority = 1000 # Lower first
The Fuck has a few settings parameters which can be changed in `~/.thefuck/settings.py`:
* `rules` &ndash; list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` &ndash; requires confirmation before running new command, by default `False`;
* `require_confirmation` &ndash; requires confirmation before running new command, by default `True`;
* `wait_command` &ndash; max amount of time in seconds for getting previous command output;
* `no_colors` &ndash; disable colored output;
* `priority` &ndash; dict with rules priorities, rule with lower `priority` will be matched first.
* `priority` &ndash; dict with rules priorities, rule with lower `priority` will be matched first;
* `debug` &ndash; enables debug output, by default `False`.
Example of `settings.py`:
@@ -261,6 +265,7 @@ require_confirmation = True
wait_command = 10
no_colors = False
priority = {'sudo': 100, 'no_command': 9999}
debug = False
```
Or via environment variables:
@@ -270,7 +275,8 @@ Or via environment variables:
* `THEFUCK_WAIT_COMMAND` &ndash; max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` &ndash; disable colored output, `true/false`;
* `THEFUCK_PRIORITY` &ndash; 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` &ndash; enables debug output, `true/false`.
For example:
@@ -291,11 +297,24 @@ pip install -r requirements.txt
python setup.py develop
```
Run tests:
Run unit tests:
```bash
py.test
```
Run unit and functional tests (requires docker):
```bash
FUNCTIONAL=true py.test
```
For sending package to pypi:
```bash
sudo apt-get install pandoc
./release.py
```
## License MIT
Project License can be found [here](LICENSE.md).

BIN
example.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python
from subprocess import call
import os
import re
@@ -28,4 +29,7 @@ call('git commit -am "Bump to {}"'.format(version), shell=True)
call('git tag {}'.format(version), shell=True)
call('git push', shell=True)
call('git push --tags', shell=True)
call('python setup.py sdist bdist_wheel upload', shell=True)
env = os.environ
env['CONVERT_README'] = 'true'
call('python setup.py sdist bdist_wheel upload', shell=True, env=env)

View File

@@ -2,3 +2,6 @@ pytest
mock
pytest-mock
wheel
setuptools>=17.1
pexpect
pypandoc

View File

@@ -1,26 +1,34 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import sys
import os
if sys.version_info < (2, 7):
if os.environ.get('CONVERT_README'):
import pypandoc
long_description = pypandoc.convert('README.md', 'rst')
else:
long_description = ''
version = sys.version_info[:2]
if version < (2, 7):
print('thefuck requires Python version 2.7 or later' +
' ({}.{} detected).'.format(*sys.version_info[:2]))
' ({}.{} detected).'.format(*version))
sys.exit(-1)
elif (3, 0) < sys.version_info < (3, 3):
elif (3, 0) < version < (3, 3):
print('thefuck requires Python version 3.3 or later' +
' ({}.{} detected).'.format(*sys.version_info[:2]))
' ({}.{} detected).'.format(*version))
sys.exit(-1)
VERSION = '1.48'
VERSION = '2.5.6'
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,
description="Magnificent app which corrects your previous console command",
long_description=long_description,
author='Vladimir Iakovlev',
author_email='nvbn.rm@gmail.com',
url='https://github.com/nvbn/thefuck',
@@ -30,6 +38,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

57
tests/functional/plots.py Normal file
View File

@@ -0,0 +1,57 @@
from pexpect import TIMEOUT
def with_confirmation(proc):
"""Ensures that command can be fixed when confirmation enabled."""
proc.sendline(u'mkdir -p ~/.thefuck')
proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py')
proc.sendline(u'ehco test')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'echo test'])
assert proc.expect([TIMEOUT, u'enter'])
assert proc.expect_exact([TIMEOUT, u'ctrl+c'])
proc.send('\n')
assert proc.expect([TIMEOUT, u'test'])
def history_changed(proc):
"""Ensures that history changed."""
proc.send('\033[A')
assert proc.expect([TIMEOUT, u'echo test'])
def history_not_changed(proc):
"""Ensures that history not changed."""
proc.send('\033[A')
assert proc.expect([TIMEOUT, u'fuck'])
def refuse_with_confirmation(proc):
"""Ensures that fix can be refused when confirmation enabled."""
proc.sendline(u'mkdir -p ~/.thefuck')
proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py')
proc.sendline(u'ehco test')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'echo test'])
assert proc.expect([TIMEOUT, u'enter'])
assert proc.expect_exact([TIMEOUT, u'ctrl+c'])
proc.send('\003')
assert proc.expect([TIMEOUT, u'Aborted'])
def without_confirmation(proc):
"""Ensures that command can be fixed when confirmation disabled."""
proc.sendline(u'mkdir -p ~/.thefuck')
proc.sendline(u'echo "require_confirmation = False" > ~/.thefuck/settings.py')
proc.sendline(u'ehco test')
proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'echo test'])
assert proc.expect([TIMEOUT, u'test'])

View File

@@ -0,0 +1,51 @@
import pytest
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation, history_changed, history_not_changed
from tests.functional.utils import spawn, functional, images
containers = images(('ubuntu-python3-bash', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
'''),
('ubuntu-python2-bash', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev
RUN pip2 install -U pip setuptools
'''))
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'bash') as proc:
proc.sendline(u"export PS1='$ '")
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'touch $HISTFILE')
with_confirmation(proc)
history_changed(proc)
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'bash') as proc:
proc.sendline(u"export PS1='$ '")
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'touch $HISTFILE')
refuse_with_confirmation(proc)
history_not_changed(proc)
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'bash') as proc:
proc.sendline(u"export PS1='$ '")
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'touch $HISTFILE')
without_confirmation(proc)
history_changed(proc)

View File

@@ -0,0 +1,53 @@
import pytest
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation
from tests.functional.utils import spawn, functional, images, bare
containers = images(('ubuntu-python3-fish', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev fish
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
'''),
('ubuntu-python2-fish', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev fish
RUN pip2 install -U pip setuptools
'''))
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'fish') as proc:
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish')
proc.sendline(u'fish')
with_confirmation(proc)
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'fish') as proc:
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish')
proc.sendline(u'fish')
refuse_with_confirmation(proc)
@functional
@pytest.mark.skipif(
bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71')
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'fish') as proc:
proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish')
proc.sendline(u'fish')
without_confirmation(proc)
# TODO: ensure that history changes.

View File

@@ -0,0 +1,47 @@
import pytest
from tests.functional.utils import spawn, functional, images
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation
containers = images(('ubuntu-python3-tcsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev tcsh
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
'''),
('ubuntu-python2-tcsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev tcsh
RUN pip2 install -U pip setuptools
'''))
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'tcsh') as proc:
proc.sendline(u'tcsh')
proc.sendline(u'eval `thefuck-alias`')
with_confirmation(proc)
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'tcsh') as proc:
proc.sendline(u'tcsh')
proc.sendline(u'eval `thefuck-alias`')
refuse_with_confirmation(proc)
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'tcsh') as proc:
proc.sendline(u'tcsh')
proc.sendline(u'eval `thefuck-alias`')
without_confirmation(proc)
# TODO: ensure that history changes.

View File

@@ -0,0 +1,51 @@
import pytest
from tests.functional.utils import spawn, functional, images
from tests.functional.plots import with_confirmation, without_confirmation, \
refuse_with_confirmation, history_changed, history_not_changed
containers = images(('ubuntu-python3-zsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python3 python3-pip python3-dev zsh
RUN pip3 install -U setuptools
RUN ln -s /usr/bin/pip3 /usr/bin/pip
'''),
('ubuntu-python2-zsh', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python python-pip python-dev zsh
RUN pip2 install -U pip setuptools
'''))
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'zsh') as proc:
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'export HISTFILE=~/.zsh_history')
proc.sendline(u'touch $HISTFILE')
with_confirmation(proc)
history_changed(proc)
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_refuse_with_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'zsh') as proc:
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'export HISTFILE=~/.zsh_history')
proc.sendline(u'touch $HISTFILE')
refuse_with_confirmation(proc)
history_not_changed(proc)
@functional
@pytest.mark.parametrize('tag, dockerfile', containers)
def test_without_confirmation(tag, dockerfile):
with spawn(tag, dockerfile, u'zsh') as proc:
proc.sendline(u'eval $(thefuck-alias)')
proc.sendline(u'export HISTFILE=~/.zsh_history')
proc.sendline(u'touch $HISTFILE')
without_confirmation(proc)
history_changed(proc)

54
tests/functional/utils.py Normal file
View File

@@ -0,0 +1,54 @@
import os
from contextlib import contextmanager
import subprocess
import shutil
from tempfile import mkdtemp
from pathlib import Path
import sys
import pexpect
import pytest
root = str(Path(__file__).parent.parent.parent.resolve())
bare = os.environ.get('BARE')
enabled = os.environ.get('FUNCTIONAL')
def build_container(tag, dockerfile):
tmpdir = mkdtemp()
with Path(tmpdir).joinpath('Dockerfile').open('w') as file:
file.write(dockerfile)
if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir],
cwd=root) != 0:
raise Exception("Can't build a container")
shutil.rmtree(tmpdir)
@contextmanager
def spawn(tag, dockerfile, cmd):
if bare:
proc = pexpect.spawnu(cmd)
else:
tag = 'thefuck/{}'.format(tag)
build_container(tag, dockerfile)
proc = pexpect.spawnu('docker run --volume {}:/src --tty=true '
'--interactive=true {} {}'.format(root, tag, cmd))
proc.sendline('pip install /src')
proc.logfile = sys.stdout
try:
yield proc
finally:
proc.terminate(force=bare)
def images(*items):
if bare:
return [items[0]]
else:
return items
functional = pytest.mark.skipif(
not enabled,
reason='Functional tests are disabled by default.')

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,62 @@
import os
import pytest
import tarfile
from thefuck.rules.dirty_untar import match, get_new_command, side_effect
from tests.utils import Command
@pytest.fixture
def tar_error(tmpdir):
def fixture(filename):
path = os.path.join(str(tmpdir), filename)
def reset(path):
with tarfile.TarFile(path, 'w') as archive:
for file in ('a', 'b', 'c'):
with open(file, 'w') as f:
f.write('*')
archive.add(file)
os.remove(file)
with tarfile.TarFile(path, 'r') as archive:
archive.extractall()
os.chdir(str(tmpdir))
reset(path)
assert(set(os.listdir('.')) == {filename, 'a', 'b', 'c'})
return fixture
parametrize_filename = pytest.mark.parametrize('filename', [
'foo.tar',
'foo.tar.gz',
'foo.tgz'])
parametrize_script = pytest.mark.parametrize('script, fixed', [
('tar xvf {}', 'mkdir -p foo && tar xvf {} -C foo'),
('tar -xvf {}', 'mkdir -p foo && tar -xvf {} -C foo'),
('tar --extract -f {}', 'mkdir -p foo && tar --extract -f {} -C foo')])
@parametrize_filename
@parametrize_script
def test_match(tar_error, filename, script, fixed):
tar_error(filename)
assert match(Command(script=script.format(filename)), None)
@parametrize_filename
@parametrize_script
def test_side_effect(tar_error, filename, script, fixed):
tar_error(filename)
side_effect(Command(script=script.format(filename)), None)
assert(os.listdir('.') == [filename])
@parametrize_filename
@parametrize_script
def test_get_new_command(tar_error, filename, script, fixed):
tar_error(filename)
assert get_new_command(Command(script=script.format(filename)), None) == fixed.format(filename)

View File

@@ -0,0 +1,45 @@
import os
import pytest
import zipfile
from thefuck.rules.dirty_unzip import match, get_new_command, side_effect
from tests.utils import Command
@pytest.fixture
def zip_error(tmpdir):
path = os.path.join(str(tmpdir), 'foo.zip')
def reset(path):
with zipfile.ZipFile(path, 'w') as archive:
archive.writestr('a', '1')
archive.writestr('b', '2')
archive.writestr('c', '3')
archive.extractall()
os.chdir(str(tmpdir))
reset(path)
assert(set(os.listdir('.')) == {'foo.zip', 'a', 'b', 'c'})
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_match(zip_error, script):
assert match(Command(script=script), None)
@pytest.mark.parametrize('script', [
'unzip foo',
'unzip foo.zip'])
def test_side_effect(zip_error, script):
side_effect(Command(script=script), None)
assert(os.listdir('.') == ['foo.zip'])
@pytest.mark.parametrize('script,fixed', [
('unzip foo', 'unzip foo -d foo'),
('unzip foo.zip', 'unzip foo.zip -d foo')])
def test_get_new_command(zip_error, script, fixed):
assert get_new_command(Command(script=script), None) == fixed

View File

@@ -0,0 +1,129 @@
import pytest
from io import BytesIO
from tests.utils import Command
from thefuck.rules.docker_not_command import get_new_command, match
@pytest.fixture
def docker_help(mocker):
help = b'''Usage: docker [OPTIONS] COMMAND [arg...]
A self-sufficient runtime for linux containers.
Options:
--api-cors-header= Set CORS headers in the remote API
-b, --bridge= Attach containers to a network bridge
--bip= Specify network bridge IP
-D, --debug=false Enable debug mode
-d, --daemon=false Enable daemon mode
--default-gateway= Container default gateway IPv4 address
--default-gateway-v6= Container default gateway IPv6 address
--default-ulimit=[] Set default ulimits for containers
--dns=[] DNS server to use
--dns-search=[] DNS search domains to use
-e, --exec-driver=native Exec driver to use
--exec-opt=[] Set exec driver options
--exec-root=/var/run/docker Root of the Docker execdriver
--fixed-cidr= IPv4 subnet for fixed IPs
--fixed-cidr-v6= IPv6 subnet for fixed IPs
-G, --group=docker Group for the unix socket
-g, --graph=/var/lib/docker Root of the Docker runtime
-H, --host=[] Daemon socket(s) to connect to
-h, --help=false Print usage
--icc=true Enable inter-container communication
--insecure-registry=[] Enable insecure registry communication
--ip=0.0.0.0 Default IP when binding container ports
--ip-forward=true Enable net.ipv4.ip_forward
--ip-masq=true Enable IP masquerading
--iptables=true Enable addition of iptables rules
--ipv6=false Enable IPv6 networking
-l, --log-level=info Set the logging level
--label=[] Set key=value labels to the daemon
--log-driver=json-file Default driver for container logs
--log-opt=map[] Set log driver options
--mtu=0 Set the containers network MTU
-p, --pidfile=/var/run/docker.pid Path to use for daemon PID file
--registry-mirror=[] Preferred Docker registry mirror
-s, --storage-driver= Storage driver to use
--selinux-enabled=false Enable selinux support
--storage-opt=[] Set storage driver options
--tls=false Use TLS; implied by --tlsverify
--tlscacert=~/.docker/ca.pem Trust certs signed only by this CA
--tlscert=~/.docker/cert.pem Path to TLS certificate file
--tlskey=~/.docker/key.pem Path to TLS key file
--tlsverify=false Use TLS and verify the remote
--userland-proxy=true Use userland proxy for loopback traffic
-v, --version=false Print version information and quit
Commands:
attach Attach to a running container
build Build an image from a Dockerfile
commit Create a new image from a container's changes
cp Copy files/folders from a container's filesystem to the host path
create Create a new container
diff Inspect changes on a container's filesystem
events Get real time events from the server
exec Run a command in a running container
export Stream the contents of a container as a tar archive
history Show the history of an image
images List images
import Create a new filesystem image from the contents of a tarball
info Display system-wide information
inspect Return low-level information on a container or image
kill Kill a running container
load Load an image from a tar archive
login Register or log in to a Docker registry server
logout Log out from a Docker registry server
logs Fetch the logs of a container
pause Pause all processes within a container
port Lookup the public-facing port that is NAT-ed to PRIVATE_PORT
ps List containers
pull Pull an image or a repository from a Docker registry server
push Push an image or a repository to a Docker registry server
rename Rename an existing container
restart Restart a running container
rm Remove one or more containers
rmi Remove one or more images
run Run a command in a new container
save Save an image to a tar archive
search Search for an image on the Docker Hub
start Start a stopped container
stats Display a stream of a containers' resource usage statistics
stop Stop a running container
tag Tag an image into a repository
top Lookup the running processes of a container
unpause Unpause a paused container
version Show the Docker version information
wait Block until a container stops, then print its exit code
Run 'docker COMMAND --help' for more information on a command.
'''
mock = mocker.patch('subprocess.Popen')
mock.return_value.stdout = BytesIO(help)
return mock
def stderr(cmd):
return "docker: '{}' is not a docker command.\n" \
"See 'docker --help'.".format(cmd)
def test_match():
assert match(Command('docker pes', stderr=stderr('pes')), None)
@pytest.mark.parametrize('script, stderr', [
('docker ps', ''),
('cat pes', stderr('pes'))])
def test_not_match(script, stderr):
assert not match(Command(script, stderr=stderr), None)
@pytest.mark.usefixtures('docker_help')
@pytest.mark.parametrize('wrong, fixed', [
('pes', 'ps'),
('tags', 'tag')])
def test_get_new_command(wrong, fixed):
command = Command('docker {}'.format(wrong), stderr=stderr(wrong))
assert get_new_command(command, None) == 'docker {}'.format(fixed)

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.get_branches')
@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,21 @@ 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'),
(['test-random-branch-123'],
Command(script='git checkout tst-rdm-brnch-123',
stderr=did_not_match('tst-rdm-brnch-123')),
'git checkout test-random-branch-123'),
(['test-random-branch-123'],
Command(script='git commit tst-rdm-brnch-123',
stderr=did_not_match('tst-rdm-brnch-123')),
'git commit test-random-branch-123')])
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

@@ -0,0 +1,31 @@
import pytest
from thefuck.rules.git_fix_stash import match, get_new_command
from tests.utils import Command
git_stash_err = '''
usage: git stash list [<options>]
or: git stash show [<stash>]
or: git stash drop [-q|--quiet] [<stash>]
or: git stash ( pop | apply ) [--index] [-q|--quiet] [<stash>]
or: git stash branch <branchname> [<stash>]
or: git stash [save [--patch] [-k|--[no-]keep-index] [-q|--quiet]
[-u|--include-untracked] [-a|--all] [<message>]]
or: git stash clear
'''
@pytest.mark.parametrize('wrong', [
'git stash opp',
'git stash Some message',
'git stash saev Some message'])
def test_match(wrong):
assert match(Command(wrong, stderr=git_stash_err), None)
@pytest.mark.parametrize('wrong,fixed', [
('git stash opp', 'git stash pop'),
('git stash Some message', 'git stash save Some message'),
('git stash saev Some message', 'git stash save Some message')])
def test_get_new_command(wrong, fixed):
assert get_new_command(Command(wrong, stderr=git_stash_err), None) == fixed

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

@@ -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,28 @@
import pytest
from tests.utils import Command
from thefuck.rules.gulp_not_task import match, get_new_command
def stdout(task):
return '''[00:41:11] Using gulpfile gulpfile.js
[00:41:11] Task '{}' is not in your gulpfile
[00:41:11] Please check the documentation for proper gulpfile formatting
'''.format(task)
def test_match():
assert match(Command('gulp srve', stdout('srve')), None)
@pytest.mark.parametrize('script, stdout', [
('gulp serve', ''),
('cat srve', stdout('srve'))])
def test_not_march(script, stdout):
assert not match(Command(script, stdout), None)
def test_get_new_command(mocker):
mocker.patch('thefuck.rules.gulp_not_task.get_gulp_tasks', return_value=[
'serve', 'build', 'default'])
command = Command('gulp srve', stdout('srve'))
assert get_new_command(command, None) == 'gulp serve'

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

@@ -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,14 @@ def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='mv foo bar/', stderr=""),
Command(script='mv foo bar/foo', stderr="mv: permission denied"),
])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(script='mv foo bar/foo', stderr="mv: cannot move 'foo' to 'bar/foo': No such file or directory"), 'mkdir -p bar && mv foo bar/foo'),
(Command(script='mv foo bar/', stderr="mv: cannot move 'foo' to 'bar/': No such file or directory"), 'mkdir -p bar && mv foo bar/'),

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

@@ -0,0 +1,37 @@
import pytest
from thefuck.rules.tsuru_login import match, get_new_command
from tests.utils import Command
error_msg = (
"Error: you're not authenticated or your session has expired.",
("You're not authenticated or your session has expired. "
"Please use \"login\" command for authentication."),
)
@pytest.mark.parametrize('command', [
Command(script='tsuru app-shell', stderr=error_msg[0]),
Command(script='tsuru app-log -f', stderr=error_msg[1]),
])
def test_match(command):
assert match(command, {})
@pytest.mark.parametrize('command', [
Command(script='tsuru'),
Command(script='tsuru app-restart', stderr=('Error: unauthorized')),
Command(script='tsuru app-log -f', stderr=('Error: unparseable data')),
])
def test_not_match(command):
assert not match(command, {})
@pytest.mark.parametrize('command, new_command', [
(Command('tsuru app-shell', stderr=error_msg[0]),
'tsuru login && tsuru app-shell'),
(Command('tsuru app-log -f', stderr=error_msg[1]),
'tsuru login && tsuru app-log -f'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command, {}) == new_command

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

@@ -14,7 +14,8 @@ def test_load_rule(mocker):
return_value=Mock(match=match,
get_new_command=get_new_command,
enabled_by_default=True,
priority=900))
priority=900,
requires_output=True))
assert main.load_rule(Path('/rules/bash.py')) \
== Rule('bash', match, get_new_command, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py')
@@ -77,23 +78,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 +111,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 +127,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),
@@ -152,7 +153,7 @@ class TestConfirm(object):
def test_with_side_effect_and_without_confirmation(self, capsys):
assert main.confirm('command', Mock(), Mock(require_confirmation=False))
assert capsys.readouterr() == ('', 'command*\n')
assert capsys.readouterr() == ('', 'command (+side effect)\n')
# `stdin` fixture should be applied after `capsys`
def test_when_confirmation_required_and_confirmed(self, capsys, stdin):
@@ -164,7 +165,7 @@ class TestConfirm(object):
def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin):
assert main.confirm('command', Mock(), Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command* [enter/ctrl+c]')
assert capsys.readouterr() == ('', 'command (+side effect) [enter/ctrl+c]')
def test_when_confirmation_required_and_aborted(self, capsys, stdin):
stdin.side_effect = KeyboardInterrupt

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, get_closest
from thefuck.utils import git_support, sudo_support, wrap_settings,\
memoize, get_closest, get_all_executables, replace_argument
from thefuck.types import Settings
from tests.utils import Command
@@ -26,6 +27,29 @@ 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
@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)
@@ -34,6 +58,15 @@ def test_memoize():
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):
@@ -41,3 +74,28 @@ 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
@pytest.mark.parametrize('args, result', [
(('apt-get instol vim', 'instol', 'install'), 'apt-get install vim'),
(('git brnch', 'brnch', 'branch'), 'git branch')])
def test_replace_argument(args, result):
assert replace_argument(*args) == result

View File

@@ -10,7 +10,8 @@ def Rule(name='', match=lambda *_: True,
get_new_command=lambda *_: '',
enabled_by_default=True,
side_effect=None,
priority=DEFAULT_PRIORITY):
priority=DEFAULT_PRIORITY,
requires_output=True):
return types.Rule(name, match, get_new_command,
enabled_by_default, side_effect,
priority)
priority, requires_output)

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': {'LC_ALL': 'C', '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,5 @@
from contextlib import contextmanager
from datetime import datetime
import sys
from traceback import format_exception
import colorama
@@ -27,19 +29,19 @@ def rule_failed(rule, exc_info, settings):
def show_command(new_command, side_effect, settings):
sys.stderr.write('{bold}{command}{side_effect}{reset}\n'.format(
sys.stderr.write('{bold}{command}{reset}{side_effect}\n'.format(
command=new_command,
side_effect='*' if side_effect else '',
side_effect=' (+side effect)' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, side_effect, settings):
sys.stderr.write(
'{bold}{command}{side_effect}{reset} '
'{bold}{command}{reset}{side_effect} '
'[{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command,
side_effect='*' if side_effect else '',
side_effect=' (+side effect)' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings),
@@ -52,3 +54,21 @@ 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)))
@contextmanager
def debug_time(msg, settings):
started = datetime.now()
try:
yield
finally:
debug('{} took: {}'.format(msg, datetime.now() - started), 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
@@ -27,7 +28,8 @@ def load_rule(rule):
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True),
getattr(rule_module, 'side_effect', None),
getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY))
getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY),
getattr(rule_module, 'requires_output', True))
def _get_loaded_rules(rules, settings):
@@ -79,19 +81,38 @@ 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'))
if wait_output(settings, result):
return types.Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8'))
env = dict(os.environ)
env.update(settings.env)
with logs.debug_time(u'Call: {}; with env: {};'.format(script, env),
settings):
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env)
if wait_output(settings, result):
stdout = result.stdout.read().decode('utf-8')
stderr = result.stderr.read().decode('utf-8')
logs.debug(u'Received stdout: {}'.format(stdout), settings)
logs.debug(u'Received stderr: {}'.format(stderr), settings)
return types.Command(script, stdout, stderr)
else:
logs.debug(u'Execution timed out!', settings)
return types.Command(script, None, None)
def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command."""
script_only = command.stdout is None and command.stderr is None
for rule in rules:
if script_only and rule.requires_output:
continue
try:
if rule.match(command, settings):
return rule
with logs.debug_time(u'Trying rule: {};'.format(rule.name),
settings):
if rule.match(command, settings):
return rule
except Exception:
logs.rule_failed(rule, sys.exc_info(), settings)
@@ -121,17 +142,32 @@ 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)
with logs.debug_time('Total', settings):
logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv)
if command:
command = get_command(settings, sys.argv)
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)
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,7 +1,7 @@
import os
import re
from subprocess import check_output
from thefuck.utils import get_closest
from thefuck.utils import get_closest, replace_argument
# Formulars are base on each local system's status
@@ -40,4 +40,4 @@ def get_new_command(command, settings):
command.stderr)[0]
exist_formula = _get_similar_formula(not_exist_formula)
return command.script.replace(not_exist_formula, exist_formula, 1)
return replace_argument(command.script, not_exist_formula, exist_formula)

View File

@@ -1,7 +1,7 @@
import os
import re
import subprocess
from thefuck.utils import get_closest
from thefuck.utils import get_closest, replace_argument
BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps'
@@ -99,4 +99,4 @@ def get_new_command(command, settings):
command.stderr)[0]
new_cmd = _get_similar_command(broken_cmd)
return command.script.replace(broken_cmd, new_cmd, 1)
return replace_argument(command.script, broken_cmd, new_cmd)

View File

@@ -1,4 +1,5 @@
import re
from thefuck.utils import replace_argument
def match(command, settings):
@@ -11,4 +12,4 @@ def get_new_command(command, settings):
broken = command.script.split()[1]
fix = re.findall(r'Did you mean `([^`]*)`', command.stderr)[0]
return command.script.replace(broken, fix, 1)
return replace_argument(command.script, broken, fix)

View File

@@ -1,4 +1,5 @@
import re
from thefuck.utils import replace_argument
def match(command, settings):
@@ -12,4 +13,4 @@ def get_new_command(command, settings):
new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.stderr)
if not new_cmd:
new_cmd = re.findall(r'Did you mean one of these\?[^\n]*\n\s*([^\n]*)', command.stderr)
return command.script.replace(broken_cmd, new_cmd[0].strip(), 1)
return replace_argument(command.script, broken_cmd, new_cmd[0].strip())

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

@@ -0,0 +1,41 @@
from thefuck import shells
import os
import tarfile
def _is_tar_extract(cmd):
if '--extract' in cmd:
return True
cmd = cmd.split()
return len(cmd) > 1 and 'x' in cmd[1]
def _tar_file(cmd):
tar_extentions = ('.tar', '.tar.Z', '.tar.bz2', '.tar.gz', '.tar.lz',
'.tar.lzma', '.tar.xz', '.taz', '.tb2', '.tbz', '.tbz2',
'.tgz', '.tlz', '.txz', '.tz')
for c in cmd.split():
for ext in tar_extentions:
if c.endswith(ext):
return (c, c[0:len(c)-len(ext)])
def match(command, settings):
return (command.script.startswith('tar')
and '-C' not in command.script
and _is_tar_extract(command.script)
and _tar_file(command.script) is not None)
def get_new_command(command, settings):
return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \
.format(dir=_tar_file(command.script)[1], cmd=command.script)
def side_effect(command, settings):
with tarfile.TarFile(_tar_file(command.script)[0]) as archive:
for file in archive.getnames():
os.remove(file)

View File

@@ -0,0 +1,39 @@
import os
import zipfile
def _is_bad_zip(file):
with zipfile.ZipFile(file, 'r') as archive:
return len(archive.namelist()) > 1
def _zip_file(command):
# unzip works that way:
# unzip [-flags] file[.zip] [file(s) ...] [-x file(s) ...]
# ^ ^ files to unzip from the archive
# archive to unzip
for c in command.script.split()[1:]:
if not c.startswith('-'):
if c.endswith('.zip'):
return c
else:
return '{}.zip'.format(c)
def match(command, settings):
return (command.script.startswith('unzip')
and '-d' not in command.script
and _is_bad_zip(_zip_file(command)))
def get_new_command(command, settings):
return '{} -d {}'.format(command.script, _zip_file(command)[:-4])
def side_effect(command, settings):
with zipfile.ZipFile(_zip_file(command), 'r') as archive:
for file in archive.namelist():
os.remove(file)
requires_output = False

View File

@@ -0,0 +1,27 @@
from itertools import dropwhile, takewhile, islice
import re
import subprocess
from thefuck.utils import get_closest, sudo_support, replace_argument
@sudo_support
def match(command, settings):
return command.script.startswith('docker') \
and 'is not a docker command' in command.stderr
def get_docker_commands():
proc = subprocess.Popen('docker', stdout=subprocess.PIPE)
lines = [line.decode('utf-8') for line in proc.stdout.readlines()]
lines = dropwhile(lambda line: not line.startswith('Commands:'), lines)
lines = islice(lines, 1, None)
lines = list(takewhile(lambda line: line != '\n', lines))
return [line.strip().split(' ')[0] for line in lines]
@sudo_support
def get_new_command(command, settings):
wrong_command = re.findall(
r"docker: '(\w+)' is not a docker command.", command.stderr)[0]
fixed_command = get_closest(wrong_command, get_docker_commands())
return replace_argument(command.script, wrong_command, fixed_command)

View File

@@ -1,13 +1,14 @@
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
return ('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,13 @@
from thefuck import utils
from thefuck.utils import replace_argument
@utils.git_support
def match(command, settings):
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 replace_argument(command.script, '-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()
return command.script.split()[1:] == '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
from thefuck.utils import replace_argument
@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 replace_argument(command.script, missing_file, closest_branch)
else:
return shells.and_('git branch {}', '{}').format(
missing_file, command.script)

View File

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

View File

@@ -0,0 +1,32 @@
from thefuck import utils
from thefuck.utils import replace_argument
@utils.git_support
def match(command, settings):
return (command.script.split()[1] == 'stash'
and 'usage:' in command.stderr)
# git's output here is too complicated to be parsed (see the test file)
stash_commands = (
'apply',
'branch',
'clear',
'drop',
'list',
'pop',
'save',
'show')
@utils.git_support
def get_new_command(command, settings):
stash_cmd = command.script.split()[2]
fixed = utils.get_closest(stash_cmd, stash_commands, fallback_to_first=False)
if fixed is not None:
return replace_argument(command.script, stash_cmd, fixed)
else:
cmd = command.script.split()
cmd.insert(2, 'save')
return ' '.join(cmd)

View File

@@ -1,11 +1,10 @@
from difflib import get_close_matches
import re
from thefuck.utils import get_closest
from thefuck.utils import get_closest, git_support, replace_argument
@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)
@@ -18,10 +17,11 @@ def _get_all_git_matched_commands(stderr):
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 = get_closest(broken_cmd,
_get_all_git_matched_commands(command.stderr))
return command.script.replace(broken_cmd, new_cmd, 1)
return replace_argument(command.script, broken_cmd, new_cmd)

View File

@@ -1,12 +1,13 @@
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
return ('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,13 @@
from thefuck import utils
from thefuck.utils import replace_argument
@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 replace_argument(command.script, 'pull', 'clone')

View File

@@ -1,8 +1,12 @@
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)
@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
from thefuck.utils import replace_argument
@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 replace_argument(command.script, 'push', 'push --force')
enabled_by_default = False

View File

@@ -0,0 +1,16 @@
from thefuck import utils, shells
from thefuck.utils import replace_argument
@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 shells.and_(replace_argument(command.script, '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
return '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)

View File

@@ -0,0 +1,22 @@
import re
import subprocess
from thefuck.utils import get_closest, replace_argument
def match(command, script):
return command.script.startswith('gulp')\
and 'is not in your gulpfile' in command.stdout
def get_gulp_tasks():
proc = subprocess.Popen(['gulp', '--tasks-simple'],
stdout=subprocess.PIPE)
return [line.decode('utf-8')[:-1]
for line in proc.stdout.readlines()]
def get_new_command(command, script):
wrong_task = re.findall(r"Task '(\w+)' is not in your gulpfile",
command.stdout)[0]
fixed_task = get_closest(wrong_task, get_gulp_tasks())
return replace_argument(command.script, wrong_task, fixed_task)

View File

@@ -0,0 +1,20 @@
import re
from thefuck.utils import get_closest, replace_argument
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 replace_argument(command.script, wrong, correct)

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,5 +1,5 @@
import re
from thefuck.utils import sudo_support
from thefuck.utils import sudo_support, replace_argument
@sudo_support
@@ -15,4 +15,4 @@ def get_new_command(command, settings):
command.stderr)[0]
new_cmd = re.findall(r'Did you mean this\?\n\s*([^\n]*)',
command.stderr)[0]
return command.script.replace(broken_cmd, new_cmd, 1)
return replace_argument(command.script, broken_cmd, new_cmd)

View File

@@ -1,38 +1,18 @@
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, get_closest
@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]
new_command = get_closest(old_command, get_all_executables())
return ' '.join([new_command] + command.script.split(' ')[1:])

View File

@@ -1,4 +1,5 @@
import re
from thefuck.utils import replace_argument
def match(command, settings):
@@ -12,4 +13,4 @@ def get_new_command(command, settings):
command.stderr)[0]
new_cmd = re.findall(r'maybe you meant \"([a-z]+)\"', command.stderr)[0]
return command.script.replace(broken_cmd, new_cmd, 1)
return replace_argument(command.script, broken_cmd, new_cmd)

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 ',
'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, replace_argument
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 replace_argument(command.script, old_cmd, new_cmd)

View File

@@ -0,0 +1,11 @@
from thefuck import shells
def match(command, settings):
return (command.script.startswith('tsuru')
and 'not authenticated' in command.stderr
and 'session has expired' in command.stderr)
def get_new_command(command, settings):
return shells.and_('tsuru login', command.script)

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

@@ -5,7 +5,7 @@ Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default', 'side_effect',
'priority'))
'priority', 'requires_output'))
class RulesNamesList(list):

View File

@@ -1,7 +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
@@ -9,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):
@@ -73,6 +74,38 @@ def sudo_support(fn):
return wrapper
def git_support(fn):
"""Resolves git aliases and supports testing for both git and hub."""
@wraps(fn)
def wrapper(command, settings):
# 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)
# 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 = {}
@@ -80,18 +113,49 @@ 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):
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]
def replace_argument(script, from_, to):
"""Replaces command line argument."""
replaced_in_the_end = re.sub(u' {}$'.format(from_), u' {}'.format(to),
script, count=1)
if replaced_in_the_end != script:
return replaced_in_the_end
else:
return script.replace(
u' {} '.format(from_), u' {} '.format(to), 1)

View File

@@ -3,4 +3,4 @@ envlist = py27,py33,py34
[testenv]
deps = -rrequirements.txt
commands = py.test
commands = py.test -v --capture=sys