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

Compare commits

...

103 Commits
1.23 ... 1.31

Author SHA1 Message Date
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
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
54 changed files with 1193 additions and 176 deletions

View File

@@ -71,8 +71,8 @@ REPL-y 0.3.1
...
```
If you are scary to blindly run changed command, there's `require_confirmation`
[settings](#Settings) option:
If you are scared to blindly run changed command, there's `require_confirmation`
[settings](#settings) option:
```bash
➜ apt-get install vim
@@ -88,8 +88,8 @@ Reading package lists... Done
## Requirements
- python (2.7+ or 3.3+)
- pip
- python
- python-dev
## Installation
@@ -100,39 +100,17 @@ Install `The Fuck` with `pip`:
sudo pip install thefuck
```
If it fails try to use `easy_install`:
```bash
sudo easy_install thefuck
```
[Or using an OS package manager (OS X, Ubuntu, Arch).](https://github.com/nvbn/thefuck/wiki/Installation)
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='eval $(thefuck $(fc -ln -1))'
# You can use whatever you want as an alias, like for Mondays:
alias FUCK='fuck'
```
Or in `config.fish`:
```fish
function fuck
eval (thefuck $history[1])
end
```
Or in your Powershell `$PROFILE` on Windows:
```powershell
function fuck {
$fuck = $(thefuck (get-history -count 1).commandline)
if($fuck.startswith("echo")) {
$fuck.substring(5)
}
else { iex "$fuck" }
}
```
[On 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.
@@ -148,19 +126,30 @@ sudo pip install thefuck --upgrade
The Fuck tries to match rule for the previous command, create new command
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_mkdir` &ndash; creates directories before cd'ing into them;
* `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_push` &ndash; adds `--set-upstream origin $branch` to previous failed `git push`;
* `has_exists_script` &ndash; prepends `./` when script/binary exists;
* `lein_not_task` &ndash; fixes wrong `lein` tasks like `lein rpl`;
* `mkdir_p` &ndash; adds `-p` when you trying to create directory without parent;
* `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;
* `sl_ls` &ndash; changes `sl` to `ls`;
* `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;
* `switch_layout` &ndash; switches command from your local layout to en.
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
For adding your own rule you should create `your-rule-name.py`
@@ -170,7 +159,7 @@ and `get_new_command(command: Command, settings: Settings) -> str`.
`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`:
@@ -191,9 +180,35 @@ def get_new_command(command, settings):
The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`:
* `rules` &ndash; list of enabled rules, by default all;
* `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.
* `rules` &ndash; list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `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;
* `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

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
VERSION = '1.31'
setup(name='thefuck',
version="1.23",
version=VERSION,
description="Magnificent app which corrects your previous console command",
author='Vladimir Iakovlev',
author_email='nvbn.rm@gmail.com',
url='https://github.com/nvbn/thefuck',
license='MIT',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
packages=find_packages(exclude=['ez_setup', 'examples',
'tests', 'release']),
include_package_data=True,
zip_safe=False,
install_requires=['pathlib', 'psutil'],
install_requires=['pathlib', 'psutil', 'colorama', 'six'],
entry_points={'console_scripts': [
'thefuck = thefuck.main:main']})

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_cmd_instaa():
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_cmd_instaa):
assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd), None)\
== 'brew list'
assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd_instaa),
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
from thefuck.main import Command
from thefuck.rules.git_not_command import match, get_new_command
from tests.utils import Command
@pytest.fixture
@@ -31,14 +31,14 @@ def git_command():
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 st', '', git_not_command_one_of_this), None)
assert not match(Command('ls brnch', '', git_not_command), None)
assert not match(Command('git branch', '', git_command), None)
assert match(Command('git brnch', stderr=git_not_command), None)
assert match(Command('git st', stderr=git_not_command_one_of_this), None)
assert not match(Command('ls brnch', stderr=git_not_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):
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'
assert get_new_command(
Command('git st', '', git_not_command_one_of_this), None) == 'git status'
assert get_new_command(Command('git st', stderr=git_not_command_one_of_this),
None) == 'git status'

View File

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

View File

@@ -1,5 +1,5 @@
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():

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 tests.utils import Command
def test_match():
assert match(Command('mkdir foo/bar/baz', '', 'mkdir: foo/bar: No such file or directory'), None)
assert not match(Command('mkdir foo/bar/baz', '', ''), None)
assert not match(Command('mkdir foo/bar/baz', '', 'foo bar baz'), None)
assert not match(Command('', '', ''), None)
assert match(Command('mkdir foo/bar/baz',
stderr='mkdir: foo/bar: No such file or directory'),
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():
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 tests.utils import Command
def test_match():
assert match(Command('temp.py', '', 'Permission denied'), None)
assert not match(Command('', '', ''), None)
assert match(Command('temp.py', stderr='Permission denied'), None)
assert not match(Command(), None)
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 tests.utils import Command
def test_match():
assert match(Command('rm foo', '', 'rm: foo: is a directory'), None)
assert not match(Command('rm foo', '', ''), None)
assert not match(Command('rm foo', '', 'foo bar baz'), None)
assert not match(Command('', '', ''), None)
@pytest.mark.parametrize('command', [
Command('rm foo', stderr='rm: foo: is a directory'),
Command('rm foo', stderr='rm: foo: Is a directory')])
def test_match(command):
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():

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,70 @@
import os
import pytest
from mock import Mock
from thefuck.rules.ssh_known_hosts import match, get_new_command,\
remove_offending_keys
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_remove_offending_keys(ssh_error):
errormsg, path, reset, known_hosts = ssh_error
command = Command('ssh user@host', stderr=errormsg)
remove_offending_keys(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
method = Mock()
monkeypatch.setattr('thefuck.rules.ssh_known_hosts.remove_offending_keys', method)
assert get_new_command(Command('ssh user@host', stderr=errormsg), None) == 'ssh user@host'
assert method.call_count

View File

@@ -1,13 +1,21 @@
from thefuck.main import Command
import pytest
from thefuck.rules.sudo import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('', '', 'Permission denied'), None)
assert match(Command('', '', 'permission denied'), None)
assert match(Command('', '', "npm ERR! Error: EACCES, unlink"), None)
assert not match(Command('', '', ''), None)
@pytest.mark.parametrize('stderr, stdout', [
('Permission denied', ''),
('permission denied', ''),
("npm ERR! Error: EACCES, unlink", ''),
('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():
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 -*-
from mock import Mock
import pytest
from thefuck.rules import switch_lang
from tests.utils import Command
def test_match():
assert switch_lang.match(Mock(stderr='command not found: фзе-пуе',
script=u'фзе-пуе'), None)
assert not switch_lang.match(Mock(stderr='command not found: pat-get',
script=u'pat-get'), None)
assert not switch_lang.match(Mock(stderr='some info',
script=u'фзе-пуе'), None)
@pytest.mark.parametrize('command', [
Command(stderr='command not found: фзе-пуе', script=u'фзе-пуе'),
Command(stderr='command not found: λσ', script=u'λσ')])
def test_match(command):
assert switch_lang.match(command, None)
def test_get_new_command():
assert switch_lang.get_new_command(
Mock(script=u'фзе-пуе штыефдд мшь'), None) == 'apt-get install vim'
@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='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

89
tests/test_conf.py Normal file
View File

@@ -0,0 +1,89 @@
import six
from mock import patch, Mock
from thefuck import conf
from tests.utils import Rule
def test_default():
assert Rule('test', enabled_by_default=True) in conf.DEFAULT_RULES
assert Rule('test', enabled_by_default=False) not in conf.DEFAULT_RULES
assert Rule('test', enabled_by_default=False) in (conf.DEFAULT_RULES + ['test'])
def test_settings_defaults():
with patch('thefuck.conf.load_source', return_value=object()), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
for key, val in conf.DEFAULT_SETTINGS.items():
assert getattr(conf.get_settings(Mock()), key) == val
def test_settings_from_file():
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
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_settings_from_file_with_DEFAULT():
with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT_RULES + ['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
settings = conf.get_settings(Mock())
assert settings.rules == conf.DEFAULT_RULES + ['test']
def test_settings_from_env():
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'],
wait_command=10)), \
patch('thefuck.conf.os.environ',
new_callable=lambda: {'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_settings_from_env_with_DEFAULT():
with patch('thefuck.conf.load_source', return_value=Mock()), \
patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}):
settings = conf.get_settings(Mock())
assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp']
def test_initialize_settings_file_ignore_if_exists():
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_initialize_settings_file_create_if_exists_not():
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,20 +1,8 @@
from subprocess import PIPE
from pathlib import PosixPath, Path
from mock import patch, Mock
from thefuck import main
def test_get_settings():
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'))
from thefuck import main, conf, types
from tests.utils import Rule, Command
def test_load_rule():
@@ -23,26 +11,31 @@ def test_load_rule():
with patch('thefuck.main.load_source',
return_value=Mock(
match=match,
get_new_command=get_new_command)) as load_source:
assert main.load_rule(Path('/rules/bash.py')) == main.Rule('bash', match, get_new_command)
get_new_command=get_new_command,
enabled_by_default=True)) as load_source:
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():
with patch('thefuck.main.Path.glob') as glob, \
patch('thefuck.main.load_source',
lambda x, _: Mock(match=x, get_new_command=x)):
lambda x, _: Mock(match=x, get_new_command=x,
enabled_by_default=True)):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
assert main.get_rules(
assert list(main.get_rules(
Path('~'),
Mock(rules=None)) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('lisp', 'lisp', 'lisp'),
main.Rule('bash', 'bash', 'bash'),
main.Rule('lisp', 'lisp', 'lisp')]
assert main.get_rules(
Mock(rules=conf.DEFAULT_RULES))) \
== [Rule('bash', 'bash', 'bash'),
Rule('lisp', 'lisp', 'lisp'),
Rule('bash', 'bash', 'bash'),
Rule('lisp', 'lisp', 'lisp')]
assert list(main.get_rules(
Path('~'),
Mock(rules=['bash'])) == [main.Rule('bash', 'bash', 'bash'),
main.Rule('bash', 'bash', 'bash')]
Mock(rules=types.RulesNamesList(['bash'])))) \
== [Rule('bash', 'bash', 'bash'),
Rule('bash', 'bash', 'bash')]
def test_get_command():
@@ -55,7 +48,7 @@ def test_get_command():
Popen.return_value.stderr.read.return_value = b'stderr'
assert main.get_command(Mock(), ['thefuck', 'apt-get',
'search', 'vim']) \
== main.Command('apt-get search vim', 'stdout', 'stderr')
== Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdout=PIPE,
@@ -65,24 +58,24 @@ def test_get_command():
def test_get_matched_rule(capsys):
rules = [main.Rule('', lambda x, _: x.script == 'cd ..', None),
main.Rule('', lambda *_: False, None),
main.Rule('rule', Mock(side_effect=OSError('Denied')), None)]
assert main.get_matched_rule(main.Command('ls', '', ''),
rules, None) is None
assert main.get_matched_rule(main.Command('cd ..', '', ''),
rules, None) == rules[0]
assert capsys.readouterr()[1].split('\n')[0]\
== '[WARN] rule: Traceback (most recent call last):'
rules = [Rule('', lambda x, _: x.script == 'cd ..'),
Rule('', lambda *_: False),
Rule('rule', Mock(side_effect=OSError('Denied')))]
assert main.get_matched_rule(Command('ls'),
rules, Mock(no_colors=True)) is None
assert main.get_matched_rule(Command('cd ..'),
rules, Mock(no_colors=True)) == rules[0]
assert capsys.readouterr()[1].split('\n')[0] \
== '[WARN] Rule rule:'
def test_run_rule(capsys):
with patch('thefuck.main.confirm', return_value=True):
main.run_rule(main.Rule('', None, lambda *_: 'new-command'),
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
None, None)
assert capsys.readouterr() == ('new-command\n', '')
with patch('thefuck.main.confirm', return_value=False):
main.run_rule(main.Rule('', None, lambda *_: 'new-command'),
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
None, None)
assert capsys.readouterr() == ('', '')
@@ -93,9 +86,11 @@ def test_confirm(capsys):
assert capsys.readouterr() == ('', 'command\n')
# When confirmation required and confirmed:
with patch('thefuck.main.sys.stdin.read', return_value='\n'):
assert main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]')
assert main.confirm('command', Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]')
# When confirmation required and ctrl+c:
with patch('thefuck.main.sys.stdin.read', side_effect=KeyboardInterrupt):
assert not main.confirm('command', Mock(require_confirmation=True))
assert capsys.readouterr() == ('', 'command [Enter/Ctrl+C]Aborted\n')
assert not main.confirm('command', Mock(require_confirmation=True,
no_colors=True))
assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n')

15
tests/test_types.py Normal file
View File

@@ -0,0 +1,15 @@
from thefuck.types import Rule, RulesNamesList, Settings
def test_rules_names_list():
assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp']
assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp'])
assert Rule('lisp', None, None, False) in RulesNamesList(['lisp'])
assert Rule('bash', None, None, False) 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 @@
from mock import Mock
from thefuck.utils import sudo_support, wrap_settings
from thefuck.types import Settings
from tests.utils import Command
def test_wrap_settings():
fn = lambda _, settings: settings
assert wrap_settings({'key': 'val'})(fn)(None, Settings({})) \
== {'key': 'val'}
assert wrap_settings({'key': 'new-val'})(fn)(
None, Settings({'key': 'val'})) == {'key': 'new-val'}
def test_sudo_support():
fn = Mock(return_value=True, __name__='')
assert sudo_support(fn)(Command('sudo ls'), None)
fn.assert_called_once_with(Command('ls'), None)
fn.return_value = False
assert not sudo_support(fn)(Command('sudo ls'), None)
fn.return_value = 'pwd'
assert sudo_support(fn)(Command('sudo ls'), None) == 'sudo pwd'
assert sudo_support(fn)(Command('ls'), None) == 'pwd'

11
tests/utils.py Normal file
View File

@@ -0,0 +1,11 @@
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):
return types.Rule(name, match, get_new_command, enabled_by_default)

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

51
thefuck/logs.py Normal file
View File

@@ -0,0 +1,51 @@
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, settings):
sys.stderr.write('{bold}{command}{reset}\n'.format(
command=new_command,
bold=color(colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings)))
def confirm_command(new_command, settings):
sys.stderr.write(
'{bold}{command}{reset} [{green}enter{reset}/{red}ctrl+c{reset}]'.format(
command=new_command,
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,16 +1,12 @@
from collections import namedtuple
from imp import load_source
from pathlib import Path
from os.path import expanduser
from subprocess import Popen, PIPE
import os
import sys
from traceback import format_exception
from psutil import Process, TimeoutExpired
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command'))
import colorama
from . import logs, conf, types
def setup_user_dir():
@@ -19,43 +15,29 @@ def setup_user_dir():
rules_dir = user_dir.joinpath('rules')
if not rules_dir.is_dir():
rules_dir.mkdir(parents=True)
user_dir.joinpath('settings.py').touch()
conf.initialize_settings_file(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):
"""Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule))
return Rule(rule.name[:-3], 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))
def get_rules(user_dir, settings):
"""Returns all enabled rules."""
bundled = Path(__file__).parent\
.joinpath('rules')\
.glob('*.py')
bundled = Path(__file__).parent \
.joinpath('rules') \
.glob('*.py')
user = user_dir.joinpath('rules').glob('*.py')
return [load_rule(rule) for rule in sorted(list(bundled)) + list(user)
if rule.name != '__init__.py' and is_rule_enabled(settings, rule)]
for rule in sorted(list(bundled)) + list(user):
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
def wait_output(settings, popen):
@@ -89,8 +71,8 @@ def get_command(settings, args):
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE,
env=dict(os.environ, LANG='C'))
if wait_output(settings, result):
return Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8'))
return types.Command(script, result.stdout.read().decode('utf-8'),
result.stderr.read().decode('utf-8'))
def get_matched_rule(command, rules, settings):
@@ -100,23 +82,21 @@ def get_matched_rule(command, rules, settings):
if rule.match(command, settings):
return rule
except Exception:
sys.stderr.write(u'[WARN] {}: {}---------------------\n\n'.format(
rule.name, ''.join(format_exception(*sys.exc_info()))))
logs.rule_failed(rule, sys.exc_info(), settings)
def confirm(new_command, settings):
"""Returns `True` when running of new command confirmed."""
if not settings.require_confirmation:
sys.stderr.write(new_command + '\n')
logs.show_command(new_command, settings)
return True
sys.stderr.write(new_command + ' [Enter/Ctrl+C]')
sys.stderr.flush()
logs.confirm_command(new_command, settings)
try:
sys.stdin.read(1)
return True
except KeyboardInterrupt:
sys.stderr.write('Aborted\n')
logs.failed('Aborted', settings)
return False
@@ -133,13 +113,14 @@ def is_second_run(command):
def main():
colorama.init()
user_dir = setup_user_dir()
settings = get_settings(user_dir)
settings = conf.get_settings(user_dir)
command = get_command(settings, sys.argv)
if command:
if is_second_run(command):
print("echo Can't fuck twice")
logs.failed("Can't fuck twice", settings)
return
rules = get_rules(user_dir, settings)
@@ -148,4 +129,4 @@ def main():
run_rule(matched_rule, command, settings)
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,33 @@
import difflib
import re
import thefuck.logs
# This commands are based on Homebrew 0.9.5
brew_commands = ['info', 'home', 'options', 'install', 'uninstall', 'search',
'list', 'update', 'upgrade', 'pin', 'unpin', 'doctor',
'create', 'edit']
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
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return command.script.startswith('cp ') \
and 'cp: omitting directory' in command.stderr.lower()
@sudo_support
def get_new_command(command, settings):
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
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return os.path.exists(command.script.split()[0]) \
and 'command not found' in command.stderr
@sudo_support
def get_new_command(command, settings):
return u'./{}'.format(command.script)

View File

@@ -1,12 +1,15 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return (command.script.startswith('lein')
and "is not a task. See 'lein help'" in command.stderr
and 'Did you mean this?' in command.stderr)
@sudo_support
def get_new_command(command, settings):
broken_cmd = re.findall(r"'([^']*)' is not a task",
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
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
return ('mkdir' in command.script
and 'No such file or directory' in command.stderr)
@sudo_support
def get_new_command(command, settings):
return re.sub('^mkdir (.*)', 'mkdir -p \\1', command.script)

View File

@@ -1,6 +1,7 @@
from difflib import get_close_matches
import os
from pathlib import Path
from thefuck.utils import sudo_support
def _safe(fn, fallback):
@@ -17,12 +18,14 @@ def _get_all_bins():
if not _safe(exe.is_dir, True)]
@sudo_support
def match(command, settings):
return 'not found' in command.stderr and \
bool(get_close_matches(command.script.split(' ')[0],
_get_all_bins()))
@sudo_support
def get_new_command(command, settings):
old_command = command.script.split(' ')[0]
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
# 1) The script does not have execute permission or
# 2) is interpreted as shell script
@sudo_support
def match(command, settings):
toks = command.script.split()
return (len(toks) > 0
@@ -10,5 +13,6 @@ def match(command, settings):
'command not found' in command.stderr))
@sudo_support
def get_new_command(command, settings):
return 'python ' + command.script

View File

@@ -1,9 +1,13 @@
import re
from thefuck.utils import sudo_support
@sudo_support
def match(command, settings):
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):
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,37 @@
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 remove_offending_keys(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)
def get_new_command(command, settings):
remove_offending_keys(command, settings)
return command.script

View File

@@ -5,12 +5,16 @@ patterns = ['permission denied',
'non-root users cannot',
'Operation not permitted',
'root privilege',
'This command has to be run under the root user.']
'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']
def match(command, settings):
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 False

View File

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