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

Compare commits

...

187 Commits
1.43 ... 2.3

Author SHA1 Message Date
nvbn
6c534c52bc Bump to 2.3 2015-07-22 04:45:04 +03:00
nvbn
b4392ba706 #N/A Add heroku_not_command rule 2015-07-22 04:44:37 +03:00
Vladimir Iakovlev
46f918718f Merge pull request #307 from evverx/lc_all
Force LC_ALL to C
2015-07-21 17:11:16 +03:00
Vladimir Iakovlev
d71ce76ae4 Merge pull request #306 from mcarton/hub
Support GitHub's hub command
2015-07-21 16:43:24 +03:00
nvbn
355505a0a8 #N/A Make git_checkout test less dependent on get_closest 2015-07-21 16:40:45 +03:00
Evgeny Vereshchagin
3d425ce831 Force LC_ALL to C
See: http://unix.stackexchange.com/a/87763/120177
2015-07-21 13:39:34 +00:00
mcarton
98a9fb3d7d Remove now redundant checks in git_* rules 2015-07-21 15:35:39 +02:00
nvbn
c8d748e095 Bump to 2.2 2015-07-21 16:31:17 +03:00
nvbn
e0af35819d Merge branch 'master' of github.com:nvbn/thefuck 2015-07-21 16:19:11 +03:00
nvbn
9e4c250e4e #301 Fix bash support on non-eng systems 2015-07-21 16:19:01 +03:00
Vladimir Iakovlev
8c395377f8 Merge pull request #299 from evverx/dnf-history
Add `dnf history` error for the sudo rule
2015-07-21 16:15:08 +03:00
Vladimir Iakovlev
f165523247 Merge pull request #304 from mcarton/fix-git_diff_staged
Fix the `git_diff_staged` rule
2015-07-21 16:12:19 +03:00
mcarton
903abff77e Support hub as well as git in @git_support 2015-07-21 15:06:04 +02:00
Evgeny Vereshchagin
6d39b78824 Add dnf history error for the sudo rule
$ dnf history
You don't have access to the history DB.
2015-07-21 12:56:25 +00:00
mcarton
1285303363 Fix the git_diff_staged rule
The problem was:
```
% git add foo
% git diff foo
% fuck
git diff foo --staged [enter/ctrl+c]
fatal: bad flag '--staged' used after filename
```
2015-07-21 14:06:37 +02:00
nvbn
66e2ec7e3f Merge branch 'mcarton-fix-readme' 2015-07-20 22:49:31 +03:00
nvbn
92cca7b641 #296 Fix [enter/ctrl+c] case in the readme 2015-07-20 22:49:21 +03:00
mcarton
e572cab1f3 Have the README look better 2015-07-20 21:12:39 +02:00
mcarton
33b1536c28 Move misplaced rule in README 2015-07-20 21:07:56 +02:00
mcarton
d4fada8e4c Reflect the new default for require_confirmation 2015-07-20 21:06:59 +02:00
mcarton
afc089bc3c Be more consistent in README 2015-07-20 20:49:21 +02:00
nvbn
300c8f528a #N/A Mention tcsh in readme 2015-07-20 21:27:19 +03:00
nvbn
7b011a504d #N/A Fix tests in travis 2015-07-20 21:24:00 +03:00
nvbn
164103693b Bump to 2.1 2015-07-20 21:16:53 +03:00
nvbn
a21c99200e #294 Mention common shells configs in readme 2015-07-20 21:15:34 +03:00
nvbn
1b961c4b87 #294 Move entry point for alias to main 2015-07-20 21:14:43 +03:00
nvbn
a849b65352 Merge branch 'easy-install' of https://github.com/mcarton/thefuck into mcarton-easy-install 2015-07-20 21:06:21 +03:00
nvbn
dee018e792 #N/A Move get_all_executables (formerly get_all_callables) to utils 2015-07-20 21:04:49 +03:00
nvbn
c67560864a #295 Add git_push_pull rule 2015-07-20 20:51:18 +03:00
Vladimir Iakovlev
b636e9bec7 Merge pull request #295 from mcarton/new-git-rules
New git rules
2015-07-20 20:42:02 +03:00
nvbn
36450b740f #270 Add default priority in the readme 2015-07-20 20:01:45 +03:00
mcarton
0f67aad93b Update README 2015-07-20 18:58:16 +02:00
mcarton
bb7579ead5 Add the git_pull_clone rule 2015-07-20 18:58:16 +02:00
mcarton
569709388d Add a git_push_force rule 2015-07-20 18:58:11 +02:00
nvbn
baf7796295 #129 Ignore thefuck alias in switch_lang rule 2015-07-20 19:40:45 +03:00
nvbn
7b32f1df04 #N/A Fix debug output with unicode commands 2015-07-20 19:35:32 +03:00
nvbn
cd084c8ba6 #N/A Fix history rule with blank history 2015-07-20 19:30:41 +03:00
nvbn
4f5659caad #87 Add ability to fix branch names in git_checkout rule 2015-07-20 19:25:29 +03:00
mcarton
370f258b89 Change installation method in README 2015-07-20 13:40:07 +02:00
mcarton
9a069daada Make thefuck-alias generated alias a parameter 2015-07-20 13:35:22 +02:00
nvbn
ee87d1c547 #N/A Ignore history lines before fuck call in history rule 2015-07-20 01:53:32 +03:00
Vladimir Iakovlev
7e03b55729 Merge pull request #293 from mcarton/git-aliases
#292 #290 Use @git_support in all git rules
2015-07-20 01:25:25 +03:00
mcarton
db76462802 #292 #290 Use @git_support in all git rules 2015-07-20 00:08:01 +02:00
Vladimir Iakovlev
dbf20ebc73 Fix typo 2015-07-19 22:41:10 +03:00
Vladimir Iakovlev
b8a74b1425 Remove barely working coveralls badge 2015-07-19 22:40:25 +03:00
nvbn
4fb990742d Bump to 2.0 2015-07-19 22:33:56 +03:00
nvbn
cf3dca6f51 #284 Add coveralls support 2015-07-19 21:57:19 +03:00
nvbn
5187bada1b #N/A Update readme 2015-07-19 21:53:08 +03:00
nvbn
0238569b71 #N/A Require confirmation by default 2015-07-19 21:52:46 +03:00
nvbn
463b4fef2f Merge branch 'mcarton-git-aliases' 2015-07-19 21:29:39 +03:00
nvbn
f90bac10ed #290: Fix typo 2015-07-19 21:29:28 +03:00
nvbn
90014b2b05 Merge branch 'git-aliases' of https://github.com/mcarton/thefuck into mcarton-git-aliases 2015-07-19 21:27:04 +03:00
Vladimir Iakovlev
4276cacaf6 Merge pull request #292 from SimenB/delete-git-branch
Add git_branch_delete rule
2015-07-19 21:26:39 +03:00
Simen Bekkhus
b31aea3737 Add git_branch_delete rule 2015-07-19 13:45:46 +02:00
nvbn
fbfb4b5e41 Merge branch 'petr-tichy-master' 2015-07-18 17:19:57 +03:00
Petr Tichý
51c37bc5ab Fix wheel dependencies for Python 2 2015-07-17 18:51:35 +02:00
mcarton
5d0912fee8 Unquote over-quoted commands in @git_support
This allows writing rules more easily (eg. the git_branch_list rule
tests for `command.script.split() == 'git branch list'.split()`) and
looks nicer when `require_confirmation` is set.
2015-07-17 14:07:17 +02:00
mcarton
f6a4902074 Use @git_support in all git_* rules 2015-07-17 13:11:36 +02:00
mcarton
707d91200e Make the environment a setting
This would allow other rules to set the environment as needed for
`@git_support` and `GIT_TRACE`.
2015-07-17 11:37:13 +02:00
mcarton
b3e09d68df Start support for git aliases 2015-07-16 20:23:31 +02:00
nvbn
78769e4fbc Bump to 1.49 2015-07-15 07:49:18 +03:00
nvbn
3e4c043ccc #280: Add debug output 2015-07-15 07:47:54 +03:00
nvbn
934099fe9e #289: Add is a directory pattern to cp_omitting_directory rule 2015-07-15 07:12:07 +03:00
Vladimir Iakovlev
464f86eccf Merge pull request #288 from scorphus/overridden-aliases
fix(fish.get_aliases): do not include overridden aliases
2015-07-15 06:58:32 +03:00
Pablo Santiago Blum de Aguiar
891fbe7ed1 fix(fish.get_aliases): do not include overridden aliases
Fish Shell overrides some shell commands, such as `cd` and `ls` and
therefore some rules fail to match. The following aliases are excluded
by default:

 * cd
 * grep
 * ls
 * man
 * open

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

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

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

```
% systemctl daemon-reload
==== AUTHENTICATING FOR org.freedesktop.systemd1.reload-daemon ===
Authentication is required to reload the systemd state.
Authenticating as: martin
Password:
```
2015-07-09 17:24:45 +02:00
nvbn
c7071763a3 Bump to 1.48 2015-07-08 21:34:39 +03:00
nvbn
27b5b9de6a #229 Use closest git command 2015-07-08 21:33:30 +03:00
nvbn
c0eae8b85c #N/A Add get_closest utility function 2015-07-08 21:30:24 +03:00
nvbn
fbf7b91005 Bump to 1.47 2015-07-07 16:39:40 +03:00
nvbn
3d842fe6eb #N/A Fix setup.py 2015-07-07 16:39:21 +03:00
Vladimir Iakovlev
17d359b43f Merge pull request #281 from scorphus/mercurial
improve(rules): add mercurial (hg) support
2015-07-07 16:36:06 +03:00
nvbn
22503cdb94 #279 Fix merge 2015-07-07 16:34:30 +03:00
nvbn
a8de919300 Merge branch 'master' of https://github.com/mkreder/thefuck into mkreder-master 2015-07-07 16:33:58 +03:00
nvbn
fac18de242 #267 Compare version_info with a tuple 2015-07-07 16:33:41 +03:00
Vladimir Iakovlev
26fc18dfe4 Merge pull request #278 from mcarton/sed
Add a sed_unterminated_s rule
2015-07-07 16:30:42 +03:00
Vladimir Iakovlev
0fb5c9a228 Merge pull request #277 from mcarton/fix-sudo
Fix the pacman rule with `sudo`
2015-07-07 16:30:02 +03:00
Vladimir Iakovlev
04a342bbc7 Merge pull request #276 from mcarton/tmux
Add a tmux rule
2015-07-07 16:29:15 +03:00
Vladimir Iakovlev
669fbff6ce Merge pull request #272 from scorphus/issue-271-ls-lah
fix(rules.ls_lah): make sure script starts with ls
2015-07-07 16:28:49 +03:00
Vladimir Iakovlev
e2542915e4 Merge pull request #267 from SanketDG/version_check
add python version testing in setup.py
2015-07-07 16:28:28 +03:00
Vladimir Iakovlev
1b08a7dcb6 Merge pull request #265 from mcarton/open
Open
2015-07-07 16:27:33 +03:00
Pablo Santiago Blum de Aguiar
8d77a2d528 improve(rules): add mercurial (hg) support
Fix #269
2015-07-06 21:37:31 -03:00
Matias Kreder
18ea4272ab change deps to install_requires #2 2015-07-04 12:26:56 -03:00
Matias Kreder
a75c99eb12 change deps to install_requires 2015-07-04 12:24:09 -03:00
mcarton
f3cdfbdbdb Add a sed_unterminated_s rule 2015-07-04 17:10:11 +02:00
Matias Kreder
30082bc9a9 fixed dependency problem, on python 3.4.0 pathlib is included in the python distribution so it must not be included in the requirements for thefuck otherwise it wont run at startup 2015-07-04 11:55:28 -03:00
mcarton
3822f62d90 Add a tmux rule 2015-07-04 14:17:33 +02:00
Pablo Santiago Blum de Aguiar
25cc98a21a fix(rules.ls_lah): make sure script starts with ls
Fix #271
2015-07-03 14:24:45 -03:00
SanketDG
409d839e92 add python version testing in setup.py 2015-07-03 00:29:07 +05:30
mcarton
51b5dd0460 Fix the pacman rule with sudo
Does not use @sudo_support as this does not place 'sudo' at the right
position.
2015-06-28 22:10:34 +02:00
Vladimir Iakovlev
cb33c912e5 Merge pull request #264 from mcarton/cleanup
Cleanup
2015-06-26 17:30:14 +03:00
Vladimir Iakovlev
470d66eeb2 Merge pull request #263 from mcarton/test.py
Add a test.py rule
2015-06-26 17:28:24 +03:00
mcarton
5552fd3dc9 s/compile/execute when talking about Python
The word 'compile' is just misleading here.
2015-06-26 14:54:33 +02:00
mcarton
ab55c1cccb Add other flavors of open command 2015-06-26 14:52:34 +02:00
mcarton
7173e0dbad Use spaces instead of tabs
The is more common in python and follows other rules usage.
2015-06-26 14:50:01 +02:00
mcarton
369ea7ff46 Add a test.py rule 2015-06-26 14:05:18 +02:00
mcarton
40fe604adc Replace use of '&&' by shells.and_ 2015-06-26 13:58:50 +02:00
mcarton
ef504b6436 Add a few other common patterns for the open rule 2015-06-26 13:45:23 +02:00
mcarton
b59e83cca9 Fix punctuation in README.md 2015-06-26 12:02:05 +02:00
mcarton
6d718a38dc :sort rules in README 2015-06-26 12:00:16 +02:00
mcarton
330f91f5dc Cleanup the systemctl rule 2015-06-26 11:55:10 +02:00
mcarton
e3cc9c52e6 Remove redundant patterns in sudo rule 2015-06-26 11:41:55 +02:00
mcarton
01ce65047a Use spaces instead of tabs
This is more common in python and follows other rules usage.
2015-06-26 11:27:04 +02:00
Vladimir Iakovlev
3203d57b36 Merge pull request #261 from maciekmm/rule-systemctl
Added systemctl rule
2015-06-24 13:50:36 +03:00
Maciej Mionskowski
28ab6c62f8 Added systemctl rule to README.md 2015-06-24 11:07:03 +02:00
Maciej Mionskowski
360e4673eb Added systemctl rule 2015-06-24 09:36:09 +02:00
Vladimir Iakovlev
c6aead735b Merge pull request #259 from diezcami/master
Added Python Compile Rule
2015-06-22 19:19:41 +03:00
Cami Diez
af3e1a555f Edited python compile rule 2015-06-21 09:27:03 +08:00
Cami Diez
897b847975 Added rule 2015-06-21 09:25:20 +08:00
Cami Diez
a0949b1102 Added Python Compile Rule 2015-06-21 09:24:27 +08:00
nvbn
2f1460120e Merge branch 'master' of github.com:nvbn/thefuck 2015-06-16 13:53:24 +03:00
nvbn
43ec397190 #252 Fix bash and zsh aliases 2015-06-16 13:52:41 +03:00
nvbn
eb537bef81 Merge branch 'issue-221-tf-alias' of https://github.com/scorphus/thefuck into scorphus-issue-221-tf-alias 2015-06-16 13:49:17 +03:00
Vladimir Iakovlev
b033d3893b Merge pull request #256 from scorphus/psutil-children
fix(main.wait_output): use Process’ children() instead of get_children()
2015-06-16 13:47:30 +03:00
nvbn
633c4f8415 #257 sudo patterns are case insensitive 2015-06-16 13:46:18 +03:00
TJ Horner
5bfe0ac997 One more trigger word 2015-06-15 17:13:21 -07:00
TJ Horner
e8a55220ad Forgot to commit the actual rule 👎 2015-06-15 17:11:54 -07:00
TJ Horner
ea306038f9 Fix sudo rule 2015-06-15 17:08:08 -07:00
Pablo Santiago Blum de Aguiar
6a88cc47b6 fix(main.wait_output): use Process’ children() instead of get_children()
Since psutil 2.0.0 `get_children()` has become deprecated and the use of
`children()` instead of it has been encouraged. In version 3.0.0, just
released, `get_children()` has been dropped. Check:

https://github.com/giampaolo/psutil/blob/master/HISTORY.rst

Fix #255
2015-06-15 09:59:58 -03:00
Pablo Santiago Blum de Aguiar
96fe1e77b3 refact(rules.no_command): do not add TF_ALIAS to the “callables” list
Fix #234, #245 and #251

Ref #221
2015-06-12 00:49:55 -03:00
Pablo Santiago Blum de Aguiar
c08d9125e4 refact(shells): use an env var TF_ALIAS to keep the name of the alias
This environment variable may be used by any rule to decide whether it
matches or not.
2015-06-10 20:50:49 -03:00
Pablo Santiago Blum de Aguiar
be682170e5 test(shells): add fuck alias to collection of aliases 2015-06-10 20:49:28 -03:00
nvbn
5e981d9b01 Bump to 1.46 2015-06-07 02:24:08 +03:00
nvbn
add499af7f #250 #247 Fix UnicodeDecodeError with fish 2015-06-07 02:23:48 +03:00
nvbn
9c711734aa Merge branch 'encode_fix' of https://github.com/SanketDG/thefuck into SanketDG-encode_fix 2015-06-07 02:13:48 +03:00
Vladimir Iakovlev
ff7a433f39 Merge pull request #249 from mcarton/cargo
Add two cargo related rules
2015-06-07 02:09:11 +03:00
SanketDG
6bb7d79ddc change encoding of return statement to utf8 2015-06-07 00:03:00 +05:30
mcarton
f6c013d033 Add a cargo_no_command rule 2015-06-06 17:22:14 +02:00
mcarton
01cf199866 Add a cargo rule 2015-06-06 17:05:51 +02:00
Vladimir Iakovlev
3d41a3fb7c Merge pull request #246 from cubuspl42/patch-1
Added mount rule
2015-06-04 20:58:24 +03:00
cubuspl42
f55fa35ebf Added mount rule
$ mount /dev/sda2 /mnt/var
mount: only root can do that
2015-06-04 18:38:09 +02:00
nvbn
ce922758a4 Bump to 1.45 2015-06-02 08:47:53 +03:00
Vladimir Iakovlev
c47968a180 Merge pull request #240 from diezcami/brew-upgrade
Added brew_upgrade rule
2015-06-02 08:46:57 +03:00
Vladimir Iakovlev
581c97ec4b Merge pull request #239 from diezcami/quotation-marks
Added quotation_marks rule
2015-06-02 08:46:06 +03:00
Vladimir Iakovlev
0a53966f9b Merge pull request #238 from diezcami/go-run
Added go_run rule
2015-06-02 08:44:56 +03:00
Camille Diez
ed4e7946d7 Updated brew_upgrade description 2015-06-02 13:27:03 +08:00
Cami Diez
2ed96b1d51 Added brew_upgrade rule 2015-06-02 13:23:34 +08:00
Cami Diez
79d94e2651 Added quotation marks rule 2015-06-02 13:18:13 +08:00
Camille Diez
c08182509d Update README.md 2015-06-02 12:08:28 +08:00
Cami Diez
1d2d907c60 Added go_run rule 2015-06-02 12:05:47 +08:00
Vladimir Iakovlev
13996261be Update README.md 2015-06-02 06:11:29 +03:00
Vladimir Iakovlev
afcd7fc67e Merge pull request #237 from waldyrious/patch-1
+how to make the command available right away
2015-06-02 06:10:13 +03:00
Vladimir Iakovlev
c0c7397057 Update README.md 2015-06-02 06:09:45 +03:00
Vladimir Iakovlev
707743e7a7 Merge pull request #236 from bugaevc/git-branch-list
Git branch list
2015-06-02 06:08:09 +03:00
Waldir Pimenta
d8779dc4a6 +how to make the command available right away 2015-06-02 00:57:00 +01:00
Sergey Bugaev
ba9214f7fc Add a test for git_branch_list rule 2015-06-02 00:17:57 +03:00
Sergey Bugaev
660422806c Add git_branch_list rule 2015-06-01 23:52:41 +03:00
Vladimir Iakovlev
3c8978784b Merge pull request #230 from scorphus/git-diff-staged-rule
add(rule): add the new git_diff_staged rule
2015-06-01 08:10:06 +03:00
Vladimir Iakovlev
995b373347 Merge pull request #232 from bugaevc/fix-sudo
Wrap apt-get rule in sudo_support
2015-05-31 01:20:15 +03:00
Sergey Bugaev
dbe1a94c7d Wrap apt-get rule in sudo_support
Fixes sudo_support not working for no_command rule.
2015-05-30 19:40:01 +03:00
Pablo Santiago Blum de Aguiar
15e13d7c1a add(rule): add the new git_diff_staged rule 2015-05-29 18:41:53 -03:00
Vladimir Iakovlev
3194913965 Merge pull request #228 from mcarton/fix-cd-space
Fix cd command
2015-05-29 04:04:02 +03:00
mcarton
237f43ebdb Fix cd command
Fix #226
2015-05-28 23:29:38 +02:00
nvbn
a5aadc6e90 Bump to 1.44 2015-05-28 21:31:10 +03:00
nvbn
18ce062300 Merge branch 'diezcami-java' 2015-05-28 18:03:37 +03:00
nvbn
73bc6c0184 Merge branch 'java' of https://github.com/diezcami/thefuck into diezcami-java
Conflicts:
	README.md
2015-05-28 18:03:24 +03:00
Vladimir Iakovlev
0296a4a46d Merge pull request #227 from Dugucloud/master
Added a sudo string of Fedora's fedup
2015-05-28 18:02:00 +03:00
Vladimir Iakovlev
54a9769c10 Merge pull request #224 from diezcami/javac
Added javac rule
2015-05-28 18:01:14 +03:00
Vladimir Iakovlev
abc7238d14 Merge pull request #219 from scorphus/fix-shell-fish
fix(shell::Fish): avoid looping when calling `fuck` twice
2015-05-28 18:00:42 +03:00
Dugucloud
710a72ee8c Added sudo string for Fedora's fedup 2015-05-28 09:46:41 +08:00
秋纫
e09c6530e5 Merge pull request #3 from nvbn/master
Sync with master
2015-05-28 09:41:18 +08:00
Cami Diez
b1da6a883a Added java rule 2015-05-27 15:50:41 +08:00
Cami Diez
a9e3b22fa4 Added javac rule 2015-05-27 15:47:34 +08:00
Pablo Santiago Blum de Aguiar
9debcdf676 fix(shells::Fish): avoid looping when calling fuck twice
Or whatever the `thefuck` function name is.

Signed-off-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
2015-05-27 00:39:47 -03:00
Vladimir Iakovlev
718cadb85a #216 add open rule to readme 2015-05-23 18:49:20 +03:00
Vladimir Iakovlev
910e6f4759 Merge pull request #216 from diezcami/master
Addressed Issue #210
2015-05-23 18:45:52 +03:00
Cami Diez
d3146aa0ac Addressed Issue #210 2015-05-23 23:18:15 +08:00
nvbn
190e47ecdb #215 Use memoize decorator for caching 2015-05-22 17:07:01 +03:00
Vladimir Iakovlev
84a28d8c73 Merge pull request #215 from scorphus/fish-functions
Cache aliases to speed up subsequent calls and add support to Fish functions
2015-05-22 16:55:00 +03:00
Pablo Santiago Blum de Aguiar
551e35e3b6 refact(shells): add support to Fish functions
Signed-off-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
2015-05-21 23:56:37 -03:00
Pablo Santiago Blum de Aguiar
2bebfabf8d refact(shells): cache aliases to speed up subsequent calls
Signed-off-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
2015-05-21 23:56:28 -03:00
Vladimir Iakovlev
675317b247 Merge pull request #214 from scorphus/improve-man
refact(man): do not match if there's no argument to man
2015-05-21 15:42:26 +03:00
Pablo Santiago Blum de Aguiar
6cf430cc23 refact(man): do not match if there's no argument to man
If there's no argument to man, a call to thefuck should just give no
fuck.

Signed-off-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
2015-05-21 00:00:22 -03:00
Dugucloud
7e55041963 Merge branch 'master' of https://github.com/nvbn/thefuck 2015-05-10 15:46:06 +08:00
Dugucloud
fc364b99b9 Revert "Added colorama in requirements.txt"
This reverts commit 742f6f9c94.
2015-04-22 23:18:11 +08:00
Dugucloud
742f6f9c94 Added colorama in requirements.txt 2015-04-22 21:48:17 +08:00
秋纫
cd1bee9cb0 Merge pull request #2 from nvbn/master
Sync with master
2015-04-22 21:36:07 +08:00
85 changed files with 1716 additions and 213 deletions

View File

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

114
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)
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,34 +104,18 @@ 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`).
## Update
@@ -140,43 +124,65 @@ Changes will be available only in a new shell session.
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
using the matched rule and runs it. Rules enabled by default are as follows:
* `cargo` &ndash; runs `cargo build` instead of `cargo`;
* `cargo_no_command` &ndash; fixes wrongs commands like `cargo buid`;
* `cd_correction` &ndash; spellchecks and correct failed cd commands;
* `cd_mkdir` &ndash; creates directories before cd'ing into them;
* `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++`;
* `dry` &ndash; fix repetitions like "git git push";
* `cpp11` &ndash; adds missing `-std=c++11` to `g++` or `clang++`;
* `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; 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_checkout` &ndash; creates the branch before checking-out;
* `git_no_command` &ndash; fixes wrong git commands like `git brnch`;
* `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; fixes branch name or creates new branch;
* `git_diff_staged` &ndash; adds `--staged` to previous `git diff` with unexpected output;
* `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;
* `grep_recursive` &ndash; adds `-r` when you trying to grep directory;
* `go_run` &ndash; appends `.go` extension when compiling/running Go programs
* `grep_recursive` &ndash; adds `-r` when you trying to grep directory;
* `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;
* `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`;
* `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_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;
* `test.py` &ndash; runs `py.test` instead of `test.py`;
* `tmux` &ndash; fixes tmux commands;
* `whois` &ndash; fixes `whois` command.
Enabled by default only on specific platforms:
@@ -184,25 +190,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;
* `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 an
optional boolean `enabled_by_default`.
`Command` has three attributes: `script`, `stdout` and `stderr`.
`Settings` is a special object filled with `~/.thefuck/settings.py` and values from env, [more](#settings).
`Settings` is a special object filled with `~/.thefuck/settings.py` and values from env ([see more below](#settings)).
Simple example of the rule for running script with `sudo`:
@@ -221,7 +232,7 @@ enabled_by_default = True
def side_effect(command, settings):
subprocess.call('chmod 777 .', shell=True)
priority = 1000 # Lower first
priority = 1000 # Lower first, default is 1000
```
[More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules),
@@ -232,10 +243,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`:
@@ -245,6 +257,7 @@ require_confirmation = True
wait_command = 10
no_colors = False
priority = {'sudo': 100, 'no_command': 9999}
debug = False
```
Or via environment variables:
@@ -254,7 +267,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:

BIN
example.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

View File

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

19
setup.py Normal file → Executable file
View File

@@ -1,8 +1,20 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import sys
if sys.version_info < (2, 7):
print('thefuck requires Python version 2.7 or later' +
' ({}.{} detected).'.format(*sys.version_info[:2]))
sys.exit(-1)
elif (3, 0) < sys.version_info < (3, 3):
print('thefuck requires Python version 3.3 or later' +
' ({}.{} detected).'.format(*sys.version_info[:2]))
sys.exit(-1)
VERSION = '1.43'
VERSION = '2.3'
install_requires = ['psutil', 'colorama', 'six']
extras_require = {':python_version<"3.4"': ['pathlib']}
setup(name='thefuck',
version=VERSION,
@@ -15,7 +27,8 @@ setup(name='thefuck',
'tests', 'release']),
include_package_data=True,
zip_safe=False,
install_requires=['pathlib', 'psutil', 'colorama', 'six'],
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

@@ -0,0 +1,15 @@
import pytest
from thefuck.rules.brew_upgrade import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='brew upgrade')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('brew upgrade'), 'brew upgrade --all')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,21 @@
import pytest
from thefuck.rules.cargo_no_command import match, get_new_command
from tests.utils import Command
no_such_subcommand = """No such subcommand
Did you mean `build`?
"""
@pytest.mark.parametrize('command', [
Command(script='cargo buid', stderr=no_such_subcommand)])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('cargo buid', stderr=no_such_subcommand), 'cargo build')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
from thefuck import shells
from thefuck.rules.git_branch_list import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('git branch list'), None)
def test_not_match():
assert not match(Command(), None)
assert not match(Command('git commit'), None)
assert not match(Command('git branch'), None)
assert not match(Command('git stash list'), None)
def test_get_new_command():
assert (get_new_command(Command('git branch list'), None) ==
shells.and_('git branch --delete list', 'git branch'))

View File

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

View File

@@ -0,0 +1,26 @@
import pytest
from thefuck.rules.git_diff_staged import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
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')])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('git diff'), 'git diff --staged'),
(Command('git diff foo'), 'git diff --staged foo')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

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

View File

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

View File

@@ -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,17 @@
import pytest
from thefuck.rules.go_run import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='go run foo'),
Command(script='go run bar')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('go run foo'), 'go run foo.go'),
(Command('go run bar'), 'go run bar.go')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

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

17
tests/rules/test_java.py Normal file
View File

@@ -0,0 +1,17 @@
import pytest
from thefuck.rules.java import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='java foo.java'),
Command(script='java bar.java')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('java foo.java'), 'java foo'),
(Command('java bar.java'), 'java bar')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

17
tests/rules/test_javac.py Normal file
View File

@@ -0,0 +1,17 @@
import pytest
from thefuck.rules.javac import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='javac foo'),
Command(script='javac bar')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('javac foo'), 'javac foo.java'),
(Command('javac bar'), 'javac bar.java')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -3,9 +3,12 @@ from thefuck.rules.ls_lah import match, get_new_command
def test_match():
assert match(Mock(script='ls'), None)
assert match(Mock(script='ls file.py'), None)
assert match(Mock(script='ls /opt'), None)
assert not match(Mock(script='ls -lah /opt'), None)
assert not match(Mock(script='pacman -S binutils'), None)
assert not match(Mock(script='lsof'), None)
def test_get_new_command():

View File

@@ -2,6 +2,7 @@ import pytest
from thefuck.rules.man import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command('man read'),
Command('man 2 read'),
@@ -14,6 +15,13 @@ def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command('man'),
Command('man ')])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('man read'), 'man 3 read'),
(Command('man 2 read'), 'man 3 read'),

View File

@@ -0,0 +1,134 @@
import pytest
from tests.utils import Command
from thefuck.rules.mercurial import (
extract_possisiblities, match, get_new_command
)
@pytest.mark.parametrize('command', [
Command('hg base', stderr=(
"hg: unknown command 'base'"
'\n(did you mean one of blame, phase, rebase?)'
)),
Command('hg branchch', stderr=(
"hg: unknown command 'branchch'"
'\n(did you mean one of branch, branches?)'
)),
Command('hg vert', stderr=(
"hg: unknown command 'vert'"
'\n(did you mean one of revert?)'
)),
Command('hg lgo -r tip', stderr=(
"hg: command 're' is ambiguous:"
'\n(did you mean one of log?)'
)),
Command('hg rerere', stderr=(
"hg: unknown command 'rerere'"
'\n(did you mean one of revert?)'
)),
Command('hg re', stderr=(
"hg: command 're' is ambiguous:"
'\n rebase recover remove rename resolve revert'
)),
Command('hg re re', stderr=(
"hg: command 're' is ambiguous:"
'\n rebase recover remove rename resolve revert'
)),
])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command('hg', stderr=(
'\nMercurial Distributed SCM\n\nbasic commands:'
)),
Command('hg asdf', stderr=(
"hg: unknown command 'asdf'"
'\nMercurial Distributed SCM\n\nbasic commands:'
)),
Command('hg qwer', stderr=(
"hg: unknown command 'qwer'"
'\nMercurial Distributed SCM\n\nbasic commands:'
)),
Command('hg me', stderr=(
"\nabort: no repository found in './thefuck' (.hg not found)!"
)),
Command('hg reb', stderr=(
"\nabort: no repository found in './thefuck' (.hg not found)!"
)),
Command('hg co', stderr=(
"\nabort: no repository found in './thefuck' (.hg not found)!"
)),
])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, possibilities', [
(Command('hg base', stderr=(
"hg: unknown command 'base'"
'\n(did you mean one of blame, phase, rebase?)'
)), ['blame', 'phase', 'rebase']),
(Command('hg branchch', stderr=(
"hg: unknown command 'branchch'"
'\n(did you mean one of branch, branches?)'
)), ['branch', 'branches']),
(Command('hg vert', stderr=(
"hg: unknown command 'vert'"
'\n(did you mean one of revert?)'
)), ['revert']),
(Command('hg lgo -r tip', stderr=(
"hg: command 're' is ambiguous:"
'\n(did you mean one of log?)'
)), ['log']),
(Command('hg rerere', stderr=(
"hg: unknown command 'rerere'"
'\n(did you mean one of revert?)'
)), ['revert']),
(Command('hg re', stderr=(
"hg: command 're' is ambiguous:"
'\n rebase recover remove rename resolve revert'
)), ['rebase', 'recover', 'remove', 'rename', 'resolve', 'revert']),
(Command('hg re re', stderr=(
"hg: command 're' is ambiguous:"
'\n rebase recover remove rename resolve revert'
)), ['rebase', 'recover', 'remove', 'rename', 'resolve', 'revert']),
])
def test_extract_possisiblities(command, possibilities):
assert extract_possisiblities(command) == possibilities
@pytest.mark.parametrize('command, new_command', [
(Command('hg base', stderr=(
"hg: unknown command 'base'"
'\n(did you mean one of blame, phase, rebase?)'
)), 'hg rebase'),
(Command('hg branchch', stderr=(
"hg: unknown command 'branchch'"
'\n(did you mean one of branch, branches?)'
)), 'hg branch'),
(Command('hg vert', stderr=(
"hg: unknown command 'vert'"
'\n(did you mean one of revert?)'
)), 'hg revert'),
(Command('hg lgo -r tip', stderr=(
"hg: command 're' is ambiguous:"
'\n(did you mean one of log?)'
)), 'hg log -r tip'),
(Command('hg rerere', stderr=(
"hg: unknown command 'rerere'"
'\n(did you mean one of revert?)'
)), 'hg revert'),
(Command('hg re', stderr=(
"hg: command 're' is ambiguous:"
'\n rebase recover remove rename resolve revert'
)), 'hg rebase'),
(Command('hg re re', stderr=(
"hg: command 're' is ambiguous:"
'\n rebase recover remove rename resolve revert'
)), 'hg rebase re'),
])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

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

25
tests/rules/test_open.py Normal file
View File

@@ -0,0 +1,25 @@
import pytest
from thefuck.rules.open import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='open foo.com'),
Command(script='open foo.ly'),
Command(script='open foo.org'),
Command(script='open foo.net'),
Command(script='open foo.se'),
Command(script='open foo.io')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('open foo.com'), 'open http://foo.com'),
(Command('open foo.ly'), 'open http://foo.ly'),
(Command('open foo.org'), 'open http://foo.org'),
(Command('open foo.net'), 'open http://foo.net'),
(Command('open foo.se'), 'open http://foo.se'),
(Command('open foo.io'), 'open http://foo.io')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -23,23 +23,25 @@ extra/vim-python3 7.4.712-1 \t/usr/bin/vim
@pytest.mark.skipif(not getattr(pacman, 'enabled_by_default', True),
reason='Skip if pacman is not available')
@pytest.mark.parametrize('command', [
Command(script='vim', stderr='vim: command not found')])
Command(script='vim', stderr='vim: command not found'),
Command(script='sudo vim', stderr='sudo: vim: command not found')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, return_value', [
(Command(script='vim', stderr='vim: command not found'), PKGFILE_OUTPUT_VIM)])
(Command(script='vim', stderr='vim: command not found'), PKGFILE_OUTPUT_VIM),
(Command(script='sudo vim', stderr='sudo: vim: command not found'), PKGFILE_OUTPUT_VIM)])
@patch('thefuck.rules.pacman.subprocess')
@patch.multiple(pacman, create=True, pacman=pacman_cmd)
def test_match_mocked(subp_mock, command, return_value):
subp_mock.check_output.return_value = return_value
assert match(command, None)
assert subp_mock.check_output.called
@pytest.mark.parametrize('command', [
Command(script='vim', stderr=''), Command()])
Command(script='vim', stderr=''), Command(),
Command(script='sudo vim', stderr=''), Command()])
def test_not_match(command):
assert not match(command, None)
@@ -48,19 +50,20 @@ def test_not_match(command):
reason='Skip if pacman is not available')
@pytest.mark.parametrize('command, new_command', [
(Command('vim'), '{} -S extra/gvim && vim'.format(pacman_cmd)),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd))])
(Command('sudo vim'), '{} -S extra/gvim && sudo vim'.format(pacman_cmd)),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd)),
(Command('sudo convert'), '{} -S extra/imagemagick && sudo convert'.format(pacman_cmd))])
def test_get_new_command(command, new_command, mocker):
assert get_new_command(command, None) == new_command
@pytest.mark.parametrize('command, new_command, return_value', [
(Command('vim'), '{} -S extra/gvim && vim'.format(pacman_cmd),
PKGFILE_OUTPUT_VIM),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd),
PKGFILE_OUTPUT_CONVERT)])
(Command('vim'), '{} -S extra/gvim && vim'.format(pacman_cmd), PKGFILE_OUTPUT_VIM),
(Command('sudo vim'), '{} -S extra/gvim && sudo vim'.format(pacman_cmd), PKGFILE_OUTPUT_VIM),
(Command('convert'), '{} -S extra/imagemagick && convert'.format(pacman_cmd), PKGFILE_OUTPUT_CONVERT),
(Command('sudo convert'), '{} -S extra/imagemagick && sudo convert'.format(pacman_cmd), PKGFILE_OUTPUT_CONVERT)])
@patch('thefuck.rules.pacman.subprocess')
@patch.multiple(pacman, create=True, pacman=pacman_cmd)
def test_get_new_command_mocked(subp_mock, command, new_command, return_value):
subp_mock.check_output.return_value = return_value
assert get_new_command(command, None) == new_command
assert subp_mock.check_output.called

View File

@@ -0,0 +1,17 @@
import pytest
from thefuck.rules.python_execute import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='python foo'),
Command(script='python bar')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('python foo'), 'python foo.py'),
(Command('python bar'), 'python bar.py')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,19 @@
import pytest
from thefuck.rules.quotation_marks import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script="git commit -m \'My Message\""),
Command(script="git commit -am \"Mismatched Quotation Marks\'"),
Command(script="echo \"hello\'")])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command("git commit -m \'My Message\""), "git commit -m \"My Message\""),
(Command("git commit -am \"Mismatched Quotation Marks\'"), "git commit -am \"Mismatched Quotation Marks\""),
(Command("echo \"hello\'"), "echo \"hello\"")])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,28 @@
import pytest
from thefuck.rules.sed_unterminated_s import match, get_new_command
from tests.utils import Command
@pytest.fixture
def sed_unterminated_s():
return "sed: -e expression #1, char 9: unterminated `s' command"
def test_match(sed_unterminated_s):
assert match(Command('sed -e s/foo/bar', stderr=sed_unterminated_s), None)
assert match(Command('sed -es/foo/bar', stderr=sed_unterminated_s), None)
assert match(Command('sed -e s/foo/bar -e s/baz/quz', stderr=sed_unterminated_s), None)
assert not match(Command('sed -e s/foo/bar'), None)
assert not match(Command('sed -es/foo/bar'), None)
assert not match(Command('sed -e s/foo/bar -e s/baz/quz'), None)
def test_get_new_command(sed_unterminated_s):
assert get_new_command(Command('sed -e s/foo/bar', stderr=sed_unterminated_s), None) \
== 'sed -e s/foo/bar/'
assert get_new_command(Command('sed -es/foo/bar', stderr=sed_unterminated_s), None) \
== 'sed -es/foo/bar/'
assert get_new_command(Command(r"sed -e 's/\/foo/bar'", stderr=sed_unterminated_s), None) \
== r"sed -e 's/\/foo/bar/'"
assert get_new_command(Command(r"sed -e s/foo/bar -es/baz/quz", stderr=sed_unterminated_s), None) \
== r"sed -e s/foo/bar/ -es/baz/quz/"

View File

@@ -8,6 +8,10 @@ from tests.utils import Command
('permission denied', ''),
("npm ERR! Error: EACCES, unlink", ''),
('requested operation requires superuser privilege', ''),
('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

@@ -0,0 +1,18 @@
import pytest
from thefuck.rules.systemctl import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('systemctl nginx start', stderr='Unknown operation \'nginx\'.'), None)
assert match(Command('sudo systemctl nginx start', stderr='Unknown operation \'nginx\'.'), None)
assert not match(Command('systemctl start nginx'), None)
assert not match(Command('systemctl start nginx'), None)
assert not match(Command('sudo systemctl nginx', stderr='Unknown operation \'nginx\'.'), None)
assert not match(Command('systemctl nginx', stderr='Unknown operation \'nginx\'.'), None)
assert not match(Command('systemctl start wtf', stderr='Failed to start wtf.service: Unit wtf.service failed to load: No such file or directory.'), None)
def test_get_new_command():
assert get_new_command(Command('systemctl nginx start'), None) == "systemctl start nginx"
assert get_new_command(Command('sudo systemctl nginx start'), None) == "sudo systemctl start nginx"

19
tests/rules/test_tmux.py Normal file
View File

@@ -0,0 +1,19 @@
import pytest
from thefuck.rules.tmux import match, get_new_command
from tests.utils import Command
@pytest.fixture
def tmux_ambiguous():
return "ambiguous command: list, could be: " \
"list-buffers, list-clients, list-commands, list-keys, " \
"list-panes, list-sessions, list-windows"
def test_match(tmux_ambiguous):
assert match(Command('tmux list', stderr=tmux_ambiguous), None)
def test_get_new_command(tmux_ambiguous):
assert get_new_command(Command('tmux list', stderr=tmux_ambiguous), None)\
== 'tmux list-keys'

View File

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

View File

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

View File

@@ -12,6 +12,16 @@ def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True)
@pytest.fixture
@pytest.mark.usefixtures('isfile')
def history_lines(mocker):
def aux(lines):
mock = mocker.patch('io.open')
mock.return_value.__enter__\
.return_value.__iter__.return_value = lines
return aux
class TestGeneric(object):
@pytest.fixture
def shell(self):
@@ -33,6 +43,18 @@ class TestGeneric(object):
def test_get_aliases(self, shell):
assert shell.get_aliases() == {}
def test_app_alias(self, shell):
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')
class TestBash(object):
@@ -44,6 +66,7 @@ class TestBash(object):
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'alias fuck=\'eval $(thefuck $(fc -ln -1))\'\n'
b'alias l=\'ls -CF\'\n'
b'alias la=\'ls -A\'\n'
b'alias ll=\'ls -alF\'')
@@ -51,6 +74,8 @@ class TestBash(object):
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('fuck', 'eval $(thefuck $(fc -ln -1))'),
('awk', 'awk'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
@@ -67,10 +92,21 @@ class TestBash(object):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'l': 'ls -CF',
assert shell.get_aliases() == {'fuck': 'eval $(thefuck $(fc -ln -1))',
'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
def test_app_alias(self, shell):
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')
class TestFish(object):
@@ -78,9 +114,38 @@ class TestFish(object):
def shell(self):
return shells.Fish()
@pytest.fixture(autouse=True)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
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'),
('ll', 'll')]) # Fish has no aliases but functions
('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"'),
('ls', 'ls')]) # Fish has no aliases but functions
def test_from_shell(self, before, after, shell):
assert shell.from_shell(before) == after
@@ -98,7 +163,22 @@ class TestFish(object):
assert shell.and_('foo', 'bar') == 'foo; and bar'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {}
assert shell.get_aliases() == {'fish_config': 'fish_config',
'fuck': 'fuck',
'funced': 'funced',
'funcsave': 'funcsave',
'history': 'history',
'll': 'll',
'math': 'math',
'popd': 'popd',
'pushd': 'pushd',
'ruby': 'ruby'}
def test_app_alias(self, shell):
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')
@@ -111,12 +191,14 @@ class TestZsh(object):
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'fuck=\'eval $(thefuck $(fc -ln -1 | tail -n 1))\'\n'
b'l=\'ls -CF\'\n'
b'la=\'ls -A\'\n'
b'll=\'ls -alF\'')
return mock
@pytest.mark.parametrize('before, after', [
('fuck', 'eval $(thefuck $(fc -ln -1 | tail -n 1))'),
('pwd', 'pwd'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after, shell):
@@ -136,6 +218,18 @@ class TestZsh(object):
assert shell.and_('ls', 'cd') == 'ls && cd'
def test_get_aliases(self, shell):
assert shell.get_aliases() == {'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
assert shell.get_aliases() == {
'fuck': 'eval $(thefuck $(fc -ln -1 | tail -n 1))',
'l': 'ls -CF',
'la': 'ls -A',
'll': 'ls -alF'}
def test_app_alias(self, shell):
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
from thefuck.utils import git_support, sudo_support, wrap_settings,\
memoize, get_closest, get_all_executables
from thefuck.types import Settings
from tests.utils import Command
@@ -24,3 +25,70 @@ def test_sudo_support(return_value, command, called, result):
fn = Mock(return_value=return_value, __name__='')
assert sudo_support(fn)(Command(command), None) == 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)
memoized()
memoized()
fn.assert_called_once_with()
@pytest.mark.usefixtures('no_memoize')
def test_no_memoize():
fn = Mock(__name__='fn')
memoized = memoize(fn)
memoized()
memoized()
assert fn.call_count == 2
class TestGetClosest(object):
def test_when_can_match(self):
assert 'branch' == get_closest('brnch', ['branch', 'status'])
def test_when_cant_match(self):
assert 'status' == get_closest('st', ['status', 'reset'])
def test_without_fallback(self):
assert get_closest('st', ['status', 'reset'],
fallback_to_first=False) is None
@pytest.fixture
def get_aliases(mocker):
mocker.patch('thefuck.shells.get_aliases',
return_value=['vim', 'apt-get', 'fsck', 'fuck'])
@pytest.mark.usefixtures('no_memoize', 'get_aliases')
def test_get_all_callables():
all_callables = get_all_executables()
assert 'vim' in all_callables
assert 'fsck' in all_callables
assert 'fuck' not in all_callables

View File

@@ -27,15 +27,18 @@ DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3,
'require_confirmation': False,
'require_confirmation': True,
'no_colors': False,
'priority': {}}
'debug': False,
'priority': {},
'env': {'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,4 @@
from pprint import pformat
import sys
from traceback import format_exception
import colorama
@@ -52,3 +53,12 @@ def failed(msg, settings):
msg=msg,
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def debug(msg, settings):
if settings.debug:
sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format(
msg=msg,
reset=color(colorama.Style.RESET_ALL, settings),
blue=color(colorama.Fore.BLUE, settings),
bold=color(colorama.Style.BRIGHT, settings)))

View File

@@ -1,6 +1,7 @@
from imp import load_source
from pathlib import Path
from os.path import expanduser
from pprint import pformat
from subprocess import Popen, PIPE
import os
import sys
@@ -62,7 +63,7 @@ def wait_output(settings, popen):
proc.wait(settings.wait_command)
return True
except TimeoutExpired:
for child in proc.get_children(recursive=True):
for child in proc.children(recursive=True):
child.kill()
proc.kill()
return False
@@ -79,8 +80,13 @@ def get_command(settings, args):
return
script = shells.from_shell(script)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C'))
logs.debug(u'Call: {}'.format(script), settings)
env = dict(os.environ)
env.update(settings.env)
logs.debug(u'Executing with env: {}'.format(env), settings)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env)
if wait_output(settings, result):
return types.Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8'))
@@ -90,6 +96,7 @@ def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command."""
for rule in rules:
try:
logs.debug(u'Trying rule: {}'.format(rule.name), settings)
if rule.match(command, settings):
return rule
except Exception:
@@ -121,17 +128,35 @@ def run_rule(rule, command, settings):
print(new_command)
# Entry points:
def main():
colorama.init()
user_dir = setup_user_dir()
settings = conf.get_settings(user_dir)
logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings)
command = get_command(settings, sys.argv)
if command:
logs.debug(u'Received stdout: {}'.format(command.stdout), settings)
logs.debug(u'Received stderr: {}'.format(command.stderr), settings)
rules = get_rules(user_dir, settings)
logs.debug(
u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)),
settings)
matched_rule = get_matched_rule(command, rules, settings)
if matched_rule:
logs.debug(u'Matched rule: {}'.format(matched_rule.name), settings)
run_rule(matched_rule, command, settings)
return
logs.failed('No fuck given', settings)
def print_alias():
alias = shells.thefuck_alias()
if len(sys.argv) > 1:
alias = sys.argv[1]
print(shells.app_alias(alias))

View File

@@ -1,11 +1,12 @@
from thefuck import shells
from thefuck.utils import sudo_support
try:
import CommandNotFound
except ImportError:
enabled_by_default = False
@sudo_support
def match(command, settings):
if 'not found' in command.stderr:
try:
@@ -17,7 +18,7 @@ def match(command, settings):
# IndexError is thrown when no matching package is found
return False
@sudo_support
def get_new_command(command, settings):
c = CommandNotFound.CommandNotFound()
pkgs = c.getPackages(command.script.split(" ")[0])

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
# Appends --all to the brew upgrade command
#
# Example:
# > brew upgrade
# Warning: brew upgrade with no arguments will change behaviour soon!
# It currently upgrades all formula but this will soon change to require '--all'.
#
#
def match(command, settings):
return (command.script == 'brew upgrade')
def get_new_command(command, settings):
return command.script + ' --all'

6
thefuck/rules/cargo.py Normal file
View File

@@ -0,0 +1,6 @@
def match(command, settings):
return command.script == 'cargo'
def get_new_command(command, settings):
return 'cargo build'

View File

@@ -0,0 +1,14 @@
import re
def match(command, settings):
return ('cargo' in command.script
and 'No such subcommand' in command.stderr
and 'Did you mean' in command.stderr)
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)

View File

@@ -47,7 +47,7 @@ def get_new_command(command, settings):
cwd = os.path.join(cwd, best_matches[0])
else:
return cd_mkdir.get_new_command(command, settings)
return "cd {0}".format(cwd)
return 'cd "{0}"'.format(cwd)
enabled_by_default = True

View File

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

View File

@@ -1,13 +1,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,12 @@
from thefuck import utils
@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 command.script.replace('-d', '-D')

View File

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

View File

@@ -0,0 +1,12 @@
from thefuck import utils
@utils.git_support
def match(command, settings):
return ('diff' in command.script and
'--staged' not in command.script)
@utils.git_support
def get_new_command(command, settings):
return command.script.replace(' diff', ' diff --staged')

View File

@@ -1,16 +1,27 @@
import re
from thefuck.utils import get_closest, git_support
@git_support
def match(command, settings):
return ('git' in command.script
and " is not a git command. See 'git --help'." in command.stderr
return (" is not a git command. See 'git --help'." in command.stderr
and 'Did you mean' in command.stderr)
def _get_all_git_matched_commands(stderr):
should_yield = False
for line in stderr.split('\n'):
if 'Did you mean' in line:
should_yield = True
elif should_yield and line:
yield line.strip()
@git_support
def get_new_command(command, settings):
broken_cmd = re.findall(r"git: '([^']*)' is not a git command",
command.stderr)[0]
new_cmd = re.findall(r'Did you mean[^\n]*\n\s*([^\n]*)',
command.stderr)[0]
new_cmd = get_closest(broken_cmd,
_get_all_git_matched_commands(command.stderr))
return command.script.replace(broken_cmd, new_cmd, 1)

View File

@@ -1,12 +1,16 @@
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]
set_upstream = line.replace('<remote>', 'origin')\
.replace('<branch>', branch)
return u'{} && {}'.format(set_upstream, command.script)
return shells.and_(set_upstream, command.script)

View File

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

View File

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

View File

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

View File

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

14
thefuck/rules/go_run.py Normal file
View File

@@ -0,0 +1,14 @@
# Appends .go when compiling go files
#
# Example:
# > go run foo
# error: go run: no go files listed
#
#
def match(command, settings):
return (command.script.startswith ('go run ')
and not command.script.endswith('.go'))
def get_new_command(command, settings):
return command.script + '.go'

View File

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

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

14
thefuck/rules/java.py Normal file
View File

@@ -0,0 +1,14 @@
# Fixes common java command mistake
#
# Example:
# > java foo.java
# Error: Could not find or load main class foo.java
def match(command, settings):
return (command.script.startswith('java ')
and command.script.endswith('.java'))
def get_new_command(command, settings):
return command.script[:-5]

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

@@ -0,0 +1,15 @@
# Appends .java when compiling java files
#
# Example:
# > javac foo
# error: Class names, 'foo', are only accepted if annotation
# processing is explicitly requested
def match(command, settings):
return (command.script.startswith('javac ')
and not command.script.endswith('.java'))
def get_new_command(command, settings):
return command.script + '.java'

View File

@@ -1,5 +1,7 @@
def match(command, settings):
return 'ls' in command.script and not ('ls -' in command.script)
return (command.script == 'ls'
or command.script.startswith('ls ')
and not ('ls -' in command.script))
def get_new_command(command, settings):

View File

@@ -1,5 +1,5 @@
def match(command, settings):
return command.script.startswith('man')
return command.script.strip().startswith('man ')
def get_new_command(command, settings):

View File

@@ -0,0 +1,29 @@
import re
from thefuck.utils import get_closest
def extract_possisiblities(command):
possib = re.findall(r'\n\(did you mean one of ([^\?]+)\?\)', command.stderr)
if possib:
return possib[0].split(', ')
possib = re.findall(r'\n ([^$]+)$', command.stderr)
if possib:
return possib[0].split(' ')
return possib
def match(command, settings):
return (command.script.startswith('hg ')
and ('hg: unknown command' in command.stderr
and '(did you mean one of ' in command.stderr
or "hg: command '" in command.stderr
and "' is ambiguous:" in command.stderr
)
)
def get_new_command(command, settings):
script = command.script.split(' ')
possisiblities = extract_possisiblities(command)
script[1] = get_closest(script[1], possisiblities)
return ' '.join(script)

View File

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

26
thefuck/rules/open.py Normal file
View File

@@ -0,0 +1,26 @@
# Opens URL's in the default web browser
#
# Example:
# > open github.com
# The file ~/github.com does not exist.
# Perhaps you meant 'http://github.com'?
#
def match(command, settings):
return (command.script.startswith(('open', 'xdg-open', 'gnome-open', 'kde-open'))
and (
'.com' in command.script
or '.net' in command.script
or '.org' in command.script
or '.ly' in command.script
or '.io' in command.script
or '.se' in command.script
or '.edu' in command.script
or '.info' in command.script
or '.me' in command.script
or 'www.' in command.script))
def get_new_command(command, settings):
return 'open http://' + command.script[5:]

View File

@@ -1,12 +1,21 @@
import subprocess
from thefuck.utils import DEVNULL, which
from thefuck import shells
from thefuck.utils import memoize
@memoize
def __get_pkgfile(command):
try:
command = command.script
if command.startswith('sudo'):
command = command[5:]
command = command.split(" ")[0]
return subprocess.check_output(
['pkgfile', '-b', '-v', command.script.split(" ")[0]],
['pkgfile', '-b', '-v', command],
universal_newlines=True, stderr=DEVNULL
).split()
except subprocess.CalledProcessError:

View File

@@ -0,0 +1,14 @@
# Appends .py when executing python files
#
# Example:
# > python foo
# error: python: can't open file 'foo': [Errno 2] No such file or directory
def match(command, settings):
return (command.script.startswith('python ')
and not command.script.endswith('.py'))
def get_new_command(command, settings):
return command.script + '.py'

View File

@@ -0,0 +1,14 @@
# Fixes careless " and ' usage
#
# Example:
# > git commit -m 'My Message"
#
#
#
def match(command, settings):
return ('\'' in command.script
and '\"' in command.script)
def get_new_command(command, settings):
return command.script.replace ('\'', '\"')

View File

@@ -0,0 +1,17 @@
import shlex
from thefuck.utils import quote
def match(command, settings):
return ('sed' in command.script
and "unterminated `s' command" in command.stderr)
def get_new_command(command, settings):
script = shlex.split(command.script)
for (i, e) in enumerate(script):
if e.startswith(('s/', '-es/')) and e[-1] != '/':
script[i] += '/'
return ' '.join(map(quote, script))

View File

@@ -7,11 +7,15 @@ patterns = ['permission denied',
'root privilege',
'This command has to be run under the root user.',
'This operation requires root.',
'You need to be root to perform this command.',
'requested operation requires superuser privilege',
'must be run as root',
'must be superuser',
'Need to be root']
'must be root',
'need to be root',
'need root',
'only root can do that',
'You don\'t have access to the history DB.',
'authentication is required']
def match(command, settings):

View File

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

View File

@@ -0,0 +1,21 @@
"""
The confusion in systemctl's param order is massive.
"""
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
# Catches 'Unknown operation 'service'.' when executing systemctl with
# misordered arguments
cmd = command.script.split()
return ('systemctl' in command.script and
'Unknown operation \'' in command.stderr and
len(cmd) - cmd.index('systemctl') == 3)
@sudo_support
def get_new_command(command, settings):
cmd = command.script.split()
cmd[-1], cmd[-2] = cmd[-2], cmd[-1]
return ' '.join(cmd)

10
thefuck/rules/test.py.py Normal file
View File

@@ -0,0 +1,10 @@
def match(command, settings):
return command.script == 'test.py' and 'not found' in command.stderr
def get_new_command(command, settings):
return 'py.test'
# make it come before the python_command rule
priority = 900

20
thefuck/rules/tmux.py Normal file
View File

@@ -0,0 +1,20 @@
from thefuck.utils import get_closest
import re
def match(command, settings):
return ('tmux' in command.script
and 'ambiguous command:' in command.stderr
and 'could be:' in command.stderr)
def get_new_command(command, settings):
cmd = re.match(r"ambiguous command: (.*), could be: (.*)",
command.stderr)
old_cmd = cmd.group(1)
suggestions = [cmd.strip() for cmd in cmd.group(2).split(',')]
new_cmd = get_closest(old_cmd, suggestions)
return command.script.replace(old_cmd, new_cmd)

View File

@@ -4,14 +4,16 @@ 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
from .utils import DEVNULL, memoize
class Generic(object):
def get_aliases(self):
return {}
@@ -31,8 +33,8 @@ class Generic(object):
"""Prepares command for running in shell."""
return command_script
def app_alias(self):
return "\nalias 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 ''
@@ -47,13 +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 ' && '.join(commands)
return u' && '.join(commands)
class Bash(Generic):
def app_alias(self):
return "\nalias 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)
@@ -62,7 +85,8 @@ class Bash(Generic):
return name, value
def get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
@@ -75,10 +99,22 @@ 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 eval_script"
" (mktemp 2>/dev/null ; or mktemp -t 'thefuck')\n"
@@ -89,7 +125,26 @@ class Fish(Generic):
" if test $exit_code -ne 0\n"
" history --delete $fucked_up_commandd\n"
" end\n"
"end")
"end").format(fuck)
def get_aliases(self):
overridden = self._get_overridden_aliases()
proc = Popen('fish -ic functions', stdout=PIPE, stderr=DEVNULL,
shell=True)
functions = proc.stdout.read().decode('utf-8').strip().split('\n')
return {func: func for func in functions if func not in overridden}
def _expand_aliases(self, command_script):
aliases = self.get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return u'fish -ic "{}"'.format(command_script.replace('"', r'\"'))
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def _get_history_file_name(self):
return os.path.expanduser('~/.config/fish/fish_history')
@@ -98,12 +153,14 @@ class Fish(Generic):
return u'- cmd: {}\n when: {}\n'.format(command_script, int(time()))
def and_(self, *commands):
return '; and '.join(commands)
return u'; and '.join(commands)
class Zsh(Generic):
def app_alias(self):
return "\nalias 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)
@@ -112,7 +169,8 @@ class Zsh(Generic):
return name, value
def get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
@@ -125,17 +183,26 @@ 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 '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
def get_aliases(self):
proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
proc = Popen('tcsh -ic alias', stdout=PIPE, stderr=DEVNULL,
shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
@@ -173,8 +240,12 @@ 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():
return os.environ.get('TF_ALIAS', 'fuck')
def put_to_history(command):
@@ -185,5 +256,11 @@ def and_(*commands):
return _get_shell().and_(*commands)
@memoize
def get_aliases():
return list(_get_shell().get_aliases().keys())
@memoize
def get_history():
return list(_get_shell().get_history())

View File

@@ -1,11 +1,21 @@
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
DEVNULL = open(os.devnull, 'w')
if six.PY2:
from pipes import quote
else:
from shlex import quote
def which(program):
"""Returns `program` path or `None`."""
@@ -62,3 +72,79 @@ def sudo_support(fn):
else:
return result
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 = {}
@wraps(fn)
def wrapper(*args, **kwargs):
key = pickle.dumps((args, kwargs))
if key not in memo or memoize.disabled:
memo[key] = fn(*args, **kwargs)
return memo[key]
return wrapper
memoize.disabled = False
def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True):
"""Returns closest match or just first from possibilities."""
possibilities = list(possibilities)
try:
return get_close_matches(word, possibilities, n, cutoff)[0]
except IndexError:
if fallback_to_first:
return possibilities[0]
@memoize
def get_all_executables():
from thefuck.shells import thefuck_alias, get_aliases
def _safe(fn, fallback):
try:
return fn()
except OSError:
return fallback
tf_alias = thefuck_alias()
return [exe.name
for path in os.environ.get('PATH', '').split(':')
for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)] + [
alias for alias in get_aliases() if alias != tf_alias]