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

Compare commits

...

143 Commits
1.20 ... 1.35

Author SHA1 Message Date
nvbn
a8ff2375c0 Bump to 1.35 2015-05-04 05:01:56 +02:00
Vladimir Iakovlev
80bfbec422 Update README.md 2015-05-04 05:00:11 +02:00
nvbn
3f2fe0d275 #89 #152 Use shell history 2015-05-04 04:44:16 +02:00
nvbn
72ac9650f9 Bump to 1.34 2015-05-03 13:25:01 +02:00
nvbn
93c90d5758 #157 Don't fail if can't get brew commands 2015-05-03 13:24:33 +02:00
nvbn
3ce8c1187c Make thefuck-alias depends on current shell 2015-05-03 13:04:33 +02:00
nvbn
bcd3154121 Bump to 1.33 2015-05-03 12:59:37 +02:00
nvbn
fcc2a1a40a #128 #69 add support of shell specific actions, add alias expansion for bash and zsh 2015-05-03 12:46:01 +02:00
nvbn
938f1df035 Remove not used fixture 2015-05-02 04:56:23 +02:00
nvbn
2acfea3350 #1 s/last_script/last_command/, s/last_fixed_script/last_fixed_command/ 2015-05-02 04:32:07 +02:00
nvbn
dd1861955c Refine tests 2015-05-02 04:29:55 +02:00
nvbn
ba601644d6 #1 Add history of last commands, allow fuck more than once 2015-05-01 08:38:38 +02:00
nvbn
fb7376f5a5 Bump to 1.32 2015-05-01 04:47:25 +02:00
nvbn
ee5c40d427 Update rules list in readme 2015-05-01 04:46:58 +02:00
nvbn
9a43ba6e24 #102 Update readme 2015-05-01 04:43:55 +02:00
nvbn
5eeb9d704c #102 Use side_effect in ssh_known_host rule 2015-05-01 04:41:33 +02:00
nvbn
b985dfbffc #102 Add support of rules with side effects 2015-05-01 04:39:37 +02:00
Vladimir Iakovlev
b928a59672 Merge pull request #150 from SanketDG/add-alias
Add thefuck-alias for outputting the alias command.
2015-04-30 20:53:22 +02:00
SanketDG
32fd929e48 add instructions to use thefuck-alias 2015-05-01 00:13:08 +05:30
SanketDG
8a49b40f6a add entry point 2015-05-01 00:12:43 +05:30
SanketDG
4276e1b991 add alias function 2015-05-01 00:12:30 +05:30
nvbn
6372674351 Merge branch 'SanketDG-sudo-shutdown' 2015-04-30 19:57:01 +02:00
nvbn
9f9c5369ec Merge branch 'sudo-shutdown' of https://github.com/SanketDG/thefuck into SanketDG-sudo-shutdown
Conflicts:
	thefuck/rules/sudo.py
2015-04-30 19:56:45 +02:00
Vladimir Iakovlev
50ab7429d9 Merge pull request #148 from danybony/patch-1
Add more patterns to sudo.py
2015-04-30 19:50:59 +02:00
SanketDG
55cfdda203 add rule for shutdown command 2015-04-30 19:50:37 +05:30
Daniele
be9446635b Add more patterns to sudo.py
These patterns cover commands like
`reboot`
or
`dpkg-reconfigure something`
2015-04-30 13:54:02 +01:00
Vladimir Iakovlev
b4cbcd7a99 Merge pull request #146 from kimtree/brew-improve
Improve a logic to get recommended command based on local environment
2015-04-29 08:48:20 +02:00
Namwoo Kim
9bf910a2dd Improve a logic to get recommended command based on local environment 2015-04-29 15:18:48 +09:00
Vladimir Iakovlev
7e76ab1dc6 Fix typo 2015-04-29 05:06:30 +02:00
nvbn
b80f3ea6e4 Bump to 1.31 2015-04-29 05:02:32 +02:00
nvbn
99d9c9aff7 #132 Merge pip_install_sudo rule with sudo rule 2015-04-29 05:01:30 +02:00
nvbn
4be60c78fa Merge branch 'pip-needs-sudo' of https://github.com/kimtree/thefuck into kimtree-pip-needs-sudo 2015-04-29 04:52:09 +02:00
Vladimir Iakovlev
847b9e2cec Merge pull request #143 from SanketDG/readme-fix
add version info to requirements
2015-04-29 04:48:48 +02:00
Vladimir Iakovlev
c463fea8a0 #145 add config examples 2015-04-29 04:47:15 +02:00
SanketDG
7652884df6 change requirements 2015-04-29 00:14:23 +05:30
Vladimir Iakovlev
e1ca120eb8 Merge pull request #137 from Sclarki/patch-1
Added dpkg rule
2015-04-28 20:32:41 +02:00
Trace
d3295e6a4e Hoping this fixes the issue of it not working.
Still new to this project. My problem of it not working (I admit to being at fault) was that I had not added the same rule to the test case.
2015-04-28 11:59:15 -05:00
Namwoo Kim
bc2c87e8fe Add a support for pip install with sudo
- Fixes #136
2015-04-28 15:52:09 +09:00
Trace
a2ac15da56 Added dpkg rule
For example, when using ```dpkg -i some-pkg.deb```
2015-04-27 17:45:59 -05:00
Vladimir Iakovlev
4cf631fa47 Fix links to wiki 2015-04-27 07:56:17 +02:00
Vladimir Iakovlev
cbf6507e1e Move aliases for specific shells to wiki 2015-04-27 07:54:40 +02:00
Vladimir Iakovlev
49ed98c8a4 Move os specific installation to wiki 2015-04-27 07:46:15 +02:00
Vladimir Iakovlev
52bf3907a1 Merge pull request #134 from firstdoit/patch-2
Fix typo: unoffical -> unofficial
2015-04-27 07:30:14 +02:00
Guilherme Rodrigues
077de17d6c Fix typo: unoffical -> unofficial 2015-04-26 20:35:04 -03:00
nvbn
419878f526 #118 Make ls_lah disabled by default 2015-04-25 03:42:36 +02:00
nvbn
f610cf2256 Merge branch 'ls_lah' of git://github.com/crimsonknave/thefuck into crimsonknave-ls_lah 2015-04-25 03:35:53 +02:00
nvbn
2f04a953ba Fix tests with python 2 2015-04-25 03:13:57 +02:00
nvbn
698451f65d Use parametrized tests where it possible 2015-04-25 02:54:39 +02:00
nvbn
b7cb407637 Add useful constructors for Rule and Command for tests 2015-04-25 02:35:26 +02:00
Vladimir Iakovlev
bb6b9a638c #124 Add note that brew package isn't offical 2015-04-25 02:11:07 +02:00
nvbn
0009fb0588 Bump to 1.30 2015-04-25 02:04:38 +02:00
Vladimir Iakovlev
a9d3456e29 Merge pull request #123 from nvbn/revert-117-master
Revert "Fixing fish shell example in README.md"
2015-04-24 18:25:05 +02:00
Vladimir Iakovlev
1e28671934 Revert "Fixing fish shell example in README.md" 2015-04-24 18:24:46 +02:00
Vladimir Iakovlev
3134a60e27 Merge pull request #120 from nwinkler/cd_mkdir
Added cd_mkdir rule
2015-04-24 18:23:22 +02:00
Vladimir Iakovlev
03dd7eda04 Merge pull request #119 from scorphus/initialize-settings
conf: initialize a settings file if it doesn't exist (fix #111)
2015-04-24 18:22:53 +02:00
Nils Winkler
d12a8bcdd8 Added cd_mkdir rule
This fixes #50 and #98.

```bash
$ cd foo/bar/baz
cd: foo: No such file or directory
$ fuck
mkdir -p foo/bar/baz && cd foo/bar/baz
```

Added matchers for both Bash and sh error messages. Depending on your
default shell, the messages might be slightly different.
2015-04-24 08:52:39 +02:00
Pablo Santiago Blum de Aguiar
58069f0a3e conf: initialize a settings file if it doesn't exist (fix #111)
Signed-off-by: Pablo Santiago Blum de Aguiar <scorphus@gmail.com>
2015-04-24 00:38:59 -03:00
nvbn
d0e02bc20c Merge branch 'master' of github.com:nvbn/thefuck 2015-04-24 05:23:04 +02:00
nvbn
e554238996 #78 Disable when can't import CommandNotFound 2015-04-24 05:22:19 +02:00
nvbn
fa465620ba Merge branch 'master' of git://github.com/luv/thefuck 2015-04-24 05:13:37 +02:00
Vladimir Iakovlev
294ba07ce1 Merge pull request #117 from J3RN/master
Fixing fish shell example in README.md
2015-04-24 04:37:27 +02:00
Joseph Henrich
e21befbcc4 add ls_lah to readme 2015-04-23 20:33:38 -04:00
Joseph Henrich
a454438939 Fuck, I wanted ls -lah 2015-04-23 20:28:19 -04:00
Jonathan Arnett
1fa7827f1a Fixing fish shell example in README.md
For me, `$history[1]` is the currently running command, so for the last one you want `$history[2]`
2015-04-23 18:35:18 -04:00
nvbn
48ec853436 Bump to 1.29 2015-04-23 21:50:38 +02:00
nvbn
5a8d889dc0 Merge branch 'master' of github.com:nvbn/thefuck 2015-04-23 21:48:05 +02:00
nvbn
1f96faef2c #116 Fix tests 2015-04-23 21:47:46 +02:00
nvbn
0235c0654d Merge branch 'master' of git://github.com/neomede/thefuck into neomede-master 2015-04-23 21:45:26 +02:00
Vladimir Iakovlev
f286033f82 Merge pull request #114 from nwinkler/fix-alt-space
Added rule for fixing Alt+Space character
2015-04-23 18:07:44 +02:00
Rubén Simón Andreo
473f5e6a33 Add composer rule 2015-04-23 17:34:34 +02:00
Nils Winkler
f1cce413b3 Added rule for fixing Alt+Space character
Happens on the Mac a lot when typing a pipe character (Alt+7), and
keeping the Alt key pressed down for a bit too long, so instead of
Space, you're typing Alt+Space. This rule replaces the Alt+Space with a
simple Space character.

$ ps -ef | grep foo
-bash:  grep: command not found
$ fuck
ps -ef | grep foo
2015-04-23 15:19:30 +02:00
Vladimir Iakovlev
ee2b208adf Merge pull request #112 from nwinkler/eval-alias
Using eval for Bash alias
2015-04-23 15:06:23 +02:00
Vladimir Iakovlev
a20bf6fa23 Merge pull request #110 from kimtree/support-brew
Support brew unknown command
2015-04-23 15:04:18 +02:00
Vladimir Iakovlev
da050f0db3 Merge pull request #109 from bethrezen/patch-1
MacOSX specific message
2015-04-23 15:03:20 +02:00
Vladimir Iakovlev
f5e9124327 Merge pull request #107 from kimtree/support-pip
Add a support for pip unknown commands
2015-04-23 15:02:56 +02:00
Vladimir Iakovlev
1f38e0a932 Merge pull request #106 from Brobin/master
New rule: sl -> ls
2015-04-23 15:01:57 +02:00
Nils Winkler
380827d1d9 Using eval for Bash alias
This fixes #108.
2015-04-23 11:26:19 +02:00
Namwoo Kim
54b5cd6122 Add a support for brew unavailable formulas 2015-04-23 18:16:36 +09:00
Namwoo Kim
9611264210 Update README.md 2015-04-23 17:06:36 +09:00
Namwoo Kim
24ce459f2c Add a support for unknown brew commands - #83 2015-04-23 17:06:05 +09:00
Alexander Kozhevnikov
07b9aba0d0 MacOSX specific message
Patch for understanding macosx message.
Example case:
```
[10:24:48][bethrezen@bethrezen-mac ~]$ apachectl graceful
This operation requires root.
[10:24:54][bethrezen@bethrezen-mac ~]$ fuck
No fuck given
```
2015-04-23 10:29:34 +03:00
Namwoo Kim
bb42780ca5 Update README.md and remove whitespaces 2015-04-23 16:05:02 +09:00
Namwoo Kim
af2bfe7c58 Add a support for pip unknown commands 2015-04-23 15:25:12 +09:00
Brobin
157e3e95fc added sl_ls test :shipit: 2015-04-22 20:51:18 -05:00
Brobin
776ff4e3db updated readme for sl_ls 2015-04-22 20:45:12 -05:00
Brobin
5de020bccd unf*ck sl -> ls 2015-04-22 20:41:56 -05:00
nvbn
0272e8a801 Bump to 1.28 2015-04-22 23:37:02 +02:00
nvbn
2e652112ff Merge branch 'master' of github.com:nvbn/thefuck 2015-04-22 23:36:43 +02:00
Vladimir Iakovlev
12eab10028 Update README.md 2015-04-22 23:08:10 +02:00
Vladimir Iakovlev
61eab83789 Merge pull request #101 from scott-abernethy/brew-install
Brew installation note in README
2015-04-22 23:05:25 +02:00
nvbn
d3d1f99232 Move special data types to types 2015-04-22 23:04:22 +02:00
Scott Abernethy
ca67080bd9 Brew installation note in README 2015-04-23 09:00:18 +12:00
nvbn
54c408a6b5 Rename DEFAULT to DEFAULT_RULES 2015-04-22 22:37:11 +02:00
nvbn
20b6c4c160 Inherit RulesNamesList from list 2015-04-22 22:36:18 +02:00
nvbn
0553d57ec1 Don't mess with inheritance for filling settings 2015-04-22 22:29:23 +02:00
Vladimir Iakovlev
e046d55de8 Merge pull request #99 from timofurrer/master
fix rm dir rule to make it case insensitive
2015-04-22 20:20:00 +02:00
nvbn
69a9516477 Add ability to change settings via environment variables 2015-04-22 20:18:53 +02:00
Timo Furrer
c788dfbc14 fix rm dir rule to make it case insensitive
In bash the output for the command `rm -f foo/` is:

    rm: cannot remove ‘foo/’: Is a directory

And not:

    rm: cannot remove ‘foo/’: is a directory
2015-04-22 19:04:52 +02:00
nvbn
b4b599df80 Update readme 2015-04-22 16:52:09 +02:00
nvbn
69ddd82bae Bump to 1.27 2015-04-22 16:46:06 +02:00
nvbn
e7b78205f4 Add transparent sudo support for rules where it required 2015-04-22 16:45:38 +02:00
nvbn
7010b3a7f6 #43 Add test for rm_root 2015-04-22 16:22:10 +02:00
nvbn
3a9c2cc204 Merge branch 'SpyCheese-patch-1' 2015-04-22 16:09:09 +02:00
nvbn
fa4e4522b7 #43 Add rm_root as disabled by default rule 2015-04-22 16:08:54 +02:00
nvbn
14ef5c7d1c Merge branch 'patch-1' of git://github.com/SpyCheese/thefuck into SpyCheese-patch-1 2015-04-22 16:03:20 +02:00
nvbn
957209bdb6 Add ability to bundle disabled by default rules 2015-04-22 15:59:44 +02:00
nvbn
8376fed459 Merge branch 'master' of github.com:nvbn/thefuck 2015-04-22 06:03:34 +02:00
nvbn
5d424dad88 Use colorama for colored output 2015-04-22 06:03:06 +02:00
nvbn
126194ec2e Put errors in stderr instead of "echo ..." in stdout 2015-04-22 05:29:44 +02:00
Vladimir Iakovlev
6b54a3a072 Merge pull request #88 from Dugucloud/master
Added sudo rule for Fedora yum's output.
2015-04-22 05:15:24 +02:00
Dugucloud
79fb7c987c Added sudo rule for Fedora yum's output. 2015-04-22 09:26:45 +08:00
秋纫
d2356c570e Merge pull request #1 from nvbn/master
Synchronize with nvbn's repo.
2015-04-22 09:23:20 +08:00
nvbn
d1b1465f4e Bump to 1.26 2015-04-21 22:31:01 +02:00
nvbn
564eb55262 Merge branch 'master' of github.com:nvbn/thefuck 2015-04-21 22:30:38 +02:00
nvbn
20f8a4ad17 Bump to 1.24 2015-04-21 22:30:15 +02:00
Vladimir Iakovlev
a794b58729 Merge pull request #86 from dionyziz/switch_lang_greek
Add Greek to switch lang
2015-04-21 22:19:23 +02:00
nvbn
fdd6144f88 Merge branch 'nicwest-ssh-known-hosts' 2015-04-21 22:11:10 +02:00
nvbn
d1416a6c2a #82 Remove unned print, fix python 3 support 2015-04-21 22:10:53 +02:00
Dionysis Zindros
4f10fe647d Add tests for greek langage 2015-04-21 22:09:48 +02:00
nvbn
3df77b5bad Merge branch 'ssh-known-hosts' of git://github.com/nicwest/thefuck into nicwest-ssh-known-hosts 2015-04-21 22:06:21 +02:00
Vladimir Iakovlev
da013c5c99 Merge pull request #84 from SanketDG/test_cd_parent
Add tests for cd_parent command.
2015-04-21 22:01:57 +02:00
Dionysis Zindros
4b8d4926aa Add Greek to switch lang 2015-04-21 22:00:05 +02:00
SanketDG
2a7cbef3b5 add tests for cd_parent 2015-04-21 23:41:49 +05:30
Nic West
943613a194 add thing for when known hosts have changed 2015-04-21 17:05:52 +01:00
Lukas Vacek
93b6a623e1 adding rule to run "sudo apt-get install" 2015-04-21 17:59:44 +02:00
Vladimir Iakovlev
5b97992d50 Merge pull request #77 from madmatt112/master
Fix spelling mistake
2015-04-21 16:55:39 +02:00
Matthew Field
3f21d5fc3f Fix spelling mistake 2015-04-21 08:47:14 -06:00
Vladimir Iakovlev
d90e093fb7 Merge pull request #75 from installgen2/patch-1
Fix broken settings link in README
2015-04-21 14:53:08 +02:00
Gen2
8e18ff6eab Fix broken settings link in README 2015-04-21 13:46:38 +01:00
nvbn
54d82f9528 Bump version 2015-04-21 14:41:28 +02:00
nvbn
888756d519 #74 Don't fail when rule throws exception 2015-04-21 14:40:52 +02:00
nvbn
d5b4bddc4c #74 Don't fail when runned without args 2015-04-21 14:26:45 +02:00
nvbn
d09238a6e8 Merge branch 'master' of github.com:nvbn/thefuck
Conflicts:
	thefuck/rules/sudo.py
2015-04-21 14:23:31 +02:00
nvbn
c6c3756caf Merge branch 'soheilpro-sudo-rule-root-privilege' 2015-04-21 14:22:34 +02:00
Vladimir Iakovlev
275574beae Merge pull request #73 from Dugucloud/master
Added a string which could be thrown by Fedora's new dnf package manager.
2015-04-21 14:21:53 +02:00
Dugucloud
de4b774134 Added a string which could be thrown by Fedora's new dnf package manager. 2015-04-21 19:43:10 +08:00
Soheil Rashidi
3af5c80d29 Add 'root privilege' pattern to sudo rule. 2015-04-21 12:57:35 +04:30
nvbn
bd5f5045aa #71 Handle iterdir iterator fails 2015-04-21 08:57:35 +02:00
nvbn
798928b5ad #71 Don't fail on non-exists dir in $PATH 2015-04-21 08:45:45 +02:00
nvbn
82e2c89472 Fix version number 2015-04-21 08:40:17 +02:00
SpyCheese
ceeccf1cd7 Update rm_root.py
Okay, there was an incorrect match function.
2015-04-19 10:21:46 +05:00
SpyCheese
f113bae59d Update rm_root.py 2015-04-19 09:12:19 +05:00
SpyCheese
2a79a5e413 Create rm_root.py 2015-04-19 09:03:34 +05:00
56 changed files with 1624 additions and 216 deletions

103
README.md
View File

@@ -1,5 +1,7 @@
# The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck) # The Fuck [![Build Status](https://travis-ci.org/nvbn/thefuck.svg)](https://travis-ci.org/nvbn/thefuck)
**Aliases changed in 1.34.**
Magnificent app which corrects your previous console command, Magnificent app which corrects your previous console command,
inspired by a [@liamosaur](https://twitter.com/liamosaur/) inspired by a [@liamosaur](https://twitter.com/liamosaur/)
[tweet](https://twitter.com/liamosaur/status/506975850596536320). [tweet](https://twitter.com/liamosaur/status/506975850596536320).
@@ -71,8 +73,8 @@ REPL-y 0.3.1
... ...
``` ```
If you are scary to blindly run changed command, there's `require_confirmation` If you are scared to blindly run changed command, there's `require_confirmation`
[settings](#Settings) option: [settings](#settings) option:
```bash ```bash
➜ apt-get install vim ➜ apt-get install vim
@@ -88,8 +90,8 @@ Reading package lists... Done
## Requirements ## Requirements
- python (2.7+ or 3.3+)
- pip - pip
- python
- python-dev - python-dev
## Installation ## Installation
@@ -100,40 +102,30 @@ Install `The Fuck` with `pip`:
sudo pip install thefuck sudo pip install thefuck
``` ```
If it fails try to use `easy_install`: [Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
And add to `.bashrc` or `.bash_profile`(for OSX):
```bash ```bash
sudo easy_install thefuck alias fuck='eval $(thefuck $(fc -ln -1)); history -r'
``` # You can use whatever you want as an alias, like for Mondays:
And add to `.bashrc` or `.zshrc` or `.bash_profile`(for OSX):
```bash
alias fuck='$(thefuck $(fc -ln -1))'
# You can use whatever you want as an alias, like for mondays:
alias FUCK='fuck' alias FUCK='fuck'
``` ```
Or in `config.fish`: Or in your `.zshrc`:
```fish ```bash
function fuck alias fuck='eval $(thefuck $(fc -ln -1 | tail -n 1)); fc -R'
eval (thefuck $history[1])
end
``` ```
Or in your Powershell `$PROFILE` on Windows: Alternatively, you can redirect the output of `thefuck-alias`:
```powershell ```bash
function fuck { thefuck-alias >> ~/.bashrc
$fuck = $(thefuck (get-history -count 1).commandline)
if($fuck.startswith("echo")) {
$fuck.substring(5)
}
else { iex "$fuck" }
}
``` ```
[Or in your shell config (Bash, Zsh, Fish, Powershell).](https://github.com/nvbn/thefuck/wiki/Shell-aliases)
Changes will be available only in a new shell session. Changes will be available only in a new shell session.
@@ -148,18 +140,32 @@ sudo pip install thefuck --upgrade
The Fuck tries to match rule for the previous command, create new command The Fuck tries to match rule for the previous command, create new command
using matched rule and run it. Rules enabled by default: using matched rule and run it. Rules enabled by default:
* `brew_unknown_command` &ndash; fixes wrong brew commands, for example `brew docto/brew doctor`;
* `cd_parent` &ndash; changes `cd..` to `cd ..`; * `cd_parent` &ndash; changes `cd..` to `cd ..`;
* `cd_mkdir` &ndash; creates directories before cd'ing into them;
* `cp_omitting_directory` &ndash; adds `-a` when you `cp` directory; * `cp_omitting_directory` &ndash; adds `-a` when you `cp` directory;
* `fix_alt_space` &ndash; replaces Alt+Space with Space character;
* `git_no_command` &ndash; fixes wrong git commands like `git brnch`; * `git_no_command` &ndash; fixes wrong git commands like `git brnch`;
* `git_push` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`; * `git_push` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`;
* `has_exists_script` &ndash; prepends `./` when script/binary exists; * `has_exists_script` &ndash; prepends `./` when script/binary exists;
* `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`; * `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`;
* `mkdir_p` &ndash; adds `-p` when you trying to create directory without parent; * `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_command` &ndash; fixes wrong console commands, for example `vom/vim`;
* `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_command` &ndash; prepends `python` when you trying to run not executable/without `./` python script;
* `sl_ls` &ndash; changes `sl` to `ls`;
* `rm_dir` &ndash; adds `-rf` when you trying to remove directory; * `rm_dir` &ndash; adds `-rf` when you trying to remove directory;
* `ssh_known_hosts` &ndash; removes host from `known_hosts` on warning;
* `sudo` &ndash; prepends `sudo` to previous command if it failed because of permissions; * `sudo` &ndash; prepends `sudo` to previous command if it failed because of permissions;
* `switch_layout` &ndash; switches command from your local layout to en. * `switch_layout` &ndash; switches command from your local layout to en;
* `apt_get` &ndash; installs app from apt if it not installed;
* `brew_install` &ndash; fixes formula name for `brew install`;
* `composer_not_command` &ndash; fixes composer command name.
Bundled, but not enabled by default:
* `ls_lah` &ndash; adds -lah to ls;
* `rm_root` &ndash; adds `--no-preserve-root` to `rm -rf /` command.
## Creating your own rules ## Creating your own rules
@@ -167,10 +173,13 @@ For adding your own rule you should create `your-rule-name.py`
in `~/.thefuck/rules`. Rule should contain two functions: in `~/.thefuck/rules`. Rule should contain two functions:
`match(command: Command, settings: Settings) -> bool` `match(command: Command, settings: Settings) -> bool`
and `get_new_command(command: Command, settings: Settings) -> str`. 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`
`Command` has three attributes: `script`, `stdout` and `stderr`. `Command` has three attributes: `script`, `stdout` and `stderr`.
`Settings` is `~/.thefuck/settings.py`. `Settings` is a special object filled with `~/.thefuck/settings.py` and values from env, [more](#settings).
Simple example of the rule for running script with `sudo`: Simple example of the rule for running script with `sudo`:
@@ -182,6 +191,12 @@ def match(command, settings):
def get_new_command(command, settings): def get_new_command(command, settings):
return 'sudo {}'.format(command.script) return 'sudo {}'.format(command.script)
# Optional:
enabled_by_default = True
def side_effect(command, settings):
subprocess.call('chmod 777 .', shell=True)
``` ```
[More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules), [More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules),
@@ -191,9 +206,35 @@ def get_new_command(command, settings):
The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`: The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`:
* `rules` &ndash; list of enabled rules, by default all; * `rules` &ndash; list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` &ndash; require confirmation before running new command, by default `False`; * `require_confirmation` &ndash; require confirmation before running new command, by default `False`;
* `wait_command` &ndash; max amount of time in seconds for getting previous command output. * `wait_command` &ndash; max amount of time in seconds for getting previous command output;
* `no_colors` &ndash; disable colored output.
Example of `settings.py`:
```python
rules = ['sudo', 'no_command']
require_confirmation = True
wait_command = 10
no_colors = False
```
Or via environment variables:
* `THEFUCK_RULES` &ndash; list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`;
* `THEFUCK_REQUIRE_CONFIRMATION` &ndash; require confirmation before running new command, `true/false`;
* `THEFUCK_WAIT_COMMAND` &ndash; max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` &ndash; disable colored output, `true/false`.
For example:
```bash
export THEFUCK_RULES='sudo:no_command'
export THEFUCK_REQUIRE_CONFIRMATION='true'
export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false'
```
## Developing ## Developing

31
release.py Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python
from subprocess import call
import re
version = None
def get_new_setup_py_lines():
global version
with open('setup.py', 'r') as sf:
current_setup = sf.readlines()
for line in current_setup:
if line.startswith('VERSION = '):
major, minor = re.findall(r"VERSION = '(\d+)\.(\d+)'", line)[0]
version = "{}.{}".format(major, int(minor) + 1)
yield "VERSION = '{}'\n".format(version)
else:
yield line
lines = list(get_new_setup_py_lines())
with open('setup.py', 'w') as sf:
sf.writelines(lines)
call('git pull', shell=True)
call('git commit -am "Bump to {}"'.format(version), shell=True)
call('git tag {}'.format(version), shell=True)
call('git push', shell=True)
call('git push --tags', shell=True)
call('python setup.py sdist upload', shell=True)

View File

@@ -1,16 +1,20 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
VERSION = '1.35'
setup(name='thefuck', setup(name='thefuck',
version=1.20, version=VERSION,
description="Magnificent app which corrects your previous console command", description="Magnificent app which corrects your previous console command",
author='Vladimir Iakovlev', author='Vladimir Iakovlev',
author_email='nvbn.rm@gmail.com', author_email='nvbn.rm@gmail.com',
url='https://github.com/nvbn/thefuck', url='https://github.com/nvbn/thefuck',
license='MIT', license='MIT',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), packages=find_packages(exclude=['ez_setup', 'examples',
'tests', 'release']),
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=['pathlib', 'psutil'], install_requires=['pathlib', 'psutil', 'colorama', 'six'],
entry_points={'console_scripts': [ entry_points={'console_scripts': [
'thefuck = thefuck.main:main']}) 'thefuck = thefuck.main:main', 'thefuck-alias = thefuck.main:alias']})

0
tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,49 @@
import pytest
from thefuck.rules.brew_install import match, get_new_command
from thefuck.rules.brew_install import brew_formulas
from tests.utils import Command
@pytest.fixture
def brew_no_available_formula():
return '''Error: No available formula for elsticsearch '''
@pytest.fixture
def brew_install_no_argument():
return '''This command requires a formula argument'''
@pytest.fixture
def brew_already_installed():
return '''Warning: git-2.3.5 already installed'''
def _is_not_okay_to_test():
if 'elasticsearch' not in brew_formulas:
return True
return False
@pytest.mark.skipif(_is_not_okay_to_test(),
reason='No need to run if there\'s no formula')
def test_match(brew_no_available_formula, brew_already_installed,
brew_install_no_argument):
assert match(Command('brew install elsticsearch',
stderr=brew_no_available_formula), None)
assert not match(Command('brew install git',
stderr=brew_already_installed), None)
assert not match(Command('brew install', stderr=brew_install_no_argument),
None)
@pytest.mark.skipif(_is_not_okay_to_test(),
reason='No need to run if there\'s no formula')
def test_get_new_command(brew_no_available_formula):
assert get_new_command(Command('brew install elsticsearch',
stderr=brew_no_available_formula), None)\
== 'brew install elasticsearch'
assert get_new_command(Command('brew install aa',
stderr=brew_no_available_formula),
None) != 'brew install aha'

View File

@@ -0,0 +1,28 @@
import pytest
from thefuck.rules.brew_unknown_command import match, get_new_command
from thefuck.rules.brew_unknown_command import brew_commands
from tests.utils import Command
@pytest.fixture
def brew_unknown_cmd():
return '''Error: Unknown command: inst'''
@pytest.fixture
def brew_unknown_cmd2():
return '''Error: Unknown command: instaa'''
def test_match(brew_unknown_cmd):
assert match(Command('brew inst', stderr=brew_unknown_cmd), None)
for command in brew_commands:
assert not match(Command('brew ' + command), None)
def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd2):
assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd),
None) == 'brew list'
assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd2),
None) == 'brew install'

View File

@@ -0,0 +1,25 @@
import pytest
from thefuck.rules.cd_mkdir import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='cd foo', stderr='cd: foo: No such file or directory'),
Command(script='cd foo/bar/baz',
stderr='cd: foo: No such file or directory'),
Command(script='cd foo/bar/baz', stderr='cd: can\'t cd to foo/bar/baz')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command', [
Command(script='cd foo', stderr=''), Command()])
def test_not_match(command):
assert not match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('cd foo'), 'mkdir -p foo && cd foo'),
(Command('cd foo/bar/baz'), 'mkdir -p foo/bar/baz && cd foo/bar/baz')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,12 @@
from thefuck.rules.cd_parent import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('cd..', stderr='cd..: command not found'), None)
assert not match(Command(), None)
def test_get_new_command():
assert get_new_command(
Command('cd..'), None) == 'cd ..'

View File

@@ -0,0 +1,53 @@
import pytest
from thefuck.rules.composer_not_command import match, get_new_command
from tests.utils import Command
@pytest.fixture
def composer_not_command():
return """
[InvalidArgumentException]
Command "udpate" is not defined.
Did you mean this?
update
"""
@pytest.fixture
def composer_not_command_one_of_this():
return """
[InvalidArgumentException]
Command "pdate" is not defined.
Did you mean one of these?
selfupdate
self-update
update
"""
def test_match(composer_not_command, composer_not_command_one_of_this):
assert match(Command('composer udpate',
stderr=composer_not_command), None)
assert match(Command('composer pdate',
stderr=composer_not_command_one_of_this), None)
assert not match(Command('ls update', stderr=composer_not_command),
None)
def test_get_new_command(composer_not_command, composer_not_command_one_of_this):
assert get_new_command(Command('composer udpate',
stderr=composer_not_command), None) \
== 'composer update'
assert get_new_command(
Command('composer pdate', stderr=composer_not_command_one_of_this),
None) == 'composer selfupdate'

View File

@@ -0,0 +1,22 @@
# -*- encoding: utf-8 -*-
from thefuck.rules.fix_alt_space import match, get_new_command
from tests.utils import Command
def test_match():
"""The character before 'grep' is Alt+Space, which happens frequently
on the Mac when typing the pipe character (Alt+7), and holding the Alt
key pressed for longer than necessary.
"""
assert match(Command(u'ps -ef | grep foo',
stderr=u'-bash:  grep: command not found'), None)
assert not match(Command('ps -ef | grep foo'), None)
assert not match(Command(), None)
def test_get_new_command():
""" Replace the Alt+Space character by a simple space """
assert get_new_command(Command(u'ps -ef | grep foo'), None)\
== 'ps -ef | grep foo'

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from thefuck.main import Command
from thefuck.rules.git_not_command import match, get_new_command from thefuck.rules.git_not_command import match, get_new_command
from tests.utils import Command
@pytest.fixture @pytest.fixture
@@ -31,14 +31,14 @@ def git_command():
def test_match(git_not_command, git_command, git_not_command_one_of_this): def test_match(git_not_command, git_command, git_not_command_one_of_this):
assert match(Command('git brnch', '', git_not_command), None) assert match(Command('git brnch', stderr=git_not_command), None)
assert match(Command('git st', '', git_not_command_one_of_this), None) assert match(Command('git st', stderr=git_not_command_one_of_this), None)
assert not match(Command('ls brnch', '', git_not_command), None) assert not match(Command('ls brnch', stderr=git_not_command), None)
assert not match(Command('git branch', '', git_command), None) assert not match(Command('git branch', stderr=git_command), None)
def test_get_new_command(git_not_command, git_not_command_one_of_this): def test_get_new_command(git_not_command, git_not_command_one_of_this):
assert get_new_command(Command('git brnch', '', git_not_command), None)\ assert get_new_command(Command('git brnch', stderr=git_not_command), None)\
== 'git branch' == 'git branch'
assert get_new_command( assert get_new_command(Command('git st', stderr=git_not_command_one_of_this),
Command('git st', '', git_not_command_one_of_this), None) == 'git status' None) == 'git status'

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from thefuck.main import Command
from thefuck.rules.git_push import match, get_new_command from thefuck.rules.git_push import match, get_new_command
from tests.utils import Command
@pytest.fixture @pytest.fixture
@@ -14,11 +14,11 @@ To push the current branch and set the remote as upstream, use
def test_match(stderr): def test_match(stderr):
assert match(Command('git push master', '', stderr), None) assert match(Command('git push master', stderr=stderr), None)
assert not match(Command('git push master', '', ''), None) assert not match(Command('git push master'), None)
assert not match(Command('ls', '', stderr), None) assert not match(Command('ls', stderr=stderr), None)
def test_get_new_command(stderr): def test_get_new_command(stderr):
assert get_new_command(Command('', '', stderr), None)\ assert get_new_command(Command(stderr=stderr), None)\
== "git push --set-upstream origin master" == "git push --set-upstream origin master"

View File

@@ -1,5 +1,5 @@
from mock import Mock, patch from mock import Mock, patch
from thefuck.rules. has_exists_script import match, get_new_command from thefuck.rules.has_exists_script import match, get_new_command
def test_match(): def test_match():

View File

@@ -0,0 +1,13 @@
from mock import patch, Mock
from thefuck.rules.ls_lah import match, get_new_command
def test_match():
assert match(Mock(script='ls file.py'), None)
assert match(Mock(script='ls /opt'), None)
assert not match(Mock(script='ls -lah /opt'), None)
def test_get_new_command():
assert get_new_command(Mock(script='ls file.py'), None) == 'ls -lah file.py'
assert get_new_command(Mock(script='ls'), None) == 'ls -lah'

View File

@@ -1,13 +1,22 @@
from thefuck.main import Command import pytest
from thefuck.rules.mkdir_p import match, get_new_command from thefuck.rules.mkdir_p import match, get_new_command
from tests.utils import Command
def test_match(): def test_match():
assert match(Command('mkdir foo/bar/baz', '', 'mkdir: foo/bar: No such file or directory'), None) assert match(Command('mkdir foo/bar/baz',
assert not match(Command('mkdir foo/bar/baz', '', ''), None) stderr='mkdir: foo/bar: No such file or directory'),
assert not match(Command('mkdir foo/bar/baz', '', 'foo bar baz'), None) None)
assert not match(Command('', '', ''), None)
@pytest.mark.parametrize('command', [
Command('mkdir foo/bar/baz'),
Command('mkdir foo/bar/baz', stderr='foo bar baz'),
Command()])
def test_not_match(command):
assert not match(command, None)
def test_get_new_command(): def test_get_new_command():
assert get_new_command(Command('mkdir foo/bar/baz', '', ''), None) == 'mkdir -p foo/bar/baz' assert get_new_command(Command('mkdir foo/bar/baz'), None)\
== 'mkdir -p foo/bar/baz'

View File

@@ -0,0 +1,25 @@
import pytest
from thefuck.rules.pip_unknown_command import match, get_new_command
from tests.utils import Command
@pytest.fixture
def pip_unknown_cmd():
return '''ERROR: unknown command "instatl" - maybe you meant "install"'''
@pytest.fixture
def pip_unknown_cmd_without_recommend():
return '''ERROR: unknown command "i"'''
def test_match(pip_unknown_cmd, pip_unknown_cmd_without_recommend):
assert match(Command('pip instatl', stderr=pip_unknown_cmd), None)
assert not match(Command('pip i',
stderr=pip_unknown_cmd_without_recommend),
None)
def test_get_new_command(pip_unknown_cmd):
assert get_new_command(Command('pip instatl', stderr=pip_unknown_cmd),
None) == 'pip install'

View File

@@ -1,9 +1,12 @@
from thefuck.main import Command
from thefuck.rules.python_command import match, get_new_command from thefuck.rules.python_command import match, get_new_command
from tests.utils import Command
def test_match(): def test_match():
assert match(Command('temp.py', '', 'Permission denied'), None) assert match(Command('temp.py', stderr='Permission denied'), None)
assert not match(Command('', '', ''), None) assert not match(Command(), None)
def test_get_new_command(): def test_get_new_command():
assert get_new_command(Command('./test_sudo.py', '', ''), None) == 'python ./test_sudo.py' assert get_new_command(Command('./test_sudo.py'), None)\
== 'python ./test_sudo.py'

View File

@@ -1,12 +1,20 @@
from thefuck.main import Command import pytest
from thefuck.rules.rm_dir import match, get_new_command from thefuck.rules.rm_dir import match, get_new_command
from tests.utils import Command
def test_match(): @pytest.mark.parametrize('command', [
assert match(Command('rm foo', '', 'rm: foo: is a directory'), None) Command('rm foo', stderr='rm: foo: is a directory'),
assert not match(Command('rm foo', '', ''), None) Command('rm foo', stderr='rm: foo: Is a directory')])
assert not match(Command('rm foo', '', 'foo bar baz'), None) def test_match(command):
assert not match(Command('', '', ''), None) assert match(command, None)
assert match(command, None)
@pytest.mark.parametrize('command', [
Command('rm foo'), Command('rm foo'), Command()])
def test_not_match(command):
assert not match(command, None)
def test_get_new_command(): def test_get_new_command():

View File

@@ -0,0 +1,21 @@
import pytest
from thefuck.rules.rm_root import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command(script='rm -rf /',
stderr='add --no-preserve-root'), None)
@pytest.mark.parametrize('command', [
Command(script='ls', stderr='add --no-preserve-root'),
Command(script='rm --no-preserve-root /', stderr='add --no-preserve-root'),
Command(script='rm -rf /', stderr='')])
def test_not_match(command):
assert not match(command, None)
def test_get_new_command():
assert get_new_command(Command(script='rm -rf /'), None) \
== 'rm -rf / --no-preserve-root'

12
tests/rules/test_sl_ls.py Normal file
View File

@@ -0,0 +1,12 @@
from thefuck.rules.sl_ls import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('sl'), None)
assert not match(Command('ls'), None)
def test_get_new_command():
assert get_new_command(Command('sl'), None) == 'ls'

View File

@@ -0,0 +1,66 @@
import os
import pytest
from mock import Mock
from thefuck.rules.ssh_known_hosts import match, get_new_command,\
side_effect
from tests.utils import Command
@pytest.fixture
def ssh_error(tmpdir):
path = os.path.join(str(tmpdir), 'known_hosts')
def reset(path):
with open(path, 'w') as fh:
lines = [
'123.234.567.890 asdjkasjdakjsd\n'
'98.765.432.321 ejioweojwejrosj\n'
'111.222.333.444 qwepoiwqepoiss\n'
]
fh.writelines(lines)
def known_hosts(path):
with open(path, 'r') as fh:
return fh.readlines()
reset(path)
errormsg = u"""@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
b6:cb:07:34:c0:a0:94:d3:0d:69:83:31:f4:c5:20:9b.
Please contact your system administrator.
Add correct host key in {0} to get rid of this message.
Offending RSA key in {0}:2
RSA host key for {1} has changed and you have requested strict checking.
Host key verification failed.""".format(path, '98.765.432.321')
return errormsg, path, reset, known_hosts
def test_match(ssh_error):
errormsg, _, _, _ = ssh_error
assert match(Command('ssh', stderr=errormsg), None)
assert match(Command('ssh', stderr=errormsg), None)
assert match(Command('scp something something', stderr=errormsg), None)
assert match(Command('scp something something', stderr=errormsg), None)
assert not match(Command(stderr=errormsg), None)
assert not match(Command('notssh', stderr=errormsg), None)
assert not match(Command('ssh'), None)
def test_side_effect(ssh_error):
errormsg, path, reset, known_hosts = ssh_error
command = Command('ssh user@host', stderr=errormsg)
side_effect(command, None)
expected = ['123.234.567.890 asdjkasjdakjsd\n', '111.222.333.444 qwepoiwqepoiss\n']
assert known_hosts(path) == expected
def test_get_new_command(ssh_error, monkeypatch):
errormsg, _, _, _ = ssh_error
assert get_new_command(Command('ssh user@host', stderr=errormsg), None) == 'ssh user@host'

View File

@@ -1,13 +1,21 @@
from thefuck.main import Command import pytest
from thefuck.rules.sudo import match, get_new_command from thefuck.rules.sudo import match, get_new_command
from tests.utils import Command
def test_match(): @pytest.mark.parametrize('stderr, stdout', [
assert match(Command('', '', 'Permission denied'), None) ('Permission denied', ''),
assert match(Command('', '', 'permission denied'), None) ('permission denied', ''),
assert match(Command('', '', "npm ERR! Error: EACCES, unlink"), None) ("npm ERR! Error: EACCES, unlink", ''),
assert not match(Command('', '', ''), None) ('requested operation requires superuser privilege', ''),
('', "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)
def test_not_match():
assert not match(Command(), None)
def test_get_new_command(): def test_get_new_command():
assert get_new_command(Command('ls', '', ''), None) == 'sudo ls' assert get_new_command(Command('ls'), None) == 'sudo ls'

View File

@@ -1,18 +1,27 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
from mock import Mock import pytest
from thefuck.rules import switch_lang from thefuck.rules import switch_lang
from tests.utils import Command
def test_match(): @pytest.mark.parametrize('command', [
assert switch_lang.match(Mock(stderr='command not found: фзе-пуе', Command(stderr='command not found: фзе-пуе', script=u'фзе-пуе'),
script=u'фзе-пуе'), None) Command(stderr='command not found: λσ', script=u'λσ')])
assert not switch_lang.match(Mock(stderr='command not found: pat-get', def test_match(command):
script=u'pat-get'), None) assert switch_lang.match(command, None)
assert not switch_lang.match(Mock(stderr='some info',
script=u'фзе-пуе'), None)
def test_get_new_command(): @pytest.mark.parametrize('command', [
assert switch_lang.get_new_command( Command(stderr='command not found: pat-get', script=u'pat-get'),
Mock(script=u'фзе-пуе штыефдд мшь'), None) == 'apt-get install vim' Command(stderr='command not found: ls', script=u'ls'),
Command(stderr='some info', script=u'фзе-пуе')])
def test_not_match(command):
assert not switch_lang.match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command(u'фзе-пуе штыефдд мшь'), 'apt-get install vim'),
(Command(u'λσ -λα'), 'ls -la')])
def test_get_new_command(command, new_command):
assert switch_lang.get_new_command(command, None) == new_command

100
tests/test_conf.py Normal file
View File

@@ -0,0 +1,100 @@
import pytest
import six
from mock import Mock
from thefuck import conf
from tests.utils import Rule
@pytest.mark.parametrize('enabled, rules, result', [
(True, conf.DEFAULT_RULES, True),
(False, conf.DEFAULT_RULES, False),
(False, conf.DEFAULT_RULES + ['test'], True)])
def test_default(enabled, rules, result):
assert (Rule('test', enabled_by_default=enabled) in rules) == result
@pytest.fixture
def load_source(monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.conf.load_source', mock)
return mock
@pytest.fixture
def environ(monkeypatch):
data = {}
monkeypatch.setattr('thefuck.conf.os.environ', data)
return data
@pytest.mark.usefixture('environ')
def test_settings_defaults(load_source):
load_source.return_value = object()
for key, val in conf.DEFAULT_SETTINGS.items():
assert getattr(conf.get_settings(Mock()), key) == val
@pytest.mark.usefixture('environ')
class TestSettingsFromFile(object):
def test_from_file(self, load_source):
load_source.return_value = Mock(rules=['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)
settings = conf.get_settings(Mock())
assert settings.rules == ['test']
assert settings.wait_command == 10
assert settings.require_confirmation is True
assert settings.no_colors is True
def test_from_file_with_DEFAULT(self, load_source):
load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)
settings = conf.get_settings(Mock())
assert settings.rules == conf.DEFAULT_RULES + ['test']
@pytest.mark.usefixture('load_source')
class TestSettingsFromEnv(object):
def test_from_env(self, environ):
environ.update({'THEFUCK_RULES': 'bash:lisp',
'THEFUCK_WAIT_COMMAND': '55',
'THEFUCK_REQUIRE_CONFIRMATION': 'true',
'THEFUCK_NO_COLORS': 'false'})
settings = conf.get_settings(Mock())
assert settings.rules == ['bash', 'lisp']
assert settings.wait_command == 55
assert settings.require_confirmation is True
assert settings.no_colors is False
def test_from_env_with_DEFAULT(self, environ):
environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})
settings = conf.get_settings(Mock())
assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp']
class TestInitializeSettingsFile(object):
def test_ignore_if_exists(self):
settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock())
user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock))
conf.initialize_settings_file(user_dir_mock)
assert settings_path_mock.is_file.call_count == 1
assert not settings_path_mock.open.called
def test_create_if_doesnt_exists(self):
settings_file = six.StringIO()
settings_path_mock = Mock(
is_file=Mock(return_value=False),
open=Mock(return_value=Mock(
__exit__=lambda *args: None, __enter__=lambda *args: settings_file)))
user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock))
conf.initialize_settings_file(user_dir_mock)
settings_file_contents = settings_file.getvalue()
assert settings_path_mock.is_file.call_count == 1
assert settings_path_mock.open.call_count == 1
assert conf.SETTINGS_HEADER in settings_file_contents
for setting in conf.DEFAULT_SETTINGS.items():
assert '# {} = {}\n'.format(*setting) in settings_file_contents
settings_file.close()

7
tests/test_logs.py Normal file
View File

@@ -0,0 +1,7 @@
from mock import Mock
from thefuck import logs
def test_color():
assert logs.color('red', Mock(no_colors=False)) == 'red'
assert logs.color('red', Mock(no_colors=True)) == ''

View File

@@ -1,97 +1,155 @@
import pytest
from subprocess import PIPE from subprocess import PIPE
from pathlib import PosixPath, Path from pathlib import PosixPath, Path
from mock import patch, Mock from mock import Mock
from thefuck import main from thefuck import main, conf, types
from tests.utils import Rule, Command
def test_get_settings(): def test_load_rule(monkeypatch):
with patch('thefuck.main.load_source', return_value=Mock(rules=['bash'])):
assert main.get_settings(Path('/')).rules == ['bash']
with patch('thefuck.main.load_source', return_value=Mock(spec=[])):
assert main.get_settings(Path('/')).rules is None
def test_is_rule_enabled():
assert main.is_rule_enabled(Mock(rules=None), Path('bash.py'))
assert main.is_rule_enabled(Mock(rules=['bash']), Path('bash.py'))
assert not main.is_rule_enabled(Mock(rules=['bash']), Path('lisp.py'))
def test_load_rule():
match = object() match = object()
get_new_command = object() get_new_command = object()
with patch('thefuck.main.load_source', load_source = Mock()
return_value=Mock( load_source.return_value = Mock(match=match,
match=match, get_new_command=get_new_command,
get_new_command=get_new_command)) as load_source: enabled_by_default=True)
assert main.load_rule(Path('/rules/bash.py')) == main.Rule(match, get_new_command) monkeypatch.setattr('thefuck.main.load_source', load_source)
load_source.assert_called_once_with('bash', '/rules/bash.py') assert main.load_rule(Path('/rules/bash.py')) \
== Rule('bash', match, get_new_command)
load_source.assert_called_once_with('bash', '/rules/bash.py')
def test_get_rules(): @pytest.mark.parametrize('conf_rules, rules', [
with patch('thefuck.main.Path.glob') as glob, \ (conf.DEFAULT_RULES, [Rule('bash', 'bash', 'bash'),
patch('thefuck.main.load_source', Rule('lisp', 'lisp', 'lisp'),
lambda x, _: Mock(match=x, get_new_command=x)): Rule('bash', 'bash', 'bash'),
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] Rule('lisp', 'lisp', 'lisp')]),
assert main.get_rules( (types.RulesNamesList(['bash']), [Rule('bash', 'bash', 'bash'),
Path('~'), Rule('bash', 'bash', 'bash')])])
Mock(rules=None)) == [main.Rule('bash', 'bash'), def test_get_rules(monkeypatch, conf_rules, rules):
main.Rule('lisp', 'lisp'), monkeypatch.setattr(
main.Rule('bash', 'bash'), 'thefuck.main.Path.glob',
main.Rule('lisp', 'lisp')] lambda *_: [PosixPath('bash.py'), PosixPath('lisp.py')])
assert main.get_rules( monkeypatch.setattr('thefuck.main.load_source',
Path('~'), lambda x, _: Mock(match=x, get_new_command=x,
Mock(rules=['bash'])) == [main.Rule('bash', 'bash'), enabled_by_default=True))
main.Rule('bash', 'bash')] assert list(main.get_rules(Path('~'), Mock(rules=conf_rules))) == rules
def test_get_command(): class TestGetCommand(object):
with patch('thefuck.main.Popen') as Popen, \ @pytest.fixture(autouse=True)
patch('thefuck.main.os.environ', def Popen(self, monkeypatch):
new_callable=lambda: {}), \ Popen = Mock()
patch('thefuck.main.wait_output',
return_value=True):
Popen.return_value.stdout.read.return_value = b'stdout' Popen.return_value.stdout.read.return_value = b'stdout'
Popen.return_value.stderr.read.return_value = b'stderr' Popen.return_value.stderr.read.return_value = b'stderr'
assert main.get_command(Mock(), ['thefuck', 'apt-get', monkeypatch.setattr('thefuck.main.Popen', Popen)
'search', 'vim']) \ return Popen
== main.Command('apt-get search vim', 'stdout', 'stderr')
@pytest.fixture(autouse=True)
def prepare(self, monkeypatch):
monkeypatch.setattr('thefuck.main.os.environ', {})
monkeypatch.setattr('thefuck.main.wait_output', lambda *_: True)
@pytest.fixture(autouse=True)
def generic_shell(self, monkeypatch):
monkeypatch.setattr('thefuck.shells.from_shell', lambda x: x)
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_get_command_calls(self, Popen):
assert main.get_command(Mock(),
['thefuck', 'apt-get', 'search', 'vim']) \
== Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim', Popen.assert_called_once_with('apt-get search vim',
shell=True, shell=True,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
env={'LANG': 'C'}) env={'LANG': 'C'})
@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
else:
assert main.get_command(Mock(), args) is None
def test_get_matched_rule(): class TestGetMatchedRule(object):
rules = [main.Rule(lambda x, _: x.script == 'cd ..', None), def test_no_match(self):
main.Rule(lambda *_: False, None)] assert main.get_matched_rule(
assert main.get_matched_rule(main.Command('ls', '', ''), Command('ls'), [Rule('', lambda *_: False)],
rules, None) is None Mock(no_colors=True)) is None
assert main.get_matched_rule(main.Command('cd ..', '', ''),
rules, None) == rules[0] def test_match(self):
rule = Rule('', lambda x, _: x.script == 'cd ..')
assert main.get_matched_rule(
Command('cd ..'), [rule], Mock(no_colors=True)) == rule
def test_when_rule_failed(self, capsys):
main.get_matched_rule(
Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))],
Mock(no_colors=True))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
def test_run_rule(capsys): class TestRunRule(object):
with patch('thefuck.main.confirm', return_value=True): @pytest.fixture(autouse=True)
main.run_rule(main.Rule(None, lambda *_: 'new-command'), def confirm(self, monkeypatch):
None, None) mock = Mock(return_value=True)
monkeypatch.setattr('thefuck.main.confirm', mock)
return mock
def test_run_rule(self, capsys):
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
Command(), None)
assert capsys.readouterr() == ('new-command\n', '') assert capsys.readouterr() == ('new-command\n', '')
with patch('thefuck.main.confirm', return_value=False):
main.run_rule(main.Rule(None, lambda *_: 'new-command'), def test_run_rule_with_side_effect(self, capsys):
None, None) side_effect = Mock()
settings = Mock()
command = Command()
main.run_rule(Rule(get_new_command=lambda *_: 'new-command',
side_effect=side_effect),
command, settings)
assert capsys.readouterr() == ('new-command\n', '')
side_effect.assert_called_once_with(command, settings)
def test_when_not_comfirmed(self, capsys, confirm):
confirm.return_value = False
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
Command(), None)
assert capsys.readouterr() == ('', '') assert capsys.readouterr() == ('', '')
def test_confirm(capsys): class TestConfirm(object):
# When confirmation not required: @pytest.fixture
assert main.confirm('command', Mock(require_confirmation=False)) def stdin(self, monkeypatch):
assert capsys.readouterr() == ('', 'command\n') mock = Mock(return_value='\n')
# When confirmation required and confirmed: monkeypatch.setattr('sys.stdin.read', mock)
with patch('thefuck.main.sys.stdin.read', return_value='\n'): return mock
assert main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]') def test_when_not_required(self, capsys):
# When confirmation required and ctrl+c: assert main.confirm('command', None, Mock(require_confirmation=False))
with patch('thefuck.main.sys.stdin.read', side_effect=KeyboardInterrupt): assert capsys.readouterr() == ('', 'command\n')
assert not main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]Aborted\n') def test_with_side_effect_and_without_confirmation(self, capsys):
assert main.confirm('command', Mock(), Mock(require_confirmation=False))
assert capsys.readouterr() == ('', 'command*\n')
# `stdin` fixture should be applied after `capsys`
def test_when_confirmation_required_and_confirmed(self, capsys, stdin):
assert main.confirm('command', None, Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]')
# `stdin` fixture should be applied after `capsys`
def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin):
assert main.confirm('command', Mock(), Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command* [enter/ctrl+c]')
def test_when_confirmation_required_and_aborted(self, capsys, stdin):
stdin.side_effect = KeyboardInterrupt
assert not main.confirm('command', None, Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n')

85
tests/test_shells.py Normal file
View File

@@ -0,0 +1,85 @@
import pytest
from mock import Mock, MagicMock
from thefuck import shells
@pytest.fixture
def builtins_open(monkeypatch):
mock = MagicMock()
monkeypatch.setattr('six.moves.builtins.open', mock)
return mock
@pytest.fixture
def isfile(monkeypatch):
mock = Mock(return_value=True)
monkeypatch.setattr('os.path.isfile', mock)
return mock
class TestGeneric(object):
def test_from_shell(self):
assert shells.Generic().from_shell('pwd') == 'pwd'
def test_to_shell(self):
assert shells.Generic().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open):
assert shells.Generic().put_to_history('ls') is None
assert builtins_open.call_count == 0
@pytest.mark.usefixtures('isfile')
class TestBash(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.shells.Popen', mock)
mock.return_value.stdout.read.return_value = (
b'alias l=\'ls -CF\'\n'
b'alias la=\'ls -A\'\n'
b'alias ll=\'ls -alF\'')
return mock
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after):
assert shells.Bash().from_shell(before) == after
def test_to_shell(self):
assert shells.Bash().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open):
shells.Bash().put_to_history('ls')
builtins_open.return_value.__enter__.return_value.\
write.assert_called_once_with('ls\n')
@pytest.mark.usefixtures('isfile')
class TestZsh(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.shells.Popen', mock)
mock.return_value.stdout.read.return_value = (
b'l=\'ls -CF\'\n'
b'la=\'ls -A\'\n'
b'll=\'ls -alF\'')
return mock
@pytest.mark.parametrize('before, after', [
('pwd', 'pwd'),
('ll', 'ls -alF')])
def test_from_shell(self, before, after):
assert shells.Zsh().from_shell(before) == after
def test_to_shell(self):
assert shells.Zsh().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, monkeypatch):
monkeypatch.setattr('thefuck.shells.time',
lambda: 1430707243.3517463)
shells.Zsh().put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(': 1430707243:0;ls\n')

16
tests/test_types.py Normal file
View File

@@ -0,0 +1,16 @@
from thefuck.types import RulesNamesList, Settings
from tests.utils import Rule
def test_rules_names_list():
assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp']
assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp'])
assert Rule('lisp') in RulesNamesList(['lisp'])
assert Rule('bash') not in RulesNamesList(['lisp'])
def test_update_settings():
settings = Settings({'key': 'val'})
new_settings = settings.update(key='new-val')
assert new_settings.key == 'new-val'
assert settings.key == 'val'

26
tests/test_utils.py Normal file
View File

@@ -0,0 +1,26 @@
import pytest
from mock import Mock
from thefuck.utils import sudo_support, wrap_settings
from thefuck.types import Settings
from tests.utils import Command
@pytest.mark.parametrize('override, old, new', [
({'key': 'val'}, {}, {'key': 'val'}),
({'key': 'new-val'}, {'key': 'val'}, {'key': 'new-val'})])
def test_wrap_settings(override, old, new):
fn = lambda _, settings: settings
assert wrap_settings(override)(fn)(None, Settings(old)) == new
@pytest.mark.parametrize('return_value, command, called, result', [
('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'),
('ls -lah', 'ls', 'ls', 'ls -lah'),
(True, 'sudo ls', 'ls', True),
(True, 'ls', 'ls', True),
(False, 'sudo ls', 'ls', False),
(False, 'ls', 'ls', False)])
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)

13
tests/utils.py Normal file
View File

@@ -0,0 +1,13 @@
from thefuck import types
def Command(script='', stdout='', stderr=''):
return types.Command(script, stdout, stderr)
def Rule(name='', match=lambda *_: True,
get_new_command=lambda *_: '',
enabled_by_default=True,
side_effect=None):
return types.Rule(name, match, get_new_command,
enabled_by_default, side_effect)

116
thefuck/conf.py Normal file
View File

@@ -0,0 +1,116 @@
from copy import copy
from imp import load_source
import os
import sys
from six import text_type
from . import logs, types
class _DefaultRulesNames(types.RulesNamesList):
def __add__(self, items):
return _DefaultRulesNames(list(self) + items)
def __contains__(self, item):
return item.enabled_by_default or \
super(_DefaultRulesNames, self).__contains__(item)
def __eq__(self, other):
if isinstance(other, _DefaultRulesNames):
return super(_DefaultRulesNames, self).__eq__(other)
else:
return False
DEFAULT_RULES = _DefaultRulesNames([])
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3,
'require_confirmation': False,
'no_colors': False}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
#
# The rules are defined as in the example bellow:
#
# rules = ['cd_parent', 'git_push', 'python_command', 'sudo']
#
# The default values are as follows. Uncomment and change to fit your needs.
# See https://github.com/nvbn/thefuck#settings for more information.
#
"""
def _settings_from_file(user_dir):
"""Loads settings from file."""
settings = load_source('settings',
text_type(user_dir.joinpath('settings.py')))
return {key: getattr(settings, key)
for key in DEFAULT_SETTINGS.keys()
if hasattr(settings, key)}
def _rules_from_env(val):
"""Transforms rules list from env-string to python."""
val = val.split(':')
if 'DEFAULT_RULES' in val:
val = DEFAULT_RULES + [rule for rule in val if rule != 'DEFAULT_RULES']
return val
def _val_from_env(env, attr):
"""Transforms env-strings to python."""
val = os.environ[env]
if attr == 'rules':
val = _rules_from_env(val)
elif attr == 'wait_command':
val = int(val)
elif attr in ('require_confirmation', 'no_colors'):
val = val.lower() == 'true'
return val
def _settings_from_env():
"""Loads settings from env."""
return {attr: _val_from_env(env, attr)
for env, attr in ENV_TO_ATTR.items()
if env in os.environ}
def get_settings(user_dir):
"""Returns settings filled with values from `settings.py` and env."""
conf = copy(DEFAULT_SETTINGS)
try:
conf.update(_settings_from_file(user_dir))
except Exception:
logs.exception("Can't load settings from file",
sys.exc_info(),
types.Settings(conf))
try:
conf.update(_settings_from_env())
except Exception:
logs.exception("Can't load settings from env",
sys.exc_info(),
types.Settings(conf))
if not isinstance(conf['rules'], types.RulesNamesList):
conf['rules'] = types.RulesNamesList(conf['rules'])
return types.Settings(conf)
def initialize_settings_file(user_dir):
settings_path = user_dir.joinpath('settings.py')
if not settings_path.is_file():
with settings_path.open(mode='w') as settings_file:
settings_file.write(SETTINGS_HEADER)
for setting in DEFAULT_SETTINGS.items():
settings_file.write(u'# {} = {}\n'.format(*setting))

54
thefuck/logs.py Normal file
View File

@@ -0,0 +1,54 @@
import sys
from traceback import format_exception
import colorama
def color(color_, settings):
"""Utility for ability to disabling colored output."""
if settings.no_colors:
return ''
else:
return color_
def exception(title, exc_info, settings):
sys.stderr.write(
u'{warn}[WARN] {title}:{reset}\n{trace}'
u'{warn}----------------------------{reset}\n\n'.format(
warn=color(colorama.Back.RED + colorama.Fore.WHITE
+ colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings),
title=title,
trace=''.join(format_exception(*exc_info))))
def rule_failed(rule, exc_info, settings):
exception('Rule {}'.format(rule.name), exc_info, settings)
def show_command(new_command, side_effect, settings):
sys.stderr.write('{bold}{command}{side_effect}{reset}\n'.format(
command=new_command,
side_effect='*' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, side_effect, settings):
sys.stderr.write(
'{bold}{command}{side_effect}{reset} '
'[{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command,
side_effect='*' if side_effect else '',
bold=color(colorama.Style.BRIGHT, settings),
green=color(colorama.Fore.GREEN, settings),
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
sys.stderr.flush()
def failed(msg, settings):
sys.stderr.write('{red}{msg}{reset}\n'.format(
msg=msg,
red=color(colorama.Fore.RED, settings),
reset=color(colorama.Style.RESET_ALL, settings)))

View File

@@ -1,4 +1,3 @@
from collections import namedtuple
from imp import load_source from imp import load_source
from pathlib import Path from pathlib import Path
from os.path import expanduser from os.path import expanduser
@@ -6,10 +5,9 @@ from subprocess import Popen, PIPE
import os import os
import sys import sys
from psutil import Process, TimeoutExpired from psutil import Process, TimeoutExpired
import colorama
import six
Command = namedtuple('Command', ('script', 'stdout', 'stderr')) from . import logs, conf, types, shells
Rule = namedtuple('Rule', ('match', 'get_new_command'))
def setup_user_dir(): def setup_user_dir():
@@ -18,42 +16,30 @@ def setup_user_dir():
rules_dir = user_dir.joinpath('rules') rules_dir = user_dir.joinpath('rules')
if not rules_dir.is_dir(): if not rules_dir.is_dir():
rules_dir.mkdir(parents=True) rules_dir.mkdir(parents=True)
user_dir.joinpath('settings.py').touch() conf.initialize_settings_file(user_dir)
return user_dir return user_dir
def get_settings(user_dir):
"""Returns prepared settings module."""
settings = load_source('settings',
str(user_dir.joinpath('settings.py')))
settings.__dict__.setdefault('rules', None)
settings.__dict__.setdefault('wait_command', 3)
settings.__dict__.setdefault('require_confirmation', False)
return settings
def is_rule_enabled(settings, rule):
"""Returns `True` when rule mentioned in `rules` or `rules`
isn't defined.
"""
return settings.rules is None or rule.name[:-3] in settings.rules
def load_rule(rule): def load_rule(rule):
"""Imports rule module and returns it.""" """Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule)) rule_module = load_source(rule.name[:-3], str(rule))
return Rule(rule_module.match, rule_module.get_new_command) return types.Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True),
getattr(rule_module, 'side_effect', None))
def get_rules(user_dir, settings): def get_rules(user_dir, settings):
"""Returns all enabled rules.""" """Returns all enabled rules."""
bundled = Path(__file__).parent\ bundled = Path(__file__).parent \
.joinpath('rules')\ .joinpath('rules') \
.glob('*.py') .glob('*.py')
user = user_dir.joinpath('rules').glob('*.py') user = user_dir.joinpath('rules').glob('*.py')
return [load_rule(rule) for rule in sorted(list(bundled)) + list(user) for rule in sorted(list(bundled)) + list(user):
if rule.name != '__init__.py' and is_rule_enabled(settings, rule)] if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
def wait_output(settings, popen): def wait_output(settings, popen):
@@ -76,66 +62,72 @@ def wait_output(settings, popen):
def get_command(settings, args): def get_command(settings, args):
"""Creates command from `args` and executes it.""" """Creates command from `args` and executes it."""
if sys.version_info[0] < 3: if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in args[1:]) script = ' '.join(arg.decode('utf-8') for arg in args[1:])
else: else:
script = ' '.join(args[1:]) script = ' '.join(args[1:])
if not script:
return
script = shells.from_shell(script)
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C')) env=dict(os.environ, LANG='C'))
if wait_output(settings, result): if wait_output(settings, result):
return Command(script, result.stdout.read().decode('utf-8'), return types.Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8')) result.stderr.read().decode('utf-8'))
def get_matched_rule(command, rules, settings): def get_matched_rule(command, rules, settings):
"""Returns first matched rule for command.""" """Returns first matched rule for command."""
for rule in rules: for rule in rules:
if rule.match(command, settings): try:
return rule if rule.match(command, settings):
return rule
except Exception:
logs.rule_failed(rule, sys.exc_info(), settings)
def confirm(new_command, settings): def confirm(new_command, side_effect, settings):
"""Returns `True` when running of new command confirmed.""" """Returns `True` when running of new command confirmed."""
if not settings.require_confirmation: if not settings.require_confirmation:
sys.stderr.write(new_command + '\n') logs.show_command(new_command, side_effect, settings)
return True return True
sys.stderr.write(new_command + ' [Enter/Ctrl+C]') logs.confirm_command(new_command, side_effect, settings)
sys.stderr.flush()
try: try:
sys.stdin.read(1) sys.stdin.read(1)
return True return True
except KeyboardInterrupt: except KeyboardInterrupt:
sys.stderr.write('Aborted\n') logs.failed('Aborted', settings)
return False return False
def run_rule(rule, command, settings): def run_rule(rule, command, settings):
"""Runs command from rule for passed command.""" """Runs command from rule for passed command."""
new_command = rule.get_new_command(command, settings) new_command = shells.to_shell(rule.get_new_command(command, settings))
if confirm(new_command, settings): if confirm(new_command, rule.side_effect, settings):
if rule.side_effect:
rule.side_effect(command, settings)
shells.put_to_history(new_command)
print(new_command) print(new_command)
def is_second_run(command): def alias():
"""Is it the second run of `fuck`?""" print(shells.app_alias())
return command.script.startswith('fuck')
def main(): def main():
colorama.init()
user_dir = setup_user_dir() user_dir = setup_user_dir()
settings = get_settings(user_dir) settings = conf.get_settings(user_dir)
command = get_command(settings, sys.argv) command = get_command(settings, sys.argv)
if command: if command:
if is_second_run(command):
print("echo Can't fuck twice")
return
rules = get_rules(user_dir, settings) rules = get_rules(user_dir, settings)
matched_rule = get_matched_rule(command, rules, settings) matched_rule = get_matched_rule(command, rules, settings)
if matched_rule: if matched_rule:
run_rule(matched_rule, command, settings) run_rule(matched_rule, command, settings)
return return
print('echo No fuck given') logs.failed('No fuck given', settings)

23
thefuck/rules/apt_get.py Normal file
View File

@@ -0,0 +1,23 @@
try:
import CommandNotFound
except ImportError:
enabled_by_default = False
def match(command, settings):
if 'not found' in command.stderr:
try:
c = CommandNotFound.CommandNotFound()
pkgs = c.getPackages(command.script.split(" ")[0])
name, _ = pkgs[0]
return True
except IndexError:
# IndexError is thrown when no matching package is found
return False
def get_new_command(command, settings):
c = CommandNotFound.CommandNotFound()
pkgs = c.getPackages(command.script.split(" ")[0])
name, _ = pkgs[0]
return "sudo apt-get install {} && {}".format(name, command.script)

View File

@@ -0,0 +1,43 @@
import difflib
import os
import re
from subprocess import check_output
import thefuck.logs
# Formulars are base on each local system's status
brew_formulas = []
try:
brew_path_prefix = check_output(['brew', '--prefix']).strip()
brew_formula_path = brew_path_prefix + '/Library/Formula'
for file_name in os.listdir(brew_formula_path):
if file_name.endswith('.rb'):
brew_formulas.append(file_name.replace('.rb', ''))
except:
pass
def _get_similar_formulars(formula_name):
return difflib.get_close_matches(formula_name, brew_formulas, 1, 0.85)
def match(command, settings):
is_proper_command = ('brew install' in command.script and
'No available formula' in command.stderr)
has_possible_formulas = False
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
return has_possible_formulas
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]
return command.script.replace(not_exist_formula, exist_formula, 1)

View File

@@ -0,0 +1,102 @@
import difflib
import os
import re
import subprocess
BREW_CMD_PATH = '/Library/Homebrew/cmd'
TAP_PATH = '/Library/Taps'
TAP_CMD_PATH = '/%s/%s/cmd'
def _get_brew_path_prefix():
"""To get brew path"""
try:
return subprocess.check_output(['brew', '--prefix']).strip()
except:
return None
def _get_brew_commands(brew_path_prefix):
"""To get brew default commands on local environment"""
brew_cmd_path = brew_path_prefix + BREW_CMD_PATH
commands = [name.replace('.rb', '') for name in os.listdir(brew_cmd_path)
if name.endswith('.rb')]
return commands
def _get_brew_tap_specific_commands(brew_path_prefix):
"""To get tap's specific commands
https://github.com/Homebrew/homebrew/blob/master/Library/brew.rb#L115"""
commands = []
brew_taps_path = brew_path_prefix + TAP_PATH
for user in _get_directory_names_only(brew_taps_path):
taps = _get_directory_names_only(brew_taps_path + '/%s' % user)
# Brew Taps's naming rule
# https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/brew-tap.md#naming-conventions-and-limitations
taps = (tap for tap in taps if tap.startswith('homebrew-'))
for tap in taps:
tap_cmd_path = brew_taps_path + TAP_CMD_PATH % (user, tap)
if os.path.isdir(tap_cmd_path):
commands += (name.replace('brew-', '').replace('.rb', '')
for name in os.listdir(tap_cmd_path)
if _is_brew_tap_cmd_naming(name))
return commands
def _is_brew_tap_cmd_naming(name):
if name.startswith('brew-') and name.endswith('.rb'):
return True
return False
def _get_directory_names_only(path):
return [d for d in os.listdir(path)
if os.path.isdir(os.path.join(path, d))]
brew_path_prefix = _get_brew_path_prefix()
# Failback commands for testing (Based on Homebrew 0.9.5)
brew_commands = ['info', 'home', 'options', 'install', 'uninstall',
'search', 'list', 'update', 'upgrade', 'pin', 'unpin',
'doctor', 'create', 'edit']
if brew_path_prefix:
try:
brew_commands = _get_brew_commands(brew_path_prefix) \
+ _get_brew_tap_specific_commands(brew_path_prefix)
except OSError:
pass
def _get_similar_commands(command):
return difflib.get_close_matches(command, brew_commands)
def match(command, settings):
is_proper_command = ('brew' in command.script and
'Unknown command' in command.stderr)
has_possible_commands = False
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
return has_possible_commands
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]
return command.script.replace(broken_cmd, new_cmd, 1)

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

@@ -0,0 +1,14 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return (command.script.startswith('cd ')
and ('no such file or directory' in command.stderr.lower()
or 'cd: can\'t cd to' in command.stderr.lower()))
@sudo_support
def get_new_command(command, settings):
return re.sub(r'^cd (.*)', 'mkdir -p \\1 && cd \\1', command.script)

View File

@@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('composer' in command.script
and ('did you mean this?' in command.stderr.lower()
or 'did you mean one of these?' in command.stderr.lower()))
def get_new_command(command, settings):
broken_cmd = re.findall(r"Command \"([^']*)\" is not defined", command.stderr)[0]
new_cmd = re.findall(r'Did you mean this\?[^\n]*\n\s*([^\n]*)', command.stderr)
if not new_cmd:
new_cmd = re.findall(r'Did you mean one of these\?[^\n]*\n\s*([^\n]*)', command.stderr)
return command.script.replace(broken_cmd, new_cmd[0].strip(), 1)

View File

@@ -1,10 +1,13 @@
import re import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings): def match(command, settings):
return command.script.startswith('cp ') \ return command.script.startswith('cp ') \
and 'cp: omitting directory' in command.stderr.lower() and 'cp: omitting directory' in command.stderr.lower()
@sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
return re.sub(r'^cp', 'cp -a', command.script) return re.sub(r'^cp', 'cp -a', command.script)

View File

@@ -0,0 +1,15 @@
# -*- encoding: utf-8 -*-
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return ('command not found' in command.stderr.lower()
and u' ' in command.script)
@sudo_support
def get_new_command(command, settings):
return re.sub(u' ', ' ', command.script)

View File

@@ -1,11 +1,14 @@
import os import os
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings): def match(command, settings):
return os.path.exists(command.script.split()[0]) \ return os.path.exists(command.script.split()[0]) \
and 'command not found' in command.stderr and 'command not found' in command.stderr
@sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
return u'./{}'.format(command.script) return u'./{}'.format(command.script)

View File

@@ -1,12 +1,15 @@
import re import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings): def match(command, settings):
return (command.script.startswith('lein') return (command.script.startswith('lein')
and "is not a task. See 'lein help'" in command.stderr and "is not a task. See 'lein help'" in command.stderr
and 'Did you mean this?' in command.stderr) and 'Did you mean this?' in command.stderr)
@sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
broken_cmd = re.findall(r"'([^']*)' is not a task", broken_cmd = re.findall(r"'([^']*)' is not a task",
command.stderr)[0] command.stderr)[0]

11
thefuck/rules/ls_lah.py Normal file
View File

@@ -0,0 +1,11 @@
enabled_by_default = False
def match(command, settings):
return 'ls' in command.script and not ('ls -' in command.script)
def get_new_command(command, settings):
command = command.script.split(' ')
command[0] = 'ls -lah'
return ' '.join(command)

View File

@@ -1,9 +1,13 @@
import re import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings): def match(command, settings):
return ('mkdir' in command.script return ('mkdir' in command.script
and 'No such file or directory' in command.stderr) and 'No such file or directory' in command.stderr)
@sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
return re.sub('^mkdir (.*)', 'mkdir -p \\1', command.script) return re.sub('^mkdir (.*)', 'mkdir -p \\1', command.script)

View File

@@ -1,6 +1,7 @@
from difflib import get_close_matches from difflib import get_close_matches
import os import os
from pathlib import Path from pathlib import Path
from thefuck.utils import sudo_support
def _safe(fn, fallback): def _safe(fn, fallback):
@@ -12,17 +13,19 @@ def _safe(fn, fallback):
def _get_all_bins(): def _get_all_bins():
return [exe.name return [exe.name
for path in os.environ['PATH'].split(':') for path in os.environ.get('PATH', '').split(':')
for exe in _safe(Path(path).iterdir, []) for exe in _safe(lambda: list(Path(path).iterdir()), [])
if not _safe(exe.is_dir, True)] if not _safe(exe.is_dir, True)]
@sudo_support
def match(command, settings): def match(command, settings):
return 'not found' in command.stderr and \ return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0], bool(get_close_matches(command.script.split(' ')[0],
_get_all_bins())) _get_all_bins()))
@sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
old_command = command.script.split(' ')[0] old_command = command.script.split(' ')[0]
new_command = get_close_matches(old_command, new_command = get_close_matches(old_command,

View File

@@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('pip' in command.script and
'unknown command' in command.stderr and
'maybe you meant' in command.stderr)
def get_new_command(command, settings):
broken_cmd = re.findall(r'ERROR: unknown command \"([a-z]+)\"',
command.stderr)[0]
new_cmd = re.findall(r'maybe you meant \"([a-z]+)\"', command.stderr)[0]
return command.script.replace(broken_cmd, new_cmd, 1)

View File

@@ -1,7 +1,10 @@
from thefuck.utils import sudo_support
# add 'python' suffix to the command if # add 'python' suffix to the command if
# 1) The script does not have execute permission or # 1) The script does not have execute permission or
# 2) is interpreted as shell script # 2) is interpreted as shell script
@sudo_support
def match(command, settings): def match(command, settings):
toks = command.script.split() toks = command.script.split()
return (len(toks) > 0 return (len(toks) > 0
@@ -10,5 +13,6 @@ def match(command, settings):
'command not found' in command.stderr)) 'command not found' in command.stderr))
@sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
return 'python ' + command.script return 'python ' + command.script

View File

@@ -1,9 +1,13 @@
import re import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings): def match(command, settings):
return ('rm' in command.script return ('rm' in command.script
and 'is a directory' in command.stderr) and 'is a directory' in command.stderr.lower())
@sudo_support
def get_new_command(command, settings): def get_new_command(command, settings):
return re.sub('^rm (.*)', 'rm -rf \\1', command.script) return re.sub('^rm (.*)', 'rm -rf \\1', command.script)

16
thefuck/rules/rm_root.py Normal file
View File

@@ -0,0 +1,16 @@
from thefuck.utils import sudo_support
enabled_by_default = False
@sudo_support
def match(command, settings):
return ({'rm', '/'}.issubset(command.script.split())
and '--no-preserve-root' not in command.script
and '--no-preserve-root' in command.stderr)
@sudo_support
def get_new_command(command, settings):
return u'{} --no-preserve-root'.format(command.script)

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

@@ -0,0 +1,14 @@
"""
This happens way too often
When typing really fast cause I'm a 1337 H4X0R,
I often fuck up 'ls' and type 'sl'. No more!
"""
def match(command, settings):
return command.script == 'sl'
def get_new_command(command, settings):
return 'ls'

View File

@@ -0,0 +1,36 @@
import re
patterns = [
r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!',
r'WARNING: POSSIBLE DNS SPOOFING DETECTED!',
r"Warning: the \S+ host key for '([^']+)' differs from the key for the IP address '([^']+)'",
]
offending_pattern = re.compile(
r'(?:Offending (?:key for IP|\S+ key)|Matching host key) in ([^:]+):(\d+)',
re.MULTILINE)
commands = ['ssh', 'scp']
def match(command, settings):
if not command.script:
return False
if not command.script.split()[0] in commands:
return False
if not any([re.findall(pattern, command.stderr) for pattern in patterns]):
return False
return True
def get_new_command(command, settings):
return command.script
def side_effect(command, settings):
offending = offending_pattern.findall(command.stderr)
for filepath, lineno in offending:
with open(filepath, 'r') as fh:
lines = fh.readlines()
del lines[int(lineno) - 1]
with open(filepath, 'w') as fh:
fh.writelines(lines)

View File

@@ -3,12 +3,21 @@ patterns = ['permission denied',
'pkg: Insufficient privileges', 'pkg: Insufficient privileges',
'you cannot perform this operation unless you are root', 'you cannot perform this operation unless you are root',
'non-root users cannot', 'non-root users cannot',
'Operation not permitted'] 'Operation not permitted',
'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']
def match(command, settings): def match(command, settings):
for pattern in patterns: for pattern in patterns:
if pattern.lower() in command.stderr.lower(): if pattern.lower() in command.stderr.lower()\
or pattern.lower() in command.stdout.lower():
return True return True
return False return False

View File

@@ -3,7 +3,8 @@
target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?''' target_layout = '''qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?'''
source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''', source_layouts = [u'''йцукенгшщзхъфывапролджэячсмитьбю.ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,''',
u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟'''] u'''ضصثقفغعهخحجچشسیبلاتنمکگظطزرذدپو./ًٌٍَُِّْ][}{ؤئيإأآة»«:؛كٓژٰ‌ٔء><؟''',
u''';ςερτυθιοπ[]ασδφγηξκλ΄ζχψωβνμ,./:΅ΕΡΤΥΘΙΟΠ{}ΑΣΔΦΓΗΞΚΛ¨"ΖΧΨΩΒΝΜ<>?''']
def _get_matched_layout(command): def _get_matched_layout(command):

120
thefuck/shells.py Normal file
View File

@@ -0,0 +1,120 @@
"""Module with shell specific actions, each shell class should
implement `from_shell`, `to_shell`, `app_alias` and `put_to_history`
methods.
"""
from collections import defaultdict
from subprocess import Popen, PIPE
from time import time
import os
from psutil import Process
FNULL = open(os.devnull, 'w')
class Generic(object):
def _get_aliases(self):
return {}
def _expand_aliases(self, command_script):
aliases = self._get_aliases()
binary = command_script.split(' ')[0]
if binary in aliases:
return command_script.replace(binary, aliases[binary], 1)
else:
return command_script
def from_shell(self, command_script):
"""Prepares command before running in app."""
return self._expand_aliases(command_script)
def to_shell(self, command_script):
"""Prepares command for running in shell."""
return command_script
def app_alias(self):
return "\nalias fuck='eval $(thefuck $(fc -ln -1))'\n"
def _get_history_file_name(self):
return ''
def _get_history_line(self, command_script):
return ''
def put_to_history(self, command_script):
"""Puts command script to shell history."""
history_file_name = self._get_history_file_name()
if os.path.isfile(history_file_name):
with open(history_file_name, 'a') as history:
history.write(self._get_history_line(command_script))
class Bash(Generic):
def _parse_alias(self, alias):
name, value = alias.replace('alias ', '', 1).split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
def _get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=FNULL, shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.bash_history'))
def _get_history_line(self, command_script):
return u'{}\n'.format(command_script)
class Zsh(Generic):
def _parse_alias(self, alias):
name, value = alias.split('=', 1)
if value[0] == value[-1] == '"' or value[0] == value[-1] == "'":
value = value[1:-1]
return name, value
def _get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=FNULL, shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
if alias)
def _get_history_file_name(self):
return os.environ.get("HISTFILE",
os.path.expanduser('~/.zsh_history'))
def _get_history_line(self, command_script):
return u': {}:0;{}\n'.format(int(time()), command_script)
shells = defaultdict(lambda: Generic(), {
'bash': Bash(),
'zsh': Zsh()})
def _get_shell():
shell = Process(os.getpid()).parent().cmdline()[0]
return shells[shell]
def from_shell(command):
return _get_shell().from_shell(command)
def to_shell(command):
return _get_shell().to_shell(command)
def app_alias():
return _get_shell().app_alias()
def put_to_history(command):
return _get_shell().put_to_history(command)

26
thefuck/types.py Normal file
View File

@@ -0,0 +1,26 @@
from collections import namedtuple
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default', 'side_effect'))
class RulesNamesList(list):
"""Wrapper a top of list for storing rules names."""
def __contains__(self, item):
return super(RulesNamesList, self).__contains__(item.name)
class Settings(dict):
def __getattr__(self, item):
return self.get(item)
def update(self, **kwargs):
"""Returns new settings with new values from `kwargs`."""
conf = dict(self)
conf.update(kwargs)
return Settings(conf)

View File

@@ -1,5 +1,7 @@
from functools import wraps from functools import wraps
import os import os
import six
from .types import Command
def which(program): def which(program):
@@ -35,9 +37,25 @@ def wrap_settings(params):
def decorator(fn): def decorator(fn):
@wraps(fn) @wraps(fn)
def wrapper(command, settings): def wrapper(command, settings):
for key, val in params.items(): return fn(command, settings.update(**params))
if not hasattr(settings, key):
setattr(settings, key, val)
return fn(command, settings)
return wrapper return wrapper
return decorator return decorator
def sudo_support(fn):
"""Removes sudo before calling fn and adds it after."""
@wraps(fn)
def wrapper(command, settings):
if not command.script.startswith('sudo '):
return fn(command, settings)
result = fn(Command(command.script[5:],
command.stdout,
command.stderr),
settings)
if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result)
else:
return result
return wrapper